Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"moment-duration-format": "^2.3.2",
"moment-timezone": "^0.5.33",
"mui-color-input": "^9.0.0",
"openstack-uicore-foundation": "5.0.36",
"openstack-uicore-foundation": "5.0.38-beta.1",
Comment thread
santipalenque marked this conversation as resolved.
Outdated
"p-limit": "^6.1.0",
"path-browserify": "^1.0.1",
"postcss-loader": "^6.2.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,33 +33,15 @@ jest.mock("openstack-uicore-foundation/lib/components", () => {
const React = require("react");
return {
__esModule: true,
MuiFormItemTable: ({ data, onNotesClick, onSettingsClick }) =>
MuiFormItemTable: ({ data }) =>
React.createElement(
"div",
null,
(data || []).map((item) =>
React.createElement(
"div",
{ key: item.form_item_id },
React.createElement("span", null, item.name),
React.createElement(
"button",
{
"aria-label": "edit",
onClick: () => onNotesClick && onNotesClick(item),
type: "button"
},
"edit"
),
React.createElement(
"button",
{
"aria-label": "settings",
onClick: () => onSettingsClick && onSettingsClick(item),
type: "button"
},
"settings"
)
React.createElement("span", null, item.name)
)
)
),
Expand Down Expand Up @@ -432,70 +414,37 @@ describe("EditCartForm", () => {
});

describe("Interactive Features", () => {
test("opens notes modal when notes button clicked", async () => {
test("renders all form items in MuiFormItemTable", async () => {
renderWithStore();

await waitFor(() => {
expect(screen.getByText("Item 1")).toBeInTheDocument();
});

// Find the Edit icon button (notes) by aria-label
const notesButton = screen.getAllByLabelText("edit")[0];

await userEvent.click(notesButton);

await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Item 2")).toBeInTheDocument();
});
});

test("closes notes modal", async () => {
test("renders Save and Cancel buttons", async () => {
renderWithStore();

await waitFor(() => {
expect(screen.getByText("Item 1")).toBeInTheDocument();
});

const notesButton = screen.getAllByLabelText("edit")[0];

await userEvent.click(notesButton);

await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
});

// Find close button within the dialog
const closeButton = screen.getByLabelText("close");

await userEvent.click(closeButton);

await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
expect(screen.getByText(/general.save/)).toBeInTheDocument();
expect(screen.getByText(/general.cancel/)).toBeInTheDocument();
});
});

test("opens settings modal when settings button clicked", async () => {
test("does not render any dialog by default", async () => {
renderWithStore();

await waitFor(() => {
expect(screen.getByText("Item 1")).toBeInTheDocument();
});

const settingsButtons = screen.getAllByRole("button");
const settingsButton = settingsButtons.find(
(btn) => btn.getAttribute("aria-label") === "settings"
);

await userEvent.click(settingsButton);

await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});

describe("Item Field Error Message", () => {
test("shows error message below Save when a required Item field is empty after submit", async () => {
test("blocks form submission when a required Item field is empty", async () => {
const formWithRequiredItemField = {
...mockCartForm,
items: [
Expand All @@ -522,10 +471,9 @@ describe("EditCartForm", () => {

await userEvent.click(screen.getByText(/general.save/));

// Validation errors prevent form submission; updateCartForm should not be called
await waitFor(() => {
expect(
screen.getByText("validation.additional_items")
).toBeInTheDocument();
expect(mockUpdateCartForm).not.toHaveBeenCalled();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* limitations under the License.
* */

import React, { useMemo, useState } from "react";
import React, { useMemo } from "react";
import T from "i18n-react/dist/i18n-react";
import { connect } from "react-redux";
import { FormikProvider, useFormik } from "formik";
Expand All @@ -24,8 +24,6 @@ import {
MuiFormItemTable,
getCurrentApplicableRate
} from "openstack-uicore-foundation/lib/components";
import NotesModal from "openstack-uicore-foundation/lib/components/mui/notes-modal";
import ItemSettingsModal from "openstack-uicore-foundation/lib/components/mui/item-settings-modal";
import { DISCOUNT_TYPES } from "../../../../../../../utils/constants";
import showConfirmDialog from "../../../../../../../components/mui/showConfirmDialog";

Expand All @@ -38,7 +36,8 @@ const parseValue = (item, timeZone) => {
case "RadioButtonList":
case "ComboBox": {
const defaultVal = item.values.find((v) => v.is_default)?.id;
return item.current_value || defaultVal || "";
const val = item.current_value || defaultVal;
return val ? String(val) : "";
}
case "CheckBoxList": {
const defaultVal = item.values.find((v) => v.is_default)?.id;
Expand Down Expand Up @@ -67,77 +66,68 @@ const getYupValidation = (field) => {

switch (field.type) {
case "Quantity": {
schema = yup.number(T.translate("validation.number"));
schema = yup.number();
if (field.minimum_quantity > 0) {
schema = schema.min(
field.minimum_quantity,
T.translate("validation.minimum", { minimum: field.minimum_quantity })
);
schema = schema.min(field.minimum_quantity, " ");
}
if (field.maximum_quantity > 0) {
schema = schema.max(
field.maximum_quantity,
T.translate("validation.maximum", { maximum: field.maximum_quantity })
);
schema = schema.max(field.maximum_quantity, " ");
}
if (field.is_required) {
schema = schema.required(T.translate("validation.required"));
schema = schema.required(" ");
}
break;
}
case "Text":
case "TextArea": {
schema = yup.string(T.translate("validation.string"));
schema = yup.string();

if (field.is_required) {
schema = schema.required(T.translate("validation.required"));
schema = schema.required(" ");
}
break;
}
case "Time":
case "DateTime": {
schema = yup.date(T.translate("validation.date"));
schema = yup.date();

if (field.is_required) {
schema = schema.required(T.translate("validation.required"));
schema = schema.required(" ");
} else {
schema = schema.nullable();
}
break;
}
case "RadioButtonList":
case "ComboBox": {
schema = yup.string(T.translate("validation.string"));
schema = yup.string();

if (field.is_required) {
schema = schema.required(T.translate("validation.required"));
schema = schema.required(" ");
}
break;
}
case "CheckBox": {
schema = yup.boolean();

if (field.is_required) {
schema = schema.oneOf([true], T.translate("validation.required"));
schema = schema.oneOf([true], " ");
}
break;
}
case "CheckBoxList": {
schema = yup
.array()
.of(yup.string())
.typeError(T.translate("validation.wrong_format"));
schema = yup.array().of(yup.string()).typeError(" ");

if (field.is_required) {
schema = schema.min(1, T.translate("validation.required"));
schema = schema.min(1, " ");
}
break;
}
default: {
schema = yup.string(T.translate("validation.wrong_format"));
schema = yup.string();

if (field.is_required) {
schema = schema.required(T.translate("validation.required"));
schema = schema.required(" ");
}
break;
}
Expand Down Expand Up @@ -177,30 +167,20 @@ const buildValidationSchema = (items) => {
const quantityKey = `i-${item.form_item_id}-c-global-f-quantity`;

// notes
acc[`i-${item.form_item_id}-c-global-f-notes`] = yup.string(
T.translate("validation.string")
);
acc[`i-${item.form_item_id}-c-global-f-notes`] = yup.string();
// validation for the global quantity input
let globalQtySchema = yup
.number(T.translate("validation.number"))
.min(0, `${T.translate("validation.minimum")} 0`);
let globalQtySchema = yup.number().min(0, " ");

if (item.quantity_limit_per_sponsor > 0) {
globalQtySchema = globalQtySchema.max(
item.quantity_limit_per_sponsor,
T.translate("validation.maximum", {
maximum: item.quantity_limit_per_sponsor
})
" "
);
}
globalQtySchema = globalQtySchema.required(
T.translate("validation.required")
);
globalQtySchema = globalQtySchema.required(" ");
acc[quantityKey] = globalQtySchema;
// custom rate
acc[`i-${item.form_item_id}-c-global-f-custom_rate`] = yup.number(
T.translate("validation.number")
);
acc[`i-${item.form_item_id}-c-global-f-custom_rate`] = yup.number();

item.meta_fields.map((f) => {
acc[`i-${item.form_item_id}-c-${f.class_field}-f-${f.type_id}`] = yup
Expand All @@ -215,10 +195,8 @@ const buildValidationSchema = (items) => {
return acc;
}, {});

schema.discount_amount = yup.number(T.translate("validation.number"));
schema.discount_type = yup
.string(T.translate("validation.string"))
.nullable();
schema.discount_amount = yup.number();
schema.discount_type = yup.string().nullable();

return schema;
};
Expand All @@ -230,9 +208,6 @@ const EditForm = ({
onSaveForm,
onCancel
}) => {
const [notesItem, setNotesItem] = useState(null);
const [settingsItem, setSettingsItem] = useState(null);

const currentApplicableRate = useMemo(
() => getCurrentApplicableRate(showTimeZone, showMetadata),
[showMetadata, showTimeZone]
Expand Down Expand Up @@ -318,10 +293,6 @@ const EditForm = ({
// wait for formik to re-initialize with form items
if (!form || Object.keys(formik.values).length === 0) return null;

const hasItemFieldErrors = Object.keys(formik.errors).some(
(key) => key.includes("-c-Item-") && formik.touched[key]
);

return (
<>
<Typography variant="h5" sx={{ mt: 4, mb: 2 }}>
Expand All @@ -332,14 +303,19 @@ const EditForm = ({
{form?.items.length} {T.translate("general.items")}
</Box>
<FormikProvider value={formik}>
<Box component="form" onSubmit={formik.handleSubmit} autoComplete="off">
<Box
component="form"
onSubmit={formik.handleSubmit}
autoComplete="off"
noValidate
>
<MuiFormItemTable
data={form.items}
currentApplicableRate={currentApplicableRate}
values={formik.values}
touched={formik.touched}
errors={formik.errors}
timeZone={showTimeZone}
onNotesClick={setNotesItem}
onSettingsClick={setSettingsItem}
/>
<Box
component="div"
Expand All @@ -364,29 +340,7 @@ const EditForm = ({
{T.translate("general.save")}
</Button>
</Box>
{hasItemFieldErrors && (
<Box component="div" sx={{ mt: 1 }}>
<Typography
variant="caption"
color="error"
display="block"
sx={{ mt: 0.25, textAlign: "right" }}
>
{T.translate("validation.additional_items")}
</Typography>
</Box>
)}
</Box>
<NotesModal
item={notesItem}
open={!!notesItem}
onClose={() => setNotesItem(null)}
/>
<ItemSettingsModal
item={settingsItem}
open={!!settingsItem}
onClose={() => setSettingsItem(null)}
/>
</FormikProvider>
</>
);
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9068,10 +9068,10 @@ open@^10.0.3:
is-inside-container "^1.0.0"
wsl-utils "^0.1.0"

openstack-uicore-foundation@5.0.36:
version "5.0.36"
resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.36.tgz#a6da3a0793227b74706cb651af22471f9374859d"
integrity sha512-8Ia0/dh1xZNK9SrykP9+trzyMalyZI2TsirYrbWp0TREdV8vtS+dsNoRxeRMOaYejQtni6JkgHoTBZIC5c7hMw==
openstack-uicore-foundation@5.0.38-beta.1:
version "5.0.38-beta.1"
resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.38-beta.1.tgz#0c7ff54b9439203c91165992f43bb63ad4f6f976"
integrity sha512-z+tFwLvvjXog36VdSp2g7Fi0w+CSNF8n5WadwNgp3cypfpf4npaBmTZsP6774gJjWzwS7pSRP2Wv9W1miiFBQg==
dependencies:
use-sync-external-store "^1.6.0"

Expand Down
Loading