Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.3",
"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,11 +36,12 @@ 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;
return item.current_value || (defaultVal ? [defaultVal] : []);
return item.current_value || (defaultVal ? [String(defaultVal)] : []);
}
case "Text":
case "TextArea":
Expand All @@ -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
Loading
Loading