diff --git a/package.json b/package.json index d83ebe1c6..87ece9ac0 100644 --- a/package.json +++ b/package.json @@ -211,6 +211,7 @@ "testEnvironment": "jsdom", "setupFilesAfterEnv": [ "/testSetupFile.js" - ] + ], + "maxWorkers": 4 } } diff --git a/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js b/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js deleted file mode 100644 index 6816e36c1..000000000 --- a/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js +++ /dev/null @@ -1,1083 +0,0 @@ -/* eslint-env jest */ - -// ---- Mocks must come first ---- - -// i18n translate: echo the key -jest.mock("i18n-react/dist/i18n-react", () => ({ - __esModule: true, - default: { translate: (key) => key } -})); - -// ---- Now imports ---- -/* eslint-disable import/first */ -import React from "react"; -import PropTypes from "prop-types"; -import { cleanup, fireEvent, screen, render } from "@testing-library/react"; -import "@testing-library/jest-dom"; -import { FormikProvider, useFormik } from "formik"; -import FormItemTable from "../index"; -import ItemTableField from "../components/ItemTableField"; -/* eslint-enable import/first */ - -const TWO_ITEMS = 4; - -// Mock form data -const MOCK_FORM_A = { - items: [ - { - form_item_id: 1, - code: "INST", - name: "Installation", - rates: { - early_bird: 15000, - standard: 18800, - onsite: 22400 - }, - meta_fields: [ - { - type_id: 1, - class_field: "Form", - name: "Qty of People", - type: "Quantity", - minimum_quantity: 1, - maximum_quantity: 4 - }, - { - type_id: 2, - class_field: "Form", - name: "Hour x Person", - type: "Quantity", - minimum_quantity: 1, - maximum_quantity: 8 - }, - { - type_id: 3, - class_field: "Form", - name: "Arrival Time", - type: "Time" - }, - { - type_id: 4, - class_field: "Item", - name: "Special Instructions", - type: "Text", - is_required: true - } - ] - }, - { - form_item_id: 2, - code: "DISMANTLE", - name: "Dismantle", - rates: { - early_bird: 15000, - standard: 18800, - onsite: 22400 - }, - meta_fields: [ - { - type_id: 1, - class_field: "Form", - name: "Qty of People", - type: "Quantity", - minimum_quantity: 1, - maximum_quantity: 4 - }, - { - type_id: 2, - class_field: "Form", - name: "Hour x Person", - type: "Quantity", - minimum_quantity: 1, - maximum_quantity: 8 - }, - { - type_id: 3, - class_field: "Form", - name: "Arrival Time", - type: "Time" - } - ] - }, - { - form_item_id: 3, - code: "INST-MAN", - name: "Installation Manpower", - rates: { - early_bird: 15000, - standard: 18800, - onsite: 22400 - }, - meta_fields: [] - }, - { - form_item_id: 4, - code: "DIS-MAN", - name: "Dismantle Manpower", - rates: { - early_bird: 15000, - standard: 18800, - onsite: 22400 - }, - meta_fields: [] - } - ] -}; - -const MOCK_ITEMS_WITH_NULL_RATES = [ - { - form_item_id: 5, - code: "ITEM-NA", - name: "Item With N/A Rates", - rates: { - early_bird: null, - standard: null, - onsite: null - }, - meta_fields: [] - } -]; - -const MOCK_ITEMS_WITH_MIXED_RATES = [ - { - form_item_id: 6, - code: "ITEM-PARTIAL", - name: "Item Partial Rates", - rates: { - early_bird: 10000, - standard: null, - onsite: null - }, - meta_fields: [] - } -]; - -jest.mock("../../formik-inputs/mui-formik-textfield", () => { - const { useField } = require("formik"); - return { - __esModule: true, - default: ({ name, label, type, slotProps, multiline, rows, ...props }) => { - const [field] = useField(name); - return ( - - ); - } - }; -}); - -jest.mock("../../formik-inputs/mui-formik-timepicker", () => ({ - __esModule: true, - default: ({ name, label, timeZone }) => ( - - ) -})); - -jest.mock("../../formik-inputs/mui-formik-datepicker", () => ({ - __esModule: true, - default: ({ name, label }) => ( - - ) -})); - -jest.mock("../../formik-inputs/mui-formik-select", () => ({ - __esModule: true, - default: ({ name, label, options, children }) => ( - - ) -})); - -jest.mock("../../formik-inputs/mui-formik-checkbox", () => ({ - __esModule: true, - default: ({ name, label }) => ( - - ) -})); - -jest.mock("../../formik-inputs/mui-formik-dropdown-checkbox", () => ({ - __esModule: true, - default: ({ name, label, options }) => ( -
- {label} - {options && - options.map((opt) => ( - // eslint-disable-next-line jsx-a11y/label-has-associated-control - - ))} -
- ) -})); - -jest.mock("../../formik-inputs/mui-formik-dropdown-radio", () => ({ - __esModule: true, - default: ({ name, label, options }) => ( -
- {label} - {options && - options.map((opt) => ( - // eslint-disable-next-line jsx-a11y/label-has-associated-control - - ))} -
- ) -})); - -jest.mock("../../formik-inputs/mui-formik-pricefield", () => { - const { useField } = require("formik"); - return { - __esModule: true, - default: ({ name, label, ...props }) => { - const [field] = useField(name); - return ( - - ); - } - }; -}); - -jest.mock("../../formik-inputs/mui-formik-discountfield", () => { - const { useField } = require("formik"); - return { - __esModule: true, - default: ({ name, label, discountType, ...props }) => { - const [field] = useField(name); - return ( - - ); - } - }; -}); - -// ---- Helpers ---- - -// Wrapper component with Formik -const FormItemTableWrapper = ({ - data, - currentApplicableRate, - timeZone, - initialValues, - onNotesClick, - onSettingsClick -}) => { - const defaultValues = { - discount_type: "AMOUNT", - discount_amount: 0, - ...initialValues - }; - - const formik = useFormik({ - initialValues: defaultValues, - onSubmit: () => {} - }); - - return ( - - - - ); -}; - -FormItemTableWrapper.propTypes = { - data: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - currentApplicableRate: PropTypes.string, - timeZone: PropTypes.string.isRequired, - initialValues: PropTypes.shape({}), - onNotesClick: PropTypes.func.isRequired, - onSettingsClick: PropTypes.func.isRequired -}; - -FormItemTableWrapper.defaultProps = { - currentApplicableRate: "early_bird", - initialValues: {} -}; - -// ---- Tests ---- -describe("FormItemTable Component", () => { - const mockOnNotesClick = jest.fn(); - const mockOnSettingsClick = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - cleanup(); - }); - - describe("Rendering", () => { - it("renders the table with correct structure", () => { - render( - - ); - - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.code") - ).toBeInTheDocument(); - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.description") - ).toBeInTheDocument(); - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.early_bird_rate") - ).toBeInTheDocument(); - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.standard_rate") - ).toBeInTheDocument(); - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.onsite_rate") - ).toBeInTheDocument(); - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.qty") - ).toBeInTheDocument(); - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.total") - ).toBeInTheDocument(); - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.notes") - ).toBeInTheDocument(); - }); - - it("renders all items from mock data", () => { - render( - - ); - - expect(screen.getByText("Installation")).toBeInTheDocument(); - expect(screen.getByText("Dismantle")).toBeInTheDocument(); - expect(screen.getByText("Installation Manpower")).toBeInTheDocument(); - expect(screen.getByText("Dismantle Manpower")).toBeInTheDocument(); - }); - - it("renders dynamic columns from meta_fields", () => { - render( - - ); - - expect(screen.getByText("Qty of People")).toBeInTheDocument(); - expect(screen.getByText("Hour x Person")).toBeInTheDocument(); - expect(screen.getByText("Arrival Time")).toBeInTheDocument(); - }); - - it("displays rate values in cents to dollar format", () => { - render( - - ); - - // early_bird: 15000 cents = $150.00 - expect(screen.getAllByText("$150.00").length).toBeGreaterThan(0); - // standard: 18800 cents = $188.00 - expect(screen.getAllByText("$188.00").length).toBeGreaterThan(0); - // onsite: 22400 cents = $224.00 - expect(screen.getAllByText("$224.00").length).toBeGreaterThan(0); - }); - - it("renders TOTAL row at the bottom", () => { - render( - - ); - - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.total_on_caps") - ).toBeInTheDocument(); - }); - }); - - describe("N/A Rates", () => { - it("displays N/A for items with null rates", () => { - render( - - ); - - expect(screen.getAllByText("general.n_a").length).toBeGreaterThan(0); - }); - - it("displays N/A for null rates and dollar amounts for non-null rates on the same item", () => { - render( - - ); - - expect(screen.getByText("$100.00")).toBeInTheDocument(); - expect(screen.getAllByText("general.n_a").length).toBe(2); - }); - - it("shows $0.00 total for items with null applicable rate", () => { - render( - - ); - - // Row total and grand total should both be $0.00 when rate is null - expect(screen.getAllByText("$0.00").length).toBeGreaterThan(0); - }); - - it("renders item with N/A rates without crashing", () => { - expect(() => { - render( - - ); - }).not.toThrow(); - }); - }); - - describe("ITEM Class Fields", () => { - it("shows warning icon for items with ITEM class fields", () => { - render( - - ); - - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.additional_info") - ).toBeInTheDocument(); - }); - - it("renders settings button only for items with ITEM class fields", () => { - const { container } = render( - - ); - - const settingsButtons = container.querySelectorAll( - "[data-testid=\"SettingsIcon\"]" - ); - expect(settingsButtons.length).toBe(1); - }); - - it("calls onSettingsClick when settings button is clicked", () => { - const { container } = render( - - ); - - const settingsButton = container.querySelector( - "[data-testid=\"SettingsIcon\"]" - ).parentElement; - fireEvent.click(settingsButton); - - expect(mockOnSettingsClick).toHaveBeenCalledWith(MOCK_FORM_A.items[0]); - expect(mockOnSettingsClick).toHaveBeenCalledTimes(1); - }); - }); - - describe("Form Inputs", () => { - it("renders Quantity input fields with correct attributes", () => { - render( - - ); - - const qtyPeopleInput = screen.getByTestId("textfield-i-1-c-Form-f-1"); - expect(qtyPeopleInput).toHaveAttribute("type", "number"); - expect(qtyPeopleInput).toHaveAttribute("min", "1"); - expect(qtyPeopleInput).toHaveAttribute("max", "4"); - }); - - it("renders Time input fields with timezone", () => { - render( - - ); - - const timeInput = screen.getByTestId("timepicker-i-1-c-Form-f-3"); - expect(timeInput).toBeInTheDocument(); - expect(timeInput).toHaveAttribute("data-timezone", "America/New_York"); - }); - - it("renders correct number of form inputs per item", () => { - render( - - ); - - expect( - screen.getByTestId("textfield-i-1-c-Form-f-1") - ).toBeInTheDocument(); - expect( - screen.getByTestId("textfield-i-1-c-Form-f-2") - ).toBeInTheDocument(); - expect( - screen.getByTestId("timepicker-i-1-c-Form-f-3") - ).toBeInTheDocument(); - expect( - screen.getByTestId("textfield-i-2-c-Form-f-1") - ).toBeInTheDocument(); - expect( - screen.getByTestId("textfield-i-2-c-Form-f-2") - ).toBeInTheDocument(); - expect( - screen.getByTestId("timepicker-i-2-c-Form-f-3") - ).toBeInTheDocument(); - }); - }); - - describe("Quantity Calculation", () => { - it("calculates quantity based on FORM Quantity fields", () => { - const initialValues = { - "i-1-c-Form-f-1": 2, - "i-1-c-Form-f-2": 4 - }; - - render( - - ); - - const qtyInput = screen.getByTestId("textfield-i-1-c-global-f-quantity"); - // eslint-disable-next-line - expect(qtyInput).toHaveValue(8); - }); - - it("renders manual quantity input when no FORM Quantity fields exist", () => { - const itemsWithoutQuantityFields = [ - { - form_item_id: 10, - code: "TEST", - name: "Test Item", - quantity: 0, - rates: { - early_bird: 10000, - standard: 12000, - onsite: 15000 - }, - meta_fields: [ - { - type_id: 100, - class: "Form", - name: "Description", - type: "Text" - } - ] - } - ]; - - render( - - ); - - expect( - screen.getByTestId("textfield-i-10-c-global-f-quantity") - ).toBeInTheDocument(); - }); - }); - - describe("Total Calculation", () => { - it("calculates item total correctly based on quantity and rate", () => { - // qty item1 = 2*4 = 8, standard rate = 18800 → 8 * 18800 = 150400 → $1504.00 - // qty item2 = 1*2 = 2, standard rate = 18800 → 2 * 18800 = 37600 → $376.00 - const initialValues = { - "i-1-c-Form-f-1": 2, - "i-1-c-Form-f-2": 4, - "i-2-c-Form-f-1": 1, - "i-2-c-Form-f-2": 2 - }; - - render( - - ); - - const allText = screen.getAllByText(/\$/); - const dollarValues = allText.map((el) => el.textContent); - - expect(dollarValues).toContain("$1504.00"); - expect(dollarValues).toContain("$376.00"); - }); - - it("calculates total amount for all items", () => { - // $1504.00 + $376.00 = $1880.00 - const initialValues = { - "i-1-c-Form-f-1": 2, - "i-1-c-Form-f-2": 4, - "i-2-c-Form-f-1": 1, - "i-2-c-Form-f-2": 2 - }; - - render( - - ); - - const allText = screen.getAllByText(/\$/); - const dollarValues = allText.map((el) => el.textContent); - expect(dollarValues).toContain("$1880.00"); - }); - - it("shows $0.00 when no quantities are set", () => { - render( - - ); - - expect( - screen.getByText("edit_sponsor.cart_tab.edit_form.total_on_caps") - ).toBeInTheDocument(); - }); - }); - - describe("Rate Highlighting", () => { - it("highlights early_bird rate when currentApplicableRate is early_bird", () => { - render( - - ); - - expect(screen.getAllByText("$150.00").length).toBeGreaterThan(0); - }); - - it("highlights standard rate when currentApplicableRate is standard", () => { - render( - - ); - - expect(screen.getAllByText("$188.00").length).toBeGreaterThan(0); - }); - - it("highlights onsite rate when currentApplicableRate is onsite", () => { - render( - - ); - - expect(screen.getAllByText("$224.00").length).toBeGreaterThan(0); - }); - }); - - describe("Notes Functionality", () => { - it("renders edit/notes button for all items", () => { - const { container } = render( - - ); - - const editButtons = container.querySelectorAll( - "[data-testid=\"EditIcon\"]" - ); - expect(editButtons.length).toBe(TWO_ITEMS); - }); - - it("calls onNotesClick with correct item when notes button is clicked", () => { - const { container } = render( - - ); - - const editButtons = container.querySelectorAll( - "[data-testid=\"EditIcon\"]" - ); - fireEvent.click(editButtons[0].parentElement); - - expect(mockOnNotesClick).toHaveBeenCalledWith(MOCK_FORM_A.items[0]); - expect(mockOnNotesClick).toHaveBeenCalledTimes(1); - }); - - it("calls onNotesClick for second item independently", () => { - const { container } = render( - - ); - - const editButtons = container.querySelectorAll( - "[data-testid=\"EditIcon\"]" - ); - fireEvent.click(editButtons[1].parentElement); - - expect(mockOnNotesClick).toHaveBeenCalledWith(MOCK_FORM_A.items[1]); - expect(mockOnNotesClick).toHaveBeenCalledTimes(1); - }); - }); - - describe("renderInput Helper Function", () => { - const RenderInputWrapper = ({ field, timeZone, label }) => { - const formik = useFormik({ initialValues: {}, onSubmit: () => {} }); - return ( - - - - ); - }; - - RenderInputWrapper.propTypes = { - field: PropTypes.shape({}).isRequired, - timeZone: PropTypes.string.isRequired, - label: PropTypes.string.isRequired - }; - - it("renders CheckBox input correctly", () => { - const field = { - type_id: 1, - class_field: "Form", - type: "CheckBox", - name: "Test Checkbox" - }; - const { container } = render( - - ); - expect( - container.querySelector("[data-testid=\"checkbox-i-1-c-Form-f-1\"]") - ).toBeInTheDocument(); - }); - - it("renders CheckBoxList input with options", () => { - const field = { - type_id: 2, - class_field: "Form", - type: "CheckBoxList", - name: "Test CheckBoxList", - values: [ - { id: 1, value: "Option 1" }, - { id: 2, value: "Option 2" } - ] - }; - const { container } = render( - - ); - expect( - container.querySelector( - "[data-testid=\"dropdown-checkbox-i-1-c-Form-f-2\"]" - ) - ).toBeInTheDocument(); - }); - - it("renders RadioButtonList input with options", () => { - const field = { - type_id: 3, - class_field: "Form", - type: "RadioButtonList", - name: "Test Radio", - values: [ - { id: 1, value: "Radio 1" }, - { id: 2, value: "Radio 2" } - ] - }; - const { container } = render( - - ); - expect( - container.querySelector("[data-testid=\"dropdown-radio-i-1-c-Form-f-3\"]") - ).toBeInTheDocument(); - }); - - it("renders DateTime input correctly", () => { - const field = { - type_id: 4, - class_field: "Item", - type: "DateTime", - name: "Test DateTime" - }; - const { container } = render( - - ); - expect( - container.querySelector("[data-testid=\"datepicker-i-1-c-Item-f-4\"]") - ).toBeInTheDocument(); - }); - - it("renders Time input with timezone", () => { - const field = { - type_id: 5, - class_field: "Form", - type: "Time", - name: "Test Time" - }; - const { container } = render( - - ); - const input = container.querySelector( - "[data-testid=\"timepicker-i-1-c-Form-f-5\"]" - ); - expect(input).toBeInTheDocument(); - expect(input).toHaveAttribute("data-timezone", "America/Chicago"); - }); - - it("renders ComboBox with options", () => { - const field = { - type_id: 6, - class_field: "Form", - type: "ComboBox", - name: "Test ComboBox", - values: [ - { id: 1, value: "Combo 1" }, - { id: 2, value: "Combo 2" } - ] - }; - const { container } = render( - - ); - expect( - container.querySelector("[data-testid=\"select-i-1-c-Form-f-6\"]") - ).toBeInTheDocument(); - }); - - it("renders Text input correctly", () => { - const field = { - type_id: 7, - class_field: "Form", - type: "Text", - name: "Test Text" - }; - const { container } = render( - - ); - expect( - container.querySelector("[data-testid=\"textfield-i-1-c-Form-f-7\"]") - ).toBeInTheDocument(); - }); - - it("renders TextArea input correctly", () => { - const field = { - type_id: 8, - class_field: "Form", - type: "TextArea", - name: "Test TextArea" - }; - const { container } = render( - - ); - expect( - container.querySelector("[data-testid=\"textfield-i-1-c-Form-f-8\"]") - ).toBeInTheDocument(); - }); - - it("renders Quantity input with min/max attributes", () => { - const field = { - type_id: 9, - class_field: "Form", - type: "Quantity", - name: "Test Quantity", - minimum_quantity: 5, - maximum_quantity: 100 - }; - const { container } = render( - - ); - const input = container.querySelector( - "[data-testid=\"textfield-i-1-c-Form-f-9\"]" - ); - expect(input).toHaveAttribute("min", "5"); - expect(input).toHaveAttribute("max", "100"); - }); - }); -}); diff --git a/src/components/mui/FormItemTable/components/GlobalQuantityField.js b/src/components/mui/FormItemTable/components/GlobalQuantityField.js deleted file mode 100644 index 9f8751f9d..000000000 --- a/src/components/mui/FormItemTable/components/GlobalQuantityField.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright 2026 OpenStack Foundation - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * */ - -import React, { useEffect } from "react"; -import { useField } from "formik"; -import MuiFormikTextField from "../../formik-inputs/mui-formik-textfield"; - -const GlobalQuantityField = ({ - row, - extraColumns, - value, - disabled = false -}) => { - const name = `i-${row.form_item_id}-c-global-f-quantity`; - // eslint-disable-next-line - const [field, meta, helpers] = useField(name); - - // using readOnly since formik won't validate disabled fields - const isReadOnly = - extraColumns.filter((eq) => eq.type === "Quantity").length > 0; - - useEffect(() => { - helpers.setValue(value); - helpers.setTouched(true); - }, [value]); - - return ( - - ); -}; - -export default GlobalQuantityField; diff --git a/src/components/mui/FormItemTable/components/ItemTableField.js b/src/components/mui/FormItemTable/components/ItemTableField.js deleted file mode 100644 index 2f5cdea12..000000000 --- a/src/components/mui/FormItemTable/components/ItemTableField.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright 2026 OpenStack Foundation - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * */ - -import React from "react"; -import { MenuItem } from "@mui/material"; -import MuiFormikCheckbox from "../../formik-inputs/mui-formik-checkbox"; -import MuiFormikDropdownCheckbox from "../../formik-inputs/mui-formik-dropdown-checkbox"; -import MuiFormikDropdownRadio from "../../formik-inputs/mui-formik-dropdown-radio"; -import MuiFormikDatepicker from "../../formik-inputs/mui-formik-datepicker"; -import MuiFormikTimepicker from "../../formik-inputs/mui-formik-timepicker"; -import MuiFormikTextField from "../../formik-inputs/mui-formik-textfield"; -import MuiFormikSelect from "../../formik-inputs/mui-formik-select"; - -const ItemTableField = ({ - rowId, - field, - timeZone, - label = "", - disabled = false -}) => { - const name = `i-${rowId}-c-${field.class_field}-f-${field.type_id}`; - const commonProps = { name, label, disabled }; - - switch (field.type) { - case "CheckBox": - return ; - case "CheckBoxList": - return ( - ({ value: v.id, label: v.value }))} - /> - ); - case "RadioButtonList": - return ( - ({ value: v.id, label: v.value }))} - /> - ); - case "DateTime": - return ; - case "Time": - return ; - case "Quantity": - return ( - 0 - ? { max: field.maximum_quantity } - : {}) - } - }} - /> - ); - case "ComboBox": - return ( - - {field.values.map((v) => ( - - {v.value} - - ))} - - ); - case "Text": - return ; - case "TextArea": - return ( - - ); - } -}; - -export default ItemTableField; diff --git a/src/components/mui/FormItemTable/components/UnderlyingAlertNote.js b/src/components/mui/FormItemTable/components/UnderlyingAlertNote.js deleted file mode 100644 index 763e6c5b0..000000000 --- a/src/components/mui/FormItemTable/components/UnderlyingAlertNote.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import { Typography } from "@mui/material"; -import ErrorIcon from "@mui/icons-material/Error"; -import T from "i18n-react/dist/i18n-react"; - -const UnderlyingAlertNote = ({ showAdditionalItems }) => { - if (!showAdditionalItems) return null; - - return ( - - {" "} - {T.translate("edit_sponsor.cart_tab.edit_form.additional_info")} - - ); -}; - -export default UnderlyingAlertNote; diff --git a/src/components/mui/FormItemTable/helpers.js b/src/components/mui/FormItemTable/helpers.js deleted file mode 100644 index 1e3b83cc2..000000000 --- a/src/components/mui/FormItemTable/helpers.js +++ /dev/null @@ -1,30 +0,0 @@ -import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods"; -import { MILLISECONDS_IN_SECOND } from "../../../utils/constants"; - -export const getCurrentApplicableRate = (timeZone, rateDates) => { - const now = epochToMomentTimeZone( - Math.floor(new Date() / MILLISECONDS_IN_SECOND), - timeZone - ); - - const earlyBirdEnd = epochToMomentTimeZone( - rateDates.early_bird_end_date, - timeZone - )?.endOf("day"); - const onsiteStart = epochToMomentTimeZone( - rateDates.onsite_price_start_date, - timeZone - )?.startOf("day"); - const onsiteEnd = epochToMomentTimeZone( - rateDates.onsite_price_end_date, - timeZone - )?.endOf("day"); - - if (earlyBirdEnd && now.isSameOrBefore(earlyBirdEnd)) return "early_bird"; - if (onsiteStart && now.isSameOrBefore(onsiteStart)) return "standard"; - if (!onsiteEnd || now.isSameOrBefore(onsiteEnd)) return "onsite"; - return "expired"; -}; - -export const isItemAvailable = (item, currentApplicableRate) => - item.rates?.[currentApplicableRate] != null; diff --git a/src/components/mui/FormItemTable/index.js b/src/components/mui/FormItemTable/index.js deleted file mode 100644 index 308323b96..000000000 --- a/src/components/mui/FormItemTable/index.js +++ /dev/null @@ -1,302 +0,0 @@ -/** - * Copyright 2026 OpenStack Foundation - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * */ - -import React, { useCallback, useMemo } from "react"; -import { - IconButton, - MenuItem, - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow -} from "@mui/material"; -import EditIcon from "@mui/icons-material/Edit"; -import SettingsIcon from "@mui/icons-material/Settings"; -import T from "i18n-react/dist/i18n-react"; -import { currencyAmountFromCents } from "openstack-uicore-foundation/lib/utils/money"; -import { DISCOUNT_TYPES, ONE_HUNDRED } from "../../../utils/constants"; -import GlobalQuantityField from "./components/GlobalQuantityField"; -import ItemTableField from "./components/ItemTableField"; -import MuiFormikSelect from "../formik-inputs/mui-formik-select"; -import MuiFormikPriceField from "../formik-inputs/mui-formik-pricefield"; -import MuiFormikDiscountField from "../formik-inputs/mui-formik-discountfield"; -import UnderlyingAlertNote from "./components/UnderlyingAlertNote"; - -const FormItemTable = ({ - data, - currentApplicableRate, - timeZone, - values, - onNotesClick, - onSettingsClick -}) => { - const valuesStr = JSON.stringify(values); - const extraColumns = - data[0]?.meta_fields?.filter((mf) => mf.class_field === "Form") || []; - const fixedColumns = 10; - const totalColumns = extraColumns.length + fixedColumns; - - const calculateQuantity = useCallback( - (row) => { - const qtyEXC = extraColumns.filter((exc) => exc.type === "Quantity"); - const globalQty = values[`i-${row.form_item_id}-c-global-f-quantity`]; - const itemLevelQty = qtyEXC.reduce((res, exc) => { - const start = res > 0 ? res : 1; - return ( - start * - (values?.[ - `i-${row.form_item_id}-c-${exc.class_field}-f-${exc.type_id}` - ] || 0) - ); - }, 0); - - return qtyEXC.length > 0 ? itemLevelQty : globalQty; - }, - [valuesStr, extraColumns] - ); - - const calculateRowTotal = (row) => { - const qty = - values[`i-${row.form_item_id}-c-global-f-quantity`] || - calculateQuantity(row); - - if (currentApplicableRate === "expired") return 0; - - const customRate = values[`i-${row.form_item_id}-c-global-f-custom_rate`]; - const rate = customRate || row.rates[currentApplicableRate]; - - if (rate == null || qty == null) return 0; - - return qty * rate; - }; - - const hasItemFields = (row) => - row.meta_fields.filter((mf) => mf.class_field === "Item").length > 0; - - const itemFieldsIncomplete = (row) => { - const requiredFields = row.meta_fields.filter( - (mf) => mf.class_field === "Item" && mf.is_required - ); - const hasMissingFields = requiredFields.some((mf) => { - const value = values[`i-${row.form_item_id}-c-item-f-${mf.type_id}`]; - return value === undefined || value === null || value === ""; - }); - - return requiredFields.length > 0 && hasMissingFields; - }; - - const formatRate = (rate) => { - if (rate == null) return T.translate("general.n_a"); - return currencyAmountFromCents(rate); - }; - - const totalAmount = useMemo(() => { - const subtotal = data.reduce((acc, row) => acc + calculateRowTotal(row), 0); - const discount = - values.discount_type === DISCOUNT_TYPES.AMOUNT - ? values.discount_amount - : subtotal * (values.discount_amount / ONE_HUNDRED / ONE_HUNDRED); // bps to fraction - - return subtotal - Math.round(discount); - }, [data, valuesStr, currentApplicableRate]); - - const handleEdit = (row) => { - onNotesClick(row); - }; - - const handleEditItemFields = (row) => { - onSettingsClick(row); - }; - - return ( - - - - - - {T.translate("edit_sponsor.cart_tab.edit_form.code")} - - - {T.translate("edit_sponsor.cart_tab.edit_form.description")} - - - {T.translate("edit_sponsor.cart_tab.edit_form.custom_rate")} - - - {T.translate("edit_sponsor.cart_tab.edit_form.early_bird_rate")} - - - {T.translate("edit_sponsor.cart_tab.edit_form.standard_rate")} - - - {T.translate("edit_sponsor.cart_tab.edit_form.onsite_rate")} - - {extraColumns.map((exc) => ( - {exc.name} - ))} - - {T.translate("edit_sponsor.cart_tab.edit_form.qty")} - - - {/* item level extra field */} - - {T.translate("edit_sponsor.cart_tab.edit_form.total")} - - - {T.translate("edit_sponsor.cart_tab.edit_form.notes")} - - - - - {data.map((row) => ( - - {row.code} - -
{row.name}
- -
- - - - - {formatRate(row.rates.early_bird)} - - - {formatRate(row.rates.standard)} - - - {formatRate(row.rates.onsite)} - - {extraColumns.map((exc) => ( - - - - ))} - - - - - {hasItemFields(row) && ( - handleEditItemFields(row)} - > - - - )} - - - {currencyAmountFromCents(calculateRowTotal(row))} - - - handleEdit(row)} - > - - - -
- ))} - - - {T.translate("edit_sponsor.cart_tab.edit_form.discount")} - - {/* eslint-disable-next-line */} - {new Array(totalColumns - 5).fill(0).map((_, i) => ( - - ))} - - - {Object.values(DISCOUNT_TYPES).map((p) => ( - - {p} - - ))} - - - - - - - - - - - {T.translate("edit_sponsor.cart_tab.edit_form.total_on_caps")} - - {/* eslint-disable-next-line */} - {new Array(totalColumns - 3).fill(0).map((_, i) => ( - - ))} - - {currencyAmountFromCents(totalAmount)} - - - -
-
-
- ); -}; - -export default FormItemTable; diff --git a/src/components/mui/ItemSettingsModal/index.js b/src/components/mui/ItemSettingsModal/index.js deleted file mode 100644 index 924504457..000000000 --- a/src/components/mui/ItemSettingsModal/index.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright 2026 OpenStack Foundation - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * */ - -import React from "react"; -import PropTypes from "prop-types"; -import T from "i18n-react/dist/i18n-react"; -import Button from "@mui/material/Button"; -import Dialog from "@mui/material/Dialog"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; -import DialogTitle from "@mui/material/DialogTitle"; -import { Divider, IconButton, Typography } from "@mui/material"; -import CloseIcon from "@mui/icons-material/Close"; -import ItemTableField from "../FormItemTable/components/ItemTableField"; - -const ItemSettingsModal = ({ item, timeZone, open, onClose }) => { - const itemFields = - item?.meta_fields.filter((f) => f.class_field === "Item") || []; - - const handleSave = () => { - onClose(); - }; - - return ( - - {T.translate("edit_form.settings")} - ({ - position: "absolute", - right: 8, - top: 8, - color: theme.palette.grey[500] - })} - > - - - - - - {item?.name} - - - {itemFields.map((exc) => ( - - - - ))} - - - - - - ); -}; - -ItemSettingsModal.propTypes = { - item: PropTypes.object.isRequired, - open: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired -}; - -export default ItemSettingsModal; diff --git a/src/components/mui/NotesModal/index.js b/src/components/mui/NotesModal/index.js deleted file mode 100644 index 96388430c..000000000 --- a/src/components/mui/NotesModal/index.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright 2026 OpenStack Foundation - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * */ - -import React, { useEffect, useState } from "react"; -import T from "i18n-react/dist/i18n-react"; -import PropTypes from "prop-types"; -import Button from "@mui/material/Button"; -import Dialog from "@mui/material/Dialog"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; -import DialogContentText from "@mui/material/DialogContentText"; -import DialogTitle from "@mui/material/DialogTitle"; -import { useField } from "formik"; -import { Divider, IconButton, TextField } from "@mui/material"; -import CloseIcon from "@mui/icons-material/Close"; - -const NotesModal = ({ item, open, onClose }) => { - const name = `i-${item?.form_item_id}-c-global-f-notes`; - // eslint-disable-next-line - const [field, meta, helpers] = useField(name); - const [notes, setNotes] = useState(""); - - useEffect(() => { - setNotes(field.value || ""); - }, [field?.value]); - - const handleSave = () => { - helpers.setValue(notes); - onClose(); - }; - - return ( - - - {T.translate("edit_sponsor.cart_tab.edit_form.notes")} - - ({ - position: "absolute", - right: 8, - top: 8, - color: theme.palette.grey[500] - })} - > - - - - - {item?.name} - setNotes(ev.target.value)} - value={notes} - margin="normal" - multiline - fullWidth - rows={4} - placeholder={T.translate( - "edit_sponsor.cart_tab.edit_form.notes_placeholder" - )} - /> - - - - - - ); -}; - -NotesModal.propTypes = { - item: PropTypes.object, - open: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired -}; - -export default NotesModal; diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/__tests__/edit-cart-form.test.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/__tests__/edit-cart-form.test.js index 46bc01308..967e2303f 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/__tests__/edit-cart-form.test.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/__tests__/edit-cart-form.test.js @@ -15,74 +15,63 @@ jest.mock("../../../../../../../../actions/sponsor-cart-actions", () => ({ updateCartForm: (...args) => mockUpdateCartForm(...args) })); -// Mock sub-components used by FormItemTable +// Mock foundation components used by EditForm jest.mock( - "../../../../../../../../components/mui/FormItemTable/components/GlobalQuantityField", + "openstack-uicore-foundation/lib/components/mui/form-item-table", () => { const React = require("react"); return { __esModule: true, - default: ({ name }) => ( - - ) - }; - } -); - -jest.mock( - "../../../../../../../../components/mui/FormItemTable/components/ItemTableField", - () => { - const React = require("react"); - return { - __esModule: true, - default: ({ name }) => ( - - ) - }; - } -); - -jest.mock( - "../../../../../../../../components/mui/formik-inputs/mui-formik-select", - () => { - const React = require("react"); - return { - __esModule: true, - default: ({ name, options, ...props }) => ( - - ) + + ), + getCurrentApplicableRate: () => "standard" }; } ); -jest.mock( - "../../../../../../../../components/mui/formik-inputs/mui-formik-pricefield", - () => { - const React = require("react"); - return { - __esModule: true, - default: ({ name }) => ( - - ) - }; - } -); +jest.mock("openstack-uicore-foundation/lib/components/mui/notes-modal", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ open, onClose }) => + open ? ( +
+ +
+ ) : null + }; +}); jest.mock( - "../../../../../../../../components/mui/formik-inputs/mui-formik-discountfield", + "openstack-uicore-foundation/lib/components/mui/item-settings-modal", () => { const React = require("react"); return { __esModule: true, - default: ({ name }) => ( - - ) + default: ({ open }) => (open ?
Settings
: null) }; } ); diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/index.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/index.js index 7b400226a..ed8dad1a8 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/index.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/index.js @@ -20,11 +20,12 @@ import { Button, Typography } from "@mui/material"; import Box from "@mui/material/Box"; import moment from "moment-timezone"; import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods"; -import FormItemTable from "../../../../../../../components/mui/FormItemTable"; +import FormItemTable, { + getCurrentApplicableRate +} from "openstack-uicore-foundation/lib/components/mui/form-item-table"; +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 NotesModal from "../../../../../../../components/mui/NotesModal"; -import ItemSettingsModal from "../../../../../../../components/mui/ItemSettingsModal"; -import { getCurrentApplicableRate } from "../../../../../../../components/mui/FormItemTable/helpers"; const parseValue = (item, timeZone) => { switch (item.type) { @@ -32,16 +33,20 @@ const parseValue = (item, timeZone) => { return item.current_value ? parseInt(item.current_value) : item.minimum_quantity || 0; - case "ComboBox": + case "RadioButtonList": + case "ComboBox": { + const defaultVal = item.values.find((v) => v.is_default)?.id; + return item.current_value || defaultVal || ""; + } + case "CheckBoxList": { + const defaultVal = item.values.find((v) => v.is_default)?.id; + return item.current_value || (defaultVal ? [defaultVal] : []); + } case "Text": case "TextArea": return item.current_value || ""; case "CheckBox": return item.current_value ? item.current_value === "True" : false; - case "CheckBoxList": - return item.current_value || []; - case "RadioButtonList": - return item.current_value || ""; case "Time": return item.current_value ? moment.tz(item.current_value, "HH:mm", timeZone) @@ -150,12 +155,10 @@ const buildInitialValues = (form, timeZone) => { const buildValidationSchema = (items) => { const schema = items.reduce((acc, item) => { - item.meta_fields - .filter((f) => f.class_field === "Form") - .map((f) => { - acc[`i-${item.form_item_id}-c-${f.class_field}-f-${f.type_id}`] = - getYupValidation(f); - }); + item.meta_fields.map((f) => { + acc[`i-${item.form_item_id}-c-${f.class_field}-f-${f.type_id}`] = + getYupValidation(f); + }); // notes acc[`i-${item.form_item_id}-c-global-f-notes`] = yup.string( T.translate("validation.string")