From 57a9aecb3de87c17f0d9ffddb5706a3aeea89f36 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Mon, 30 Mar 2026 08:57:11 -0300 Subject: [PATCH 1/8] feat: add mui componentes from summit-admin --- package.json | 18 +- src/components/index.js | 49 + src/components/inputs/promocode-input.js | 4 +- .../__tests__/FormItemTable.test.js | 1096 +++++++++++++++++ .../components/GlobalQuantityField.js | 71 ++ .../components/ItemTableField.js | 99 ++ .../components/UnderlyingAlertNote.js | 41 + src/components/mui/FormItemTable/helpers.js | 43 + src/components/mui/FormItemTable/index.js | 302 +++++ src/components/mui/ItemSettingsModal/index.js | 91 ++ src/components/mui/NotesModal/index.js | 90 ++ .../mui/SnackbarNotification/Context.js | 20 + .../mui/SnackbarNotification/index.js | 106 ++ .../__tests__/additional-input-list.test.js | 362 ++++++ .../mui/__tests__/additional-input.test.js | 266 ++++ .../mui/__tests__/chip-list.test.js | 87 ++ .../mui/__tests__/item-price-tiers.test.js | 155 +++ .../mui/__tests__/meta-field-values.test.js | 332 +++++ .../mui-formik-checkbox-group.test.js | 176 +++ .../__tests__/mui-formik-datepicker.test.js | 63 + .../mui-formik-file-size-field.test.js | 236 ++++ .../mui-formik-quantity-field.test.js | 81 ++ .../__tests__/mui-formik-radio-group.test.js | 161 +++ .../mui/__tests__/mui-sponsor-input.test.js | 298 +++++ .../mui/__tests__/mui-table-editable.test.js | 380 ++++++ src/components/mui/checkbox-list.js | 106 ++ src/components/mui/chip-list.js | 42 + src/components/mui/chip-notify.js | 28 + src/components/mui/chip-select-input.js | 148 +++ src/components/mui/confirm-dialog.js | 105 ++ src/components/mui/custom-alert.js | 31 + src/components/mui/dnd-list.js | 102 ++ src/components/mui/dropdown-checkbox.js | 89 ++ .../mui/editable-table/mui-table-editable.js | 391 ++++++ .../mui/editable-table/styles.module.less | 14 + .../additional-input/additional-input-list.js | 122 ++ .../additional-input/additional-input.js | 198 +++ .../additional-input/meta-field-values.js | 193 +++ .../mui/formik-inputs/company-input-mui.js | 206 ++++ .../mui/formik-inputs/item-price-tiers.js | 103 ++ .../formik-inputs/mui-formik-async-select.js | 140 +++ .../mui-formik-checkbox-group.js | 88 ++ .../mui/formik-inputs/mui-formik-checkbox.js | 57 + .../formik-inputs/mui-formik-datepicker.js | 79 ++ .../formik-inputs/mui-formik-discountfield.js | 110 ++ .../mui-formik-dropdown-checkbox.js | 88 ++ .../mui-formik-dropdown-radio.js | 65 + .../mui-formik-file-size-field.js | 96 ++ .../formik-inputs/mui-formik-pricefield.js | 96 ++ .../mui-formik-quantity-field.js | 43 + .../formik-inputs/mui-formik-radio-group.js | 82 ++ .../formik-inputs/mui-formik-select-group.js | 352 ++++++ .../mui/formik-inputs/mui-formik-select.js | 95 ++ .../mui-formik-summit-addon-select.js | 49 + .../mui/formik-inputs/mui-formik-switch.js | 57 + .../mui/formik-inputs/mui-formik-textfield.js | 69 ++ .../formik-inputs/mui-formik-timepicker.js | 69 ++ .../mui/formik-inputs/mui-formik-upload.js | 112 ++ .../mui/formik-inputs/mui-sponsor-input.js | 166 +++ .../formik-inputs/sponsorship-input-mui.js | 162 +++ .../sponsorship-summit-select-mui.js | 166 +++ src/components/mui/infinite-table/index.js | 161 +++ .../mui/infinite-table/styles.module.less | 14 + src/components/mui/menu-button.js | 122 ++ src/components/mui/search-input.js | 71 ++ src/components/mui/showConfirmDialog.js | 57 + .../mui/sortable-table/mui-table-sortable.js | 315 +++++ .../mui/sortable-table/styles.module.less | 14 + src/components/mui/sponsor-addon-select.js | 78 ++ src/components/mui/summit-addon-select.js | 74 ++ src/components/mui/summits-dropdown.js | 58 + .../mui/table/extra-rows/NotesRow.jsx | 29 + .../mui/table/extra-rows/TotalRow.jsx | 45 + src/components/mui/table/extra-rows/index.js | 15 + src/components/mui/table/mui-table.js | 308 +++++ .../mui/table/mui-table.module.less | 14 + src/i18n/en.json | 103 +- src/utils/actions.js | 20 + src/utils/constants.js | 49 + src/utils/methods.js | 3 + src/utils/query-actions.js | 238 +++- src/utils/reducers.js | 9 + webpack.common.js | 49 + yarn.lock | 886 +++++++++++-- 84 files changed, 11342 insertions(+), 136 deletions(-) create mode 100644 src/components/mui/FormItemTable/__tests__/FormItemTable.test.js create mode 100644 src/components/mui/FormItemTable/components/GlobalQuantityField.js create mode 100644 src/components/mui/FormItemTable/components/ItemTableField.js create mode 100644 src/components/mui/FormItemTable/components/UnderlyingAlertNote.js create mode 100644 src/components/mui/FormItemTable/helpers.js create mode 100644 src/components/mui/FormItemTable/index.js create mode 100644 src/components/mui/ItemSettingsModal/index.js create mode 100644 src/components/mui/NotesModal/index.js create mode 100644 src/components/mui/SnackbarNotification/Context.js create mode 100644 src/components/mui/SnackbarNotification/index.js create mode 100644 src/components/mui/__tests__/additional-input-list.test.js create mode 100644 src/components/mui/__tests__/additional-input.test.js create mode 100644 src/components/mui/__tests__/chip-list.test.js create mode 100644 src/components/mui/__tests__/item-price-tiers.test.js create mode 100644 src/components/mui/__tests__/meta-field-values.test.js create mode 100644 src/components/mui/__tests__/mui-formik-checkbox-group.test.js create mode 100644 src/components/mui/__tests__/mui-formik-datepicker.test.js create mode 100644 src/components/mui/__tests__/mui-formik-file-size-field.test.js create mode 100644 src/components/mui/__tests__/mui-formik-quantity-field.test.js create mode 100644 src/components/mui/__tests__/mui-formik-radio-group.test.js create mode 100644 src/components/mui/__tests__/mui-sponsor-input.test.js create mode 100644 src/components/mui/__tests__/mui-table-editable.test.js create mode 100644 src/components/mui/checkbox-list.js create mode 100644 src/components/mui/chip-list.js create mode 100644 src/components/mui/chip-notify.js create mode 100644 src/components/mui/chip-select-input.js create mode 100644 src/components/mui/confirm-dialog.js create mode 100644 src/components/mui/custom-alert.js create mode 100644 src/components/mui/dnd-list.js create mode 100644 src/components/mui/dropdown-checkbox.js create mode 100644 src/components/mui/editable-table/mui-table-editable.js create mode 100644 src/components/mui/editable-table/styles.module.less create mode 100644 src/components/mui/formik-inputs/additional-input/additional-input-list.js create mode 100644 src/components/mui/formik-inputs/additional-input/additional-input.js create mode 100644 src/components/mui/formik-inputs/additional-input/meta-field-values.js create mode 100644 src/components/mui/formik-inputs/company-input-mui.js create mode 100644 src/components/mui/formik-inputs/item-price-tiers.js create mode 100644 src/components/mui/formik-inputs/mui-formik-async-select.js create mode 100644 src/components/mui/formik-inputs/mui-formik-checkbox-group.js create mode 100644 src/components/mui/formik-inputs/mui-formik-checkbox.js create mode 100644 src/components/mui/formik-inputs/mui-formik-datepicker.js create mode 100644 src/components/mui/formik-inputs/mui-formik-discountfield.js create mode 100644 src/components/mui/formik-inputs/mui-formik-dropdown-checkbox.js create mode 100644 src/components/mui/formik-inputs/mui-formik-dropdown-radio.js create mode 100644 src/components/mui/formik-inputs/mui-formik-file-size-field.js create mode 100644 src/components/mui/formik-inputs/mui-formik-pricefield.js create mode 100644 src/components/mui/formik-inputs/mui-formik-quantity-field.js create mode 100644 src/components/mui/formik-inputs/mui-formik-radio-group.js create mode 100644 src/components/mui/formik-inputs/mui-formik-select-group.js create mode 100644 src/components/mui/formik-inputs/mui-formik-select.js create mode 100644 src/components/mui/formik-inputs/mui-formik-summit-addon-select.js create mode 100644 src/components/mui/formik-inputs/mui-formik-switch.js create mode 100644 src/components/mui/formik-inputs/mui-formik-textfield.js create mode 100644 src/components/mui/formik-inputs/mui-formik-timepicker.js create mode 100644 src/components/mui/formik-inputs/mui-formik-upload.js create mode 100644 src/components/mui/formik-inputs/mui-sponsor-input.js create mode 100644 src/components/mui/formik-inputs/sponsorship-input-mui.js create mode 100644 src/components/mui/formik-inputs/sponsorship-summit-select-mui.js create mode 100644 src/components/mui/infinite-table/index.js create mode 100644 src/components/mui/infinite-table/styles.module.less create mode 100644 src/components/mui/menu-button.js create mode 100644 src/components/mui/search-input.js create mode 100644 src/components/mui/showConfirmDialog.js create mode 100644 src/components/mui/sortable-table/mui-table-sortable.js create mode 100644 src/components/mui/sortable-table/styles.module.less create mode 100644 src/components/mui/sponsor-addon-select.js create mode 100644 src/components/mui/summit-addon-select.js create mode 100644 src/components/mui/summits-dropdown.js create mode 100644 src/components/mui/table/extra-rows/NotesRow.jsx create mode 100644 src/components/mui/table/extra-rows/TotalRow.jsx create mode 100644 src/components/mui/table/extra-rows/index.js create mode 100644 src/components/mui/table/mui-table.js create mode 100644 src/components/mui/table/mui-table.module.less diff --git a/package.json b/package.json index f89bb04f..74bf5a51 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,13 @@ "@babel/runtime": "^7.20.7", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^7.3.9", - "@mui/material": "^5.15.20", + "@mui/icons-material": "^6.4.3", + "@mui/material": "^6.4.3", + "@mui/x-date-pickers": "^7.26.0", "@react-pdf/renderer": "^3.1.11", + "@testing-library/jest-dom": "5.17.0", + "@testing-library/react": "12.1.5", + "@testing-library/user-event": "14.5.2", "awesome-bootstrap-checkbox": "^1.0.1", "babel-cli": "^6.26.0", "babel-jest": "^28.1.0", @@ -42,6 +46,7 @@ "file-loader": "^6.2.0", "final-form": "^4.20.7", "font-awesome": "^4.7.0", + "formik": "^2.4.6", "history": "^4.7.2", "i18n-react": "^0.6.4", "identity-obj-proxy": "^3.0.0", @@ -66,6 +71,7 @@ "path": "^0.12.7", "postcss-loader": "^6.2.1", "react": "^16.6.3", + "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^0.31.5", "react-datetime": "^2.16.2", "react-dnd": "^16.0.0", @@ -106,8 +112,9 @@ "peerDependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.20", - "@mui/material": "^5.15.20", + "@mui/icons-material": "^6.4.3", + "@mui/material": "^6.4.3", + "@mui/x-date-pickers": "^7.26.0", "@react-pdf/renderer": "^3.1.11", "awesome-bootstrap-checkbox": "^1.0.1", "browser-tabs-lock": "^1.2.15", @@ -116,6 +123,7 @@ "extend": "^3.0.1", "final-form": "^4.20.7", "font-awesome": "^4.7.0", + "formik": "^2.4.6", "history": "^4.7.2", "i18n-react": "^0.6.4", "idtoken-verifier": "^2.2.2", @@ -126,6 +134,7 @@ "moment": "^2.22.2", "moment-timezone": "^0.5.21", "react": "^16.6.3", + "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^0.31.5", "react-datetime": "^2.16.2", "react-dnd": "^16.0.0", @@ -134,6 +143,7 @@ "react-dropzone": "^4.2.9", "react-final-form": "^6.5.9", "react-google-maps": "^9.4.5", + "react-redux": "^5.0.7", "react-rte": "^0.16.3", "react-select": "^2.4.3", "react-star-ratings": "^2.3.0", diff --git a/src/components/index.js b/src/components/index.js index f59a678b..ea38031e 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -57,6 +57,55 @@ export {default as SummitVenuesSelect} from './inputs/summit-venues-select' export {default as BulkActionsSelector} from './bulk-actions-selector' export {default as ScheduleBuilderView} from './schedule-builder-view' +// mui components +export {default as MuiCheckboxList} from './mui/checkbox-list' +export {default as MuiChipList} from './mui/chip-list' +export {default as MuiChipNotify} from './mui/chip-notify' +export {default as MuiChipSelectInput} from './mui/chip-select-input' +export {default as MuiConfirmDialog} from './mui/confirm-dialog' +export {default as MuiCustomAlert} from './mui/custom-alert' +export {default as MuiDndList} from './mui/dnd-list' +export {default as MuiDropdownCheckbox} from './mui/dropdown-checkbox' +export {default as MuiMenuButton} from './mui/menu-button' +export {default as MuiSearchInput} from './mui/search-input' +export {default as MuiShowConfirmDialog} from './mui/showConfirmDialog' +export {default as MuiSponsorAddonSelect} from './mui/sponsor-addon-select' +export {default as MuiSummitAddonSelect} from './mui/summit-addon-select' +export {default as MuiSummitsDropdown} from './mui/summits-dropdown' +export {default as MuiFormItemTable} from './mui/FormItemTable' +export {default as MuiItemSettingsModal} from './mui/ItemSettingsModal' +export {default as MuiNotesModal} from './mui/NotesModal' +export {default as MuiSnackbarNotification} from './mui/SnackbarNotification' +export {default as MuiInfiniteTable} from './mui/infinite-table' +export {default as MuiEditableTable} from './mui/editable-table/mui-table-editable' +export {default as MuiSortableTable} from './mui/sortable-table/mui-table-sortable' +export {default as MuiTable} from './mui/table/mui-table' +export {default as MuiAdditionalInput} from './mui/formik-inputs/additional-input/additional-input' +export {default as MuiAdditionalInputList} from './mui/formik-inputs/additional-input/additional-input-list' +export {default as MuiFormikAsyncSelect} from './mui/formik-inputs/mui-formik-async-select' +export {default as MuiFormikCheckboxGroup} from './mui/formik-inputs/mui-formik-checkbox-group' +export {default as MuiFormikCheckbox} from './mui/formik-inputs/mui-formik-checkbox' +export {default as MuiFormikDatepicker} from './mui/formik-inputs/mui-formik-datepicker' +export {default as MuiFormikDiscountField} from './mui/formik-inputs/mui-formik-discountfield' +export {default as MuiFormikDropdownCheckbox} from './mui/formik-inputs/mui-formik-dropdown-checkbox' +export {default as MuiFormikDropdownRadio} from './mui/formik-inputs/mui-formik-dropdown-radio' +export {default as MuiFormikFileSizeField} from './mui/formik-inputs/mui-formik-file-size-field' +export {default as MuiFormikPriceField} from './mui/formik-inputs/mui-formik-pricefield' +export {default as MuiFormikQuantityField} from './mui/formik-inputs/mui-formik-quantity-field' +export {default as MuiFormikRadioGroup} from './mui/formik-inputs/mui-formik-radio-group' +export {default as MuiFormikSelectGroup} from './mui/formik-inputs/mui-formik-select-group' +export {default as MuiFormikSelect} from './mui/formik-inputs/mui-formik-select' +export {default as MuiFormikSummitAddonSelect} from './mui/formik-inputs/mui-formik-summit-addon-select' +export {default as MuiFormikSwitch} from './mui/formik-inputs/mui-formik-switch' +export {default as MuiFormikTextField} from './mui/formik-inputs/mui-formik-textfield' +export {default as MuiFormikTimepicker} from './mui/formik-inputs/mui-formik-timepicker' +export {default as MuiFormikUpload} from './mui/formik-inputs/mui-formik-upload' +export {default as MuiCompanyInput} from './mui/formik-inputs/company-input-mui' +export {default as MuiItemPriceTiers} from './mui/formik-inputs/item-price-tiers' +export {default as MuiSponsorInput} from './mui/formik-inputs/mui-sponsor-input' +export {default as MuiSponsorshipInput} from './mui/formik-inputs/sponsorship-input-mui' +export {default as MuiSponsorshipSummitSelect} from './mui/formik-inputs/sponsorship-summit-select-mui' + // this 5 includes 3rd party deps // export {default as ExtraQuestionsForm } from './extra-questions/index.js'; // export {default as GMap} from './google-map'; diff --git a/src/components/inputs/promocode-input.js b/src/components/inputs/promocode-input.js index 754a1b82..f6714331 100644 --- a/src/components/inputs/promocode-input.js +++ b/src/components/inputs/promocode-input.js @@ -14,7 +14,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; -import {DEFAULT_PAGE_SIZE, queryPromocodes} from '../../utils/query-actions'; +import {DEFAULT_PER_PAGE, queryPromocodes} from '../../utils/query-actions'; const PromocodeInput = ({summitId, error, value, onChange, id, multi, perPage, extraFilters, ...rest}) => { @@ -87,7 +87,7 @@ PromocodeInput.propTypes = { }; PromocodeInput.defaultProps = { - perPage: DEFAULT_PAGE_SIZE, + perPage: DEFAULT_PER_PAGE, extraFilters: [] }; diff --git a/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js b/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js new file mode 100644 index 00000000..0683609b --- /dev/null +++ b/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js @@ -0,0 +1,1096 @@ +/** + * 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. + * */ + +/* 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("sponsor_edit_form.code") + ).toBeInTheDocument(); + expect( + screen.getByText("sponsor_edit_form.description") + ).toBeInTheDocument(); + expect( + screen.getByText("sponsor_edit_form.early_bird_rate") + ).toBeInTheDocument(); + expect( + screen.getByText("sponsor_edit_form.standard_rate") + ).toBeInTheDocument(); + expect( + screen.getByText("sponsor_edit_form.onsite_rate") + ).toBeInTheDocument(); + expect( + screen.getByText("sponsor_edit_form.qty") + ).toBeInTheDocument(); + expect( + screen.getByText("sponsor_edit_form.total") + ).toBeInTheDocument(); + expect( + screen.getByText("sponsor_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("sponsor_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("sponsor_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("sponsor_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 new file mode 100644 index 00000000..9f8751f9 --- /dev/null +++ b/src/components/mui/FormItemTable/components/GlobalQuantityField.js @@ -0,0 +1,71 @@ +/** + * 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 new file mode 100644 index 00000000..2f5cdea1 --- /dev/null +++ b/src/components/mui/FormItemTable/components/ItemTableField.js @@ -0,0 +1,99 @@ +/** + * 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 new file mode 100644 index 00000000..49008b23 --- /dev/null +++ b/src/components/mui/FormItemTable/components/UnderlyingAlertNote.js @@ -0,0 +1,41 @@ +/** + * 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 { 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("sponsor_edit_form.additional_info")} + + ); +}; + +export default UnderlyingAlertNote; diff --git a/src/components/mui/FormItemTable/helpers.js b/src/components/mui/FormItemTable/helpers.js new file mode 100644 index 00000000..03315030 --- /dev/null +++ b/src/components/mui/FormItemTable/helpers.js @@ -0,0 +1,43 @@ +/** + * 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 { epochToMomentTimeZone } from "../../../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 new file mode 100644 index 00000000..e5855cfd --- /dev/null +++ b/src/components/mui/FormItemTable/index.js @@ -0,0 +1,302 @@ +/** + * 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 "../../../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("sponsor_edit_form.code")} + + + {T.translate("sponsor_edit_form.description")} + + + {T.translate("sponsor_edit_form.custom_rate")} + + + {T.translate("sponsor_edit_form.early_bird_rate")} + + + {T.translate("sponsor_edit_form.standard_rate")} + + + {T.translate("sponsor_edit_form.onsite_rate")} + + {extraColumns.map((exc) => ( + {exc.name} + ))} + + {T.translate("sponsor_edit_form.qty")} + + + {/* item level extra field */} + + {T.translate("sponsor_edit_form.total")} + + + {T.translate("sponsor_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("sponsor_edit_form.discount")} + + {/* eslint-disable-next-line */} + {new Array(totalColumns - 5).fill(0).map((_, i) => ( + + ))} + + + {Object.values(DISCOUNT_TYPES).map((p) => ( + + {p} + + ))} + + + + + + + + + + + {T.translate("sponsor_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 new file mode 100644 index 00000000..d6788f28 --- /dev/null +++ b/src/components/mui/ItemSettingsModal/index.js @@ -0,0 +1,91 @@ +/** + * 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("general.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 new file mode 100644 index 00000000..150cf6dd --- /dev/null +++ b/src/components/mui/NotesModal/index.js @@ -0,0 +1,90 @@ +/** + * 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("sponsor_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( + "sponsor_edit_form.notes_placeholder" + )} + /> + + + + + + ); +}; + +NotesModal.propTypes = { + item: PropTypes.object, + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired +}; + +export default NotesModal; diff --git a/src/components/mui/SnackbarNotification/Context.js b/src/components/mui/SnackbarNotification/Context.js new file mode 100644 index 00000000..b7e8926a --- /dev/null +++ b/src/components/mui/SnackbarNotification/Context.js @@ -0,0 +1,20 @@ +/** + * 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 { createContext, useContext } from "react"; + +const SnackbarNotificationContext = createContext(null); + +export const useSnackbarMessage = () => useContext(SnackbarNotificationContext); + +export default SnackbarNotificationContext; diff --git a/src/components/mui/SnackbarNotification/index.js b/src/components/mui/SnackbarNotification/index.js new file mode 100644 index 00000000..5b46ccfc --- /dev/null +++ b/src/components/mui/SnackbarNotification/index.js @@ -0,0 +1,106 @@ +/** + * 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, useMemo, useState } from "react"; +import { connect } from "react-redux"; +import PropTypes from "prop-types"; +import { Alert, Snackbar, Typography } from "@mui/material"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import ErrorIcon from "@mui/icons-material/Error"; +import SnackbarNotificationContext from "./Context"; +import { NOTIFICATION_TIMEOUT } from "../../../utils/constants"; +import { clearSnackbarMessage } from "../../../utils/actions"; + +/* + This component works in two ways: + - useSnackbarMessage hook that returns successMessage and errorMessage methods to trigger the snackbar. + - snackbarErrorHandler and snackbarSuccessHandler actions from base-actions that change the base reducer + */ + +const SnackbarNotification = ({ + children, + snackbarMessage, + clearSnackbarMessage +}) => { + const [open, setOpen] = useState(false); + const [msgData, setMsgData] = useState({}); + // this two methods are for on-demand messaging + const successMessage = (msg) => setMsgData({ html: msg, type: "success" }); + const errorMessage = (msg) => setMsgData({ html: msg, type: "warning" }); + const messageContext = useMemo(() => ({ successMessage, errorMessage }), []); + + const clearMessage = () => { + setMsgData({}); + clearSnackbarMessage(); + }; + + const onClose = () => { + setOpen(false); + setTimeout(clearMessage, NOTIFICATION_TIMEOUT); + }; + + useEffect(() => { + if (msgData.html) { + setOpen(true); + } + }, [msgData]); + + // when snackbarMessage changes in base-reducer, we trigger the snackbar + useEffect(() => { + if (snackbarMessage?.html) { + setMsgData(snackbarMessage); + } + }, [snackbarMessage]); + + return ( + + + , + error: + }} + > + + + + {children} + + ); +}; + +SnackbarNotification.propTypes = { + children: PropTypes.node, + snackbarMessage: PropTypes.object +}; + +const mapStateToProps = ({ baseState }) => ({ + snackbarMessage: baseState.snackbarMessage +}); + +export default connect(mapStateToProps, { clearSnackbarMessage })( + SnackbarNotification +); diff --git a/src/components/mui/__tests__/additional-input-list.test.js b/src/components/mui/__tests__/additional-input-list.test.js new file mode 100644 index 00000000..42a421fd --- /dev/null +++ b/src/components/mui/__tests__/additional-input-list.test.js @@ -0,0 +1,362 @@ +/** + * 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 { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form, useFormikContext } from "formik"; +import "@testing-library/jest-dom"; +import AdditionalInputList from "../formik-inputs/additional-input/additional-input-list"; +import showConfirmDialog from "../showConfirmDialog"; + +// Mocks +jest.mock("../showConfirmDialog", () => jest.fn()); + +jest.mock( + "../formik-inputs/additional-input/additional-input", + () => + function MockAdditionalInput({ + item, + itemIdx, + onAdd, + onDelete, + isAddDisabled + }) { + return ( +
+ {item.name} + {item.type} + + +
+ ); + } +); + +// Helper function to render the component with Formik +const renderWithFormik = (props, initialValues = { meta_fields: [] }) => + render( + +
+ + +
+ ); + +describe("AdditionalInputList", () => { + const defaultMetaField = { + id: 1, + name: "Field 1", + type: "Text", + is_required: false, + minimum_quantity: 0, + maximum_quantity: 0, + values: [] + }; + + const defaultProps = { + name: "meta_fields", + onDelete: jest.fn(), + onDeleteValue: jest.fn(), + entityId: 1 + }; + + const defaultInitialValues = { + meta_fields: [defaultMetaField] + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Rendering", () => { + test("renders an AdditionalInput for each meta field", () => { + const multipleFields = { + meta_fields: [ + { ...defaultMetaField, id: 1, name: "Field 1" }, + { ...defaultMetaField, id: 2, name: "Field 2" }, + { ...defaultMetaField, id: 3, name: "Field 3" } + ] + }; + + renderWithFormik(defaultProps, multipleFields); + + expect(screen.getByTestId("additional-input-0")).toBeInTheDocument(); + expect(screen.getByTestId("additional-input-1")).toBeInTheDocument(); + expect(screen.getByTestId("additional-input-2")).toBeInTheDocument(); + expect(screen.getByTestId("item-name-0")).toHaveTextContent("Field 1"); + expect(screen.getByTestId("item-name-1")).toHaveTextContent("Field 2"); + expect(screen.getByTestId("item-name-2")).toHaveTextContent("Field 3"); + }); + + test("renders a default metafield when meta_fields is empty", () => { + renderWithFormik(defaultProps, { meta_fields: [] }); + + // Should render one default empty field + expect(screen.getByTestId("additional-input-0")).toBeInTheDocument(); + expect(screen.getByTestId("item-name-0")).toHaveTextContent(""); + expect(screen.getByTestId("item-type-0")).toHaveTextContent(""); + }); + }); + + describe("handleAddItem", () => { + test("adds a new empty meta field when onAdd is called", async () => { + const TestWrapper = () => { + const { values } = useFormikContext(); + return ( + <> + +
{values.meta_fields.length}
+ + ); + }; + + render( + +
+ + +
+ ); + + expect(screen.getByTestId("field-count")).toHaveTextContent("1"); + + const addButton = screen.getByTestId("add-btn-0"); + await userEvent.click(addButton); + + await waitFor(() => { + expect(screen.getByTestId("field-count")).toHaveTextContent("2"); + }); + }); + }); + + describe("handleRemove", () => { + test("shows confirmation dialog when delete is clicked", async () => { + showConfirmDialog.mockResolvedValue(false); + + renderWithFormik(defaultProps, defaultInitialValues); + + const deleteButton = screen.getByTestId("delete-btn-0"); + await userEvent.click(deleteButton); + + expect(showConfirmDialog).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.any(String), + type: "warning" + }) + ); + }); + + test("calls API and removes from UI when item has id", async () => { + const mockOnDelete = jest.fn().mockResolvedValue(); + showConfirmDialog.mockResolvedValue(true); + + const TestWrapper = ({ onDelete }) => { + const { values } = useFormikContext(); + return ( + <> + +
{values.meta_fields.length}
+ + ); + }; + + render( + +
+ + +
+ ); + + expect(screen.getByTestId("field-count")).toHaveTextContent("1"); + + const deleteButton = screen.getByTestId("delete-btn-0"); + await userEvent.click(deleteButton); + + await waitFor(() => { + expect(mockOnDelete).toHaveBeenCalledWith(1, 1); // entityId, item.id + }); + }); + + test("removes from UI without API call when item has no id", async () => { + const mockOnDelete = jest.fn(); + showConfirmDialog.mockResolvedValue(true); + + const fieldWithoutId = { + name: "New Field", + type: "Text", + is_required: false, + values: [] + }; + + const TestWrapper = ({ onDelete }) => { + const { values } = useFormikContext(); + return ( + <> + +
{values.meta_fields.length}
+ + ); + }; + + render( + +
+ + +
+ ); + + expect(screen.getByTestId("field-count")).toHaveTextContent("2"); + + // remove the second field (without id) + const deleteButton = screen.getByTestId("delete-btn-1"); + await userEvent.click(deleteButton); + + await waitFor(() => { + expect(mockOnDelete).not.toHaveBeenCalled(); + expect(screen.getByTestId("field-count")).toHaveTextContent("1"); + }); + }); + + test("resets to default meta field when last item is deleted", async () => { + const mockOnDelete = jest.fn().mockResolvedValue(); + showConfirmDialog.mockResolvedValue(true); + + const TestWrapper = ({ onDelete }) => { + const { values } = useFormikContext(); + return ( + <> + +
{values.meta_fields.length}
+
+ {values.meta_fields[0]?.name || "empty"} +
+ + ); + }; + + render( + +
+ + +
+ ); + + const deleteButton = screen.getByTestId("delete-btn-0"); + await userEvent.click(deleteButton); + + await waitFor(() => { + // should still have 1 field (the default empty one) + expect(screen.getByTestId("field-count")).toHaveTextContent("1"); + // field should be reset to empty + expect(screen.getByTestId("first-field-name")).toHaveTextContent( + "empty" + ); + }); + }); + }); + + describe("areMetafieldsIncomplete", () => { + test("disables add button when name is empty", () => { + const fieldWithEmptyName = { ...defaultMetaField, name: "" }; + + renderWithFormik(defaultProps, { meta_fields: [fieldWithEmptyName] }); + + expect(screen.getByTestId("add-btn-0")).toBeDisabled(); + }); + + test("disables add button when type is empty", () => { + const fieldWithEmptyType = { + ...defaultMetaField, + name: "Field", + type: "" + }; + + renderWithFormik(defaultProps, { meta_fields: [fieldWithEmptyType] }); + + expect(screen.getByTestId("add-btn-0")).toBeDisabled(); + }); + + test("disables add button when type with options has no values", () => { + const fieldWithNoValues = { + ...defaultMetaField, + name: "Field", + type: "CheckBoxList", + values: [] + }; + + renderWithFormik(defaultProps, { meta_fields: [fieldWithNoValues] }); + + expect(screen.getByTestId("add-btn-0")).toBeDisabled(); + }); + + test("disables add button when values are incomplete", () => { + const fieldWithIncompleteValues = { + ...defaultMetaField, + name: "Field", + type: "ComboBox", + values: [{ name: "Option", value: "" }] // value is empty + }; + + renderWithFormik(defaultProps, { + meta_fields: [fieldWithIncompleteValues] + }); + + expect(screen.getByTestId("add-btn-0")).toBeDisabled(); + }); + + test("enables add button when all fields are complete", () => { + const completeField = { + ...defaultMetaField, + name: "Field", + type: "Text" + }; + + renderWithFormik(defaultProps, { meta_fields: [completeField] }); + + expect(screen.getByTestId("add-btn-0")).not.toBeDisabled(); + }); + + test("enables add button when type with options has complete values", () => { + const completeFieldWithValues = { + ...defaultMetaField, + name: "Field", + type: "CheckBoxList", + values: [{ name: "Option 1", value: "opt1" }] + }; + + renderWithFormik(defaultProps, { + meta_fields: [completeFieldWithValues] + }); + + expect(screen.getByTestId("add-btn-0")).not.toBeDisabled(); + }); + }); +}); diff --git a/src/components/mui/__tests__/additional-input.test.js b/src/components/mui/__tests__/additional-input.test.js new file mode 100644 index 00000000..cb604cf4 --- /dev/null +++ b/src/components/mui/__tests__/additional-input.test.js @@ -0,0 +1,266 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import AdditionalInput from "../formik-inputs/additional-input/additional-input"; + +// Mocks +jest.mock( + "../formik-inputs/additional-input/meta-field-values", + () => + function MockMetaFieldValues() { + return
MetaFieldValues
; + } +); + +// Helper function to render the component with Formik +const renderWithFormik = (props, initialValues = { meta_fields: [] }) => + render( + +
+ + +
+ ); + +describe("AdditionalInput", () => { + const defaultItem = { + id: 1, + name: "Test Field", + type: "", + is_required: false, + minimum_quantity: 0, + maximum_quantity: 0, + values: [] + }; + + const defaultProps = { + item: defaultItem, + itemIdx: 0, + baseName: "meta_fields", + onAdd: jest.fn(), + onDelete: jest.fn(), + onDeleteValue: jest.fn(), + entityId: 1, + isAddDisabled: false + }; + + const defaultInitialMetaFields = { + meta_fields: [defaultItem] + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Rendering", () => { + test("renders name, type and is_required fields", () => { + renderWithFormik(defaultProps, defaultInitialMetaFields); + + expect( + screen.getByPlaceholderText( + "additional_inputs.placeholders.title" + ) + ).toBeInTheDocument(); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + expect(screen.getByRole("checkbox")).toBeInTheDocument(); + }); + + test("renders add and delete buttons", () => { + renderWithFormik(defaultProps, defaultInitialMetaFields); + + expect( + screen.getByRole("button", { name: /delete/i }) + ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /add/i })).toBeInTheDocument(); + }); + }); + + describe("Conditional rendering based on type", () => { + test("shows MetaFieldValues when type is CheckBoxList", () => { + const itemWithOptions = { ...defaultItem, type: "CheckBoxList" }; + + renderWithFormik( + { ...defaultProps, item: itemWithOptions }, + { meta_fields: [itemWithOptions] } + ); + + expect(screen.getByTestId("meta-field-values")).toBeInTheDocument(); + }); + + test("shows MetaFieldValues when type is ComboBox", () => { + const itemWithOptions = { ...defaultItem, type: "ComboBox" }; + + renderWithFormik( + { ...defaultProps, item: itemWithOptions }, + { meta_fields: [itemWithOptions] } + ); + + expect(screen.getByTestId("meta-field-values")).toBeInTheDocument(); + }); + + test("shows MetaFieldValues when type is RadioButtonList", () => { + const itemWithOptions = { ...defaultItem, type: "RadioButtonList" }; + + renderWithFormik( + { ...defaultProps, item: itemWithOptions }, + { meta_fields: [itemWithOptions] } + ); + + expect(screen.getByTestId("meta-field-values")).toBeInTheDocument(); + }); + + test("does not show MetaFieldValues when type is Text", () => { + renderWithFormik(defaultProps, defaultInitialMetaFields); + + expect(screen.queryByTestId("meta-field-values")).not.toBeInTheDocument(); + }); + + test("shows quantity fields when type is Quantity", () => { + const itemQuantity = { ...defaultItem, type: "Quantity" }; + + renderWithFormik( + { ...defaultProps, item: itemQuantity }, + { meta_fields: [itemQuantity] } + ); + + expect( + screen.getByPlaceholderText( + "additional_inputs.placeholders.minimum_quantity" + ) + ).toBeInTheDocument(); + expect( + screen.getByPlaceholderText( + "additional_inputs.placeholders.maximum_quantity" + ) + ).toBeInTheDocument(); + }); + + test("does not show quantity fields when type is not Quantity", () => { + renderWithFormik(defaultProps, defaultInitialMetaFields); + + expect( + screen.queryByPlaceholderText( + "additional_inputs.placeholders.minimum_quantity" + ) + ).not.toBeInTheDocument(); + expect( + screen.queryByPlaceholderText( + "additional_inputs.placeholders.maximum_quantity" + ) + ).not.toBeInTheDocument(); + }); + }); + + describe("Button interactions", () => { + test("calls onDelete with item and index when delete button is clicked", async () => { + const mockOnDelete = jest.fn(); + + renderWithFormik( + { ...defaultProps, onDelete: mockOnDelete }, + defaultInitialMetaFields + ); + + const deleteButton = screen.getByRole("button", { name: /delete/i }); + await userEvent.click(deleteButton); + + expect(mockOnDelete).toHaveBeenCalledWith(defaultItem, 0); + }); + + test("calls onAdd when add button is clicked", async () => { + const mockOnAdd = jest.fn(); + + renderWithFormik( + { ...defaultProps, onAdd: mockOnAdd }, + defaultInitialMetaFields + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + await userEvent.click(addButton); + + expect(mockOnAdd).toHaveBeenCalled(); + }); + + test("disables add button when isAddDisabled is true", () => { + renderWithFormik( + { ...defaultProps, isAddDisabled: true }, + defaultInitialMetaFields + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).toBeDisabled(); + }); + + test("enables add button when isAddDisabled is false", () => { + renderWithFormik( + { ...defaultProps, isAddDisabled: false }, + defaultInitialMetaFields + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).not.toBeDisabled(); + }); + }); + + describe("Error display", () => { + test("shows values error when touched and has error", () => { + const itemWithOptions = { ...defaultItem, type: "CheckBoxList" }; + + render( + +
+ + +
+ ); + + expect( + screen.getByText("At least one option required") + ).toBeInTheDocument(); + }); + + test("does not show values error when not touched", () => { + const itemWithOptions = { ...defaultItem, type: "CheckBoxList" }; + + render( + +
+ + +
+ ); + + expect( + screen.queryByText("At least one option required") + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/mui/__tests__/chip-list.test.js b/src/components/mui/__tests__/chip-list.test.js new file mode 100644 index 00000000..4a2f5e07 --- /dev/null +++ b/src/components/mui/__tests__/chip-list.test.js @@ -0,0 +1,87 @@ +/** + * 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. + * */ + +// chip-list.test.js +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import ChipList from "../chip-list"; + +// Mock MUI Tooltip because it uses portals which are hard to test +jest.mock("@mui/material/Tooltip", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ children, title }) => ( +
+ {children} +
+ ) + }; +}); + +describe("ChipList", () => { + test("renders all chips when below maxLength", () => { + const chips = ["React", "Jest", "JavaScript"]; + const maxLength = 5; + + render(); + + chips.forEach((chip) => { + expect(screen.getByText(chip)).toBeInTheDocument(); + }); + expect(screen.queryByText("...")).not.toBeInTheDocument(); + }); + + test("renders maxLength chips and ellipsis when chips exceed maxLength", () => { + const chips = ["React", "Jest", "JavaScript", "TypeScript", "HTML", "CSS"]; + const maxLength = 3; + + render(); + + // First 3 should be visible + expect(screen.getByText("React")).toBeInTheDocument(); + expect(screen.getByText("Jest")).toBeInTheDocument(); + expect(screen.getByText("JavaScript")).toBeInTheDocument(); + + // Rest should not be directly visible + expect(screen.queryByText("TypeScript")).not.toBeInTheDocument(); + expect(screen.queryByText("HTML")).not.toBeInTheDocument(); + expect(screen.queryByText("CSS")).not.toBeInTheDocument(); + + // Ellipsis chip should be present + expect(screen.getByText("...")).toBeInTheDocument(); + }); + + test("tooltip contains remaining chips when some are hidden", () => { + const chips = ["React", "Jest", "JavaScript", "TypeScript", "HTML", "CSS"]; + const maxLength = 3; + + render(); + + const tooltip = screen.getByTestId("tooltip"); + // Check if tooltip contains 3 items (remaining chips) + expect(tooltip).toHaveAttribute("data-title", "3"); + expect(tooltip).toContainElement(screen.getByText("...")); + }); + + test("renders empty box when no chips are provided", () => { + render(); + + // Box should be empty, no chips or ellipsis + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/item-price-tiers.test.js b/src/components/mui/__tests__/item-price-tiers.test.js new file mode 100644 index 00000000..a3cb1f1d --- /dev/null +++ b/src/components/mui/__tests__/item-price-tiers.test.js @@ -0,0 +1,155 @@ +/** + * 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 { render, screen, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import ItemPriceTiers from "../formik-inputs/item-price-tiers"; + +jest.mock("i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +const ALL_ENABLED = { + early_bird_rate: 0, + standard_rate: 500, + onsite_rate: 1000 +}; + +const ALL_DISABLED = { + early_bird_rate: null, + standard_rate: null, + onsite_rate: null +}; + +const MIXED = { + early_bird_rate: 0, + standard_rate: null, + onsite_rate: 1000 +}; + +const renderComponent = (initialValues, props = {}, onSubmit = jest.fn()) => + render( + +
+ + + +
+ ); + +describe("ItemPriceTiers", () => { + describe("all enabled", () => { + it("should render all 3 checkboxes as unchecked", () => { + renderComponent(ALL_ENABLED); + const checkboxes = screen.getAllByRole("checkbox"); + expect(checkboxes).toHaveLength(3); + checkboxes.forEach((cb) => expect(cb).not.toBeChecked()); + }); + }); + + describe("all disabled", () => { + it("should render all 3 checkboxes as checked", () => { + renderComponent(ALL_DISABLED); + const checkboxes = screen.getAllByRole("checkbox"); + expect(checkboxes).toHaveLength(3); + checkboxes.forEach((cb) => expect(cb).toBeChecked()); + }); + }); + + describe("mixed state", () => { + it("should render the correct number of checked and unchecked checkboxes", () => { + renderComponent(MIXED); + const checkboxes = screen.getAllByRole("checkbox"); + const checked = checkboxes.filter((cb) => cb.checked); + const unchecked = checkboxes.filter((cb) => !cb.checked); + expect(checked).toHaveLength(1); + expect(unchecked).toHaveLength(2); + }); + }); + + describe("marking as N/A", () => { + it("should check the checkbox when an active tier is clicked", async () => { + renderComponent(ALL_ENABLED); + const checkboxes = screen.getAllByRole("checkbox"); + + await act(async () => { + await userEvent.click(checkboxes[0]); + }); + + expect(checkboxes[0]).toBeChecked(); + }); + + it("should set the field value to null when marked as N/A", async () => { + const onSubmit = jest.fn(); + renderComponent(ALL_ENABLED, {}, onSubmit); + const checkboxes = screen.getAllByRole("checkbox"); + + await act(async () => { + await userEvent.click(checkboxes[0]); + await userEvent.click(screen.getByText("submit")); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ early_bird_rate: null }), + expect.anything() + ); + }); + }); + + describe("enabling", () => { + it("should uncheck the checkbox when an N/A tier is clicked", async () => { + renderComponent(ALL_DISABLED); + const checkboxes = screen.getAllByRole("checkbox"); + + await act(async () => { + await userEvent.click(checkboxes[0]); + }); + + expect(checkboxes[0]).not.toBeChecked(); + }); + + it("should set the field value to 0 when enabled", async () => { + const onSubmit = jest.fn(); + renderComponent(ALL_DISABLED, {}, onSubmit); + const checkboxes = screen.getAllByRole("checkbox"); + + await act(async () => { + await userEvent.click(checkboxes[0]); + await userEvent.click(screen.getByText("submit")); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ early_bird_rate: 0 }), + expect.anything() + ); + }); + }); + + describe("readOnly prop", () => { + it("should disable all checkboxes when readOnly is true", () => { + renderComponent(ALL_ENABLED, { readOnly: true }); + const checkboxes = screen.getAllByRole("checkbox"); + checkboxes.forEach((cb) => expect(cb).toBeDisabled()); + }); + + it("should not disable checkboxes when readOnly is false", () => { + renderComponent(ALL_ENABLED, { readOnly: false }); + const checkboxes = screen.getAllByRole("checkbox"); + checkboxes.forEach((cb) => expect(cb).not.toBeDisabled()); + }); + }); +}); diff --git a/src/components/mui/__tests__/meta-field-values.test.js b/src/components/mui/__tests__/meta-field-values.test.js new file mode 100644 index 00000000..4c0e20a5 --- /dev/null +++ b/src/components/mui/__tests__/meta-field-values.test.js @@ -0,0 +1,332 @@ +/** + * 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 { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form, useFormikContext } from "formik"; +import "@testing-library/jest-dom"; +import MetaFieldValues from "../formik-inputs/additional-input/meta-field-values"; +import showConfirmDialog from "../showConfirmDialog"; + +// Mocks +jest.mock("../showConfirmDialog", () => jest.fn()); + +jest.mock( + "../dnd-list", + () => + function MockDragAndDropList({ items, renderItem }) { + return ( +
+ {items.map((item, index) => ( +
+ {renderItem(item, index, {}, { isDragging: false })} +
+ ))} +
+ ); + } +); + +// Helper function to render the component with Formik +const renderWithFormik = (props, initialValues = { meta_fields: [] }) => + render( + +
+ + +
+ ); + +describe("MetaFieldValues", () => { + const defaultField = { + id: 1, + name: "Test Field", + type: "CheckBoxList", + values: [ + { id: 101, name: "Option 1", value: "opt1", is_default: false, order: 1 }, + { id: 102, name: "Option 2", value: "opt2", is_default: true, order: 2 } + ] + }; + + const defaultProps = { + field: defaultField, + fieldIndex: 0, + baseName: "meta_fields", + onMetaFieldTypeValueDeleted: jest.fn(), + entityId: 1 + }; + + const defaultInitialValues = { + meta_fields: [defaultField] + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Rendering", () => { + test("renders all field values sorted by order prop", () => { + const fieldWithUnorderedValues = { + ...defaultField, + values: [ + { id: 103, name: "Option 3", value: "opt3", order: 3 }, + { id: 101, name: "Option 1", value: "opt1", order: 1 }, + { id: 102, name: "Option 2", value: "opt2", order: 2 } + ] + }; + + renderWithFormik( + { ...defaultProps, field: fieldWithUnorderedValues }, + { meta_fields: [fieldWithUnorderedValues] } + ); + + // verify all values are rendered + const items = screen.getAllByPlaceholderText( + "meta_fields.placeholders.name" + ); + expect(items).toHaveLength(3); + + // verify the values are rendered using the order prop + expect(items[0]).toHaveValue("Option 1"); + expect(items[1]).toHaveValue("Option 2"); + expect(items[2]).toHaveValue("Option 3"); + }); + }); + + describe("handleAddValue", () => { + test("adds a new empty value when add button is clicked", async () => { + // Componente wrapper que sincroniza field con Formik + const TestWrapper = () => { + const { values } = useFormikContext(); + const field = values.meta_fields[0]; + return ( + + ); + }; + + render( + +
+ + +
+ ); + + // Verificar cantidad inicial + const initialInputs = screen.getAllByPlaceholderText( + "meta_fields.placeholders.name" + ); + expect(initialInputs).toHaveLength(2); + + // Click en agregar + const addButton = screen.getByRole("button", { name: /add/i }); + await userEvent.click(addButton); + + // Esperar actualización + await waitFor(() => { + const updatedInputs = screen.getAllByPlaceholderText( + "meta_fields.placeholders.name" + ); + expect(updatedInputs).toHaveLength(3); + }); + }); + }); + + describe("isMetafieldValueIncomplete", () => { + test("disables add button when a value name is empty", () => { + const fieldWithIncomplete = { + ...defaultField, + values: [ + { id: 101, name: "", value: "opt1", is_default: false, order: 1 } + ] + }; + + renderWithFormik( + { ...defaultProps, field: fieldWithIncomplete }, + { meta_fields: [fieldWithIncomplete] } + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).toBeDisabled(); + }); + + test("disables add button when a value is empty", () => { + const fieldWithIncomplete = { + ...defaultField, + values: [ + { id: 101, name: "Option", value: "", is_default: false, order: 1 } + ] + }; + + renderWithFormik( + { ...defaultProps, field: fieldWithIncomplete }, + { meta_fields: [fieldWithIncomplete] } + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).toBeDisabled(); + }); + + test("enables add button when all values are complete", () => { + renderWithFormik(defaultProps, defaultInitialValues); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).not.toBeDisabled(); + }); + + test("enables add button when there are no values", () => { + const fieldWithNoValues = { ...defaultField, values: [] }; + + renderWithFormik( + { ...defaultProps, field: fieldWithNoValues }, + { meta_fields: [fieldWithNoValues] } + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).not.toBeDisabled(); + }); + }); + + describe("handleDefaultChange", () => { + test("only one value can be default at a time", async () => { + renderWithFormik(defaultProps, defaultInitialValues); + + const checkboxes = screen.getAllByRole("checkbox"); + + // Option 2 is default + expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[0]).not.toBeChecked(); + + // click on Option 1 + await userEvent.click(checkboxes[0]); + + // Option 1 should be checked and Option 2 unchecked + expect(checkboxes[0]).toBeChecked(); + expect(checkboxes[1]).not.toBeChecked(); + }); + }); + + describe("handleRemoveValue", () => { + test("shows confirmation dialog when remove is clicked", async () => { + showConfirmDialog.mockResolvedValue(false); + renderWithFormik(defaultProps, defaultInitialValues); + + const closeIcons = screen.getAllByTestId("CloseIcon"); + const closeButton = closeIcons[0].closest("button"); + await userEvent.click(closeButton); + + expect(showConfirmDialog).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.any(String), + type: "warning" + }) + ); + }); + + test("calls API and removes from UI when value has id", async () => { + const mockOnDelete = jest.fn().mockResolvedValue(); + showConfirmDialog.mockResolvedValue(true); + + const TestWrapper = ({ onDelete }) => { + const { values } = useFormikContext(); + const field = values.meta_fields[0]; + return ( + + ); + }; + + render( + +
+ + +
+ ); + + expect(screen.getAllByTestId("CloseIcon")).toHaveLength(2); + + const closeButton = screen + .getAllByTestId("CloseIcon")[0] + .closest("button"); + await userEvent.click(closeButton); + + await waitFor(() => { + expect(mockOnDelete).toHaveBeenCalledWith(1, 1, 101); + expect(screen.getAllByTestId("CloseIcon")).toHaveLength(1); + }); + }); + + test("removes from UI without API call when value has no id", async () => { + const mockOnDelete = jest.fn(); + showConfirmDialog.mockResolvedValue(true); + + const fieldWithoutIds = { + ...defaultField, + values: [ + { name: "Option 1", value: "opt1", is_default: false, order: 1 }, + { name: "Option 2", value: "opt2", is_default: false, order: 2 } + ] + }; + + const TestWrapper = ({ onDelete }) => { + const { values } = useFormikContext(); + const field = values.meta_fields[0]; + return ( + + ); + }; + + render( + +
+ + +
+ ); + + expect(screen.getAllByTestId("CloseIcon")).toHaveLength(2); + + const closeButton = screen + .getAllByTestId("CloseIcon")[0] + .closest("button"); + await userEvent.click(closeButton); + + await waitFor(() => { + expect(mockOnDelete).not.toHaveBeenCalled(); + expect(screen.getAllByTestId("CloseIcon")).toHaveLength(1); + }); + }); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-checkbox-group.test.js b/src/components/mui/__tests__/mui-formik-checkbox-group.test.js new file mode 100644 index 00000000..8fde91cc --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-checkbox-group.test.js @@ -0,0 +1,176 @@ +/** + * 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. + * */ + +// mui-formik-checkbox-group.test.js +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikCheckboxGroup from "../formik-inputs/mui-formik-checkbox-group"; + +// Helper function to render the component with Formik +const renderWithFormik = (props, initialValues = { testField: [] }) => + render( + +
+ + +
+ ); + +describe("MuiFormikCheckboxGroup", () => { + const options = [ + { value: 1, label: "Option 1" }, + { value: 2, label: "Option 2" }, + { value: 3, label: "Option 3" } + ]; + + test("renders the component with label", () => { + renderWithFormik({ label: "Test Label", options }); + + expect(screen.getByText("Test Label")).toBeInTheDocument(); + expect(screen.getByText("Option 1")).toBeInTheDocument(); + expect(screen.getByText("Option 2")).toBeInTheDocument(); + expect(screen.getByText("Option 3")).toBeInTheDocument(); + }); + + test("renders all checkboxes unchecked when no values are selected", () => { + renderWithFormik({ options }); + + const checkboxes = screen.getAllByRole("checkbox"); + expect(checkboxes).toHaveLength(3); + checkboxes.forEach((checkbox) => { + expect(checkbox).not.toBeChecked(); + }); + }); + + test("renders with pre-selected values", () => { + renderWithFormik({ options }, { testField: [1, 3] }); + + const checkboxes = screen.getAllByRole("checkbox"); + expect(checkboxes[0]).toBeChecked(); // Option 1 + expect(checkboxes[1]).not.toBeChecked(); // Option 2 + expect(checkboxes[2]).toBeChecked(); // Option 3 + }); + + test("handles checking a checkbox", async () => { + renderWithFormik({ options }); + + const checkboxes = screen.getAllByRole("checkbox"); + await userEvent.click(checkboxes[0]); + + // After clicking, the first checkbox should be checked + expect(checkboxes[0]).toBeChecked(); + expect(checkboxes[1]).not.toBeChecked(); + expect(checkboxes[2]).not.toBeChecked(); + }); + + test("handles unchecking a checkbox", async () => { + renderWithFormik({ options }, { testField: [1, 2] }); + + const checkboxes = screen.getAllByRole("checkbox"); + // Initial state + expect(checkboxes[0]).toBeChecked(); + expect(checkboxes[1]).toBeChecked(); + + // Uncheck the first checkbox + await userEvent.click(checkboxes[0]); + + // After clicking, only the second checkbox should remain checked + expect(checkboxes[0]).not.toBeChecked(); + expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[2]).not.toBeChecked(); + }); + + test("handles non-array initial values gracefully", () => { + renderWithFormik({ options }, { testField: null }); + + const checkboxes = screen.getAllByRole("checkbox"); + expect(checkboxes).toHaveLength(3); + checkboxes.forEach((checkbox) => { + expect(checkbox).not.toBeChecked(); + }); + }); + + test("displays error message when touched and has error", () => { + render( + +
+ + +
+ ); + + expect(screen.getByText("This field is required")).toBeInTheDocument(); + }); + + test("does not display error message when not touched", () => { + render( + +
+ + +
+ ); + + expect( + screen.queryByText("This field is required") + ).not.toBeInTheDocument(); + }); + + test("parses checkbox values as integers", async () => { + // Using a mock to inspect the actual value set in Formik + const mockSetValue = jest.fn(); + + render( + + {() => { + // Override useField to spy on setValue + const originalUseField = require("formik").useField; + jest + .spyOn(require("formik"), "useField") + .mockImplementation((props) => { + const [field, meta, helpers] = originalUseField(props); + return [field, meta, { ...helpers, setValue: mockSetValue }]; + }); + + return ( +
+ + + ); + }} +
+ ); + + const checkboxes = screen.getAllByRole("checkbox"); + await userEvent.click(checkboxes[0]); + + // Check that setValue was called with the correct value (integer 1, not string '1') + expect(mockSetValue).toHaveBeenCalledWith([1]); + + // Restore the original implementation + require("formik").useField.mockRestore(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-datepicker.test.js b/src/components/mui/__tests__/mui-formik-datepicker.test.js new file mode 100644 index 00000000..f0734180 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-datepicker.test.js @@ -0,0 +1,63 @@ +/** + * 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 { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikDatepicker from "../formik-inputs/mui-formik-datepicker"; + +const renderWithFormik = (props, initialValues = { test_date: null }) => + render( + { + const errors = {}; + if (!values.test_date) { + errors.test_date = "This field is required"; + } + return errors; + }} + onSubmit={jest.fn()} + > +
+ + +
+ ); + +describe("MuiFormikDatepicker", () => { + test("shows required marker in label", () => { + renderWithFormik({ required: true }); + + expect(screen.getByLabelText("Test Date *")).toBeInTheDocument(); + }); + + test("shows validation error when blurring without value", async () => { + renderWithFormik({ required: true }); + + expect( + screen.queryByText("This field is required") + ).not.toBeInTheDocument(); + + const user = userEvent.setup(); + const input = screen.getByLabelText("Test Date *"); + await user.click(input); + await user.tab(); + + await waitFor(() => { + expect(screen.getByText("This field is required")).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-file-size-field.test.js b/src/components/mui/__tests__/mui-formik-file-size-field.test.js new file mode 100644 index 00000000..d6bbb884 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-file-size-field.test.js @@ -0,0 +1,236 @@ +/** + * 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 { render, screen, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikFilesizeField from "../formik-inputs/mui-formik-file-size-field"; +import { BYTES_PER_MB } from "../../../utils/constants"; + +const renderWithFormik = (props, initialValues = { max_file_size: 0 }) => + render( + +
+ + + +
+ ); + +describe("MuiFormikFilesizeField", () => { + describe("display and store", () => { + it("converts MB input to bytes", async () => { + const onSubmit = jest.fn(); + renderWithFormik({ + label: "Max File Size", + onSubmit + }); + + const field = screen.getByLabelText("Max File Size"); + const submitButton = screen.getByText("submit"); + + await act(async () => { + await userEvent.clear(field); + await userEvent.type(field, "10"); + await userEvent.click(submitButton); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + max_file_size: 10 * BYTES_PER_MB + }), + expect.anything() + ); + }); + + it("displays bytes as MB", async () => { + const onSubmit = jest.fn(); + renderWithFormik( + { + label: "Max File Size", + onSubmit + }, + { max_file_size: 15_728_640 } // 15 * 1_048_576 + ); + + const field = screen.getByLabelText("Max File Size"); + expect(field).toHaveValue(15); + }); + + it("shows 0 when initial value is 0", () => { + renderWithFormik( + { label: "Max File Size", onSubmit: jest.fn() }, + { max_file_size: 0 } + ); + + const field = screen.getByLabelText("Max File Size"); + expect(field).toHaveValue(0); + }); + }); + + describe("clearing behavior", () => { + it("shows empty field after clearing", async () => { + renderWithFormik( + { label: "Max File Size", onSubmit: jest.fn() }, + { max_file_size: 5 * BYTES_PER_MB } + ); + + const field = screen.getByLabelText("Max File Size"); + expect(field).toHaveValue(5); + + await act(async () => { + await userEvent.clear(field); + }); + + expect(field).toHaveValue(null); // empty number input + }); + + it("submits 0 after clearing when initial value was numeric", async () => { + const onSubmit = jest.fn(); + renderWithFormik( + { label: "Max File Size", onSubmit }, + { max_file_size: 5 * BYTES_PER_MB } + ); + + const field = screen.getByLabelText("Max File Size"); + const submitButton = screen.getByText("submit"); + + await act(async () => { + await userEvent.clear(field); + await userEvent.click(submitButton); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ max_file_size: 0 }), + expect.anything() + ); + }); + + it("submits null after clearing when initial value was null", async () => { + const onSubmit = jest.fn(); + renderWithFormik( + { label: "Max File Size", onSubmit }, + { max_file_size: null } + ); + + const field = screen.getByLabelText("Max File Size"); + const submitButton = screen.getByText("submit"); + + await act(async () => { + await userEvent.type(field, "10"); + await userEvent.clear(field); + await userEvent.click(submitButton); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ max_file_size: null }), + expect.anything() + ); + }); + + it("accepts new value after clearing", async () => { + const onSubmit = jest.fn(); + renderWithFormik( + { label: "Max File Size", onSubmit }, + { max_file_size: 5 * BYTES_PER_MB } + ); + + const field = screen.getByLabelText("Max File Size"); + const submitButton = screen.getByText("submit"); + + await act(async () => { + await userEvent.clear(field); + await userEvent.type(field, "20"); + await userEvent.click(submitButton); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ max_file_size: 20 * BYTES_PER_MB }), + expect.anything() + ); + }); + }); + + describe("zero as first character", () => { + it("blocks zero as first character", async () => { + renderWithFormik( + { label: "Max File Size", onSubmit: jest.fn() }, + { max_file_size: null } + ); + + const field = screen.getByLabelText("Max File Size"); + + await act(async () => { + await userEvent.type(field, "0"); + }); + + // zero is not a valid first character, field stays empty + expect(field).toHaveValue(null); + }); + + it("blocks leading zero followed by digits", async () => { + renderWithFormik( + { label: "Max File Size", onSubmit: jest.fn() }, + { max_file_size: null } + ); + + const field = screen.getByLabelText("Max File Size"); + + await act(async () => { + await userEvent.type(field, "099"); + }); + + // leading zero blocked, only "99" accepted + expect(field).toHaveValue(99); + }); + + it("allows zero in non-leading position", async () => { + renderWithFormik( + { label: "Max File Size", onSubmit: jest.fn() }, + { max_file_size: null } + ); + + const field = screen.getByLabelText("Max File Size"); + + await act(async () => { + await userEvent.type(field, "10"); + }); + + expect(field).toHaveValue(10); + }); + }); + + describe("blocked keys", () => { + it.each(["e", "E", "+", "-", ".", ","])( + "blocks '%s' key from being entered", + async (key) => { + renderWithFormik( + { label: "Max File Size", onSubmit: jest.fn() }, + { max_file_size: 0 } + ); + + const field = screen.getByLabelText("Max File Size"); + + await act(async () => { + await userEvent.clear(field); + await userEvent.type(field, key); + }); + + // field should remain empty since the key was blocked + expect(field).toHaveValue(null); + } + ); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-quantity-field.test.js b/src/components/mui/__tests__/mui-formik-quantity-field.test.js new file mode 100644 index 00000000..257e1820 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-quantity-field.test.js @@ -0,0 +1,81 @@ +/** + * 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 { render, screen, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikQuantityField from "../formik-inputs/mui-formik-quantity-field"; + +const renderWithFormik = (props, initialValues = { testField: [] }) => + render( + +
+ + + +
+ ); + +describe("MuiFormikQuantityField", () => { + it("must accept user input", async () => { + const onSubmit = jest.fn(); + + renderWithFormik({ + label: "some field", + onSubmit + }); + + const field = screen.getByLabelText("some field"); + expect(field).toBeInTheDocument(); + + const submitButton = screen.getByText("submit"); + await act(async () => { + await userEvent.type(field, "12345"); + await userEvent.click(submitButton); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + testField: 12345 + }), + expect.anything() + ); + }); + + it("must filter invalid characters", async () => { + const onSubmit = jest.fn(); + + renderWithFormik({ + label: "some field", + onSubmit + }); + + const field = screen.getByLabelText("some field"); + expect(field).toBeInTheDocument(); + + const submitButton = screen.getByText("submit"); + await act(async () => { + await userEvent.type(field, "123eEe45"); + await userEvent.click(submitButton); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + testField: 12345 + }), + expect.anything() + ); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-radio-group.test.js b/src/components/mui/__tests__/mui-formik-radio-group.test.js new file mode 100644 index 00000000..cf4f1065 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-radio-group.test.js @@ -0,0 +1,161 @@ +/** + * 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. + * */ + +// mui-formik-radio-group.test.js +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikRadioGroup from "../formik-inputs/mui-formik-radio-group"; + +// Helper function to render the component with Formik +const renderWithFormik = (props, initialValues = { testField: "" }) => render( + +
+ + +
+ ); + +describe("MuiFormikRadioGroup", () => { + const options = [ + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, + { value: "option3", label: "Option 3" } + ]; + + test("renders the component with label", () => { + renderWithFormik({ label: "Test Radio Group", options }); + + expect(screen.getByText("Test Radio Group")).toBeInTheDocument(); + expect(screen.getByText("Option 1")).toBeInTheDocument(); + expect(screen.getByText("Option 2")).toBeInTheDocument(); + expect(screen.getByText("Option 3")).toBeInTheDocument(); + }); + + test("renders all radio buttons with none selected when no default value", () => { + renderWithFormik({ options }); + + const radioButtons = screen.getAllByRole("radio"); + expect(radioButtons).toHaveLength(3); + radioButtons.forEach((radio) => { + expect(radio).not.toBeChecked(); + }); + }); + + test("renders with pre-selected value", () => { + renderWithFormik({ options }, { testField: "option2" }); + + const radioButtons = screen.getAllByRole("radio"); + expect(radioButtons[0]).not.toBeChecked(); // Option 1 + expect(radioButtons[1]).toBeChecked(); // Option 2 + expect(radioButtons[2]).not.toBeChecked(); // Option 3 + }); + + test("handles selecting a radio button", async () => { + renderWithFormik({ options }); + + const radioButtons = screen.getAllByRole("radio"); + await userEvent.click(radioButtons[0]); + + // After clicking, the first radio should be checked and others should be unchecked + expect(radioButtons[0]).toBeChecked(); + expect(radioButtons[1]).not.toBeChecked(); + expect(radioButtons[2]).not.toBeChecked(); + }); + + test("handles changing selection", async () => { + renderWithFormik({ options }, { testField: "option1" }); + + const radioButtons = screen.getAllByRole("radio"); + // Initial state + expect(radioButtons[0]).toBeChecked(); + expect(radioButtons[1]).not.toBeChecked(); + expect(radioButtons[2]).not.toBeChecked(); + + // Change selection to option2 + await userEvent.click(radioButtons[1]); + + // After clicking, only the second radio should be checked + expect(radioButtons[0]).not.toBeChecked(); + expect(radioButtons[1]).toBeChecked(); + expect(radioButtons[2]).not.toBeChecked(); + }); + + test("displays error message when touched and has error", () => { + render( + +
+ + +
+ ); + + expect(screen.getByText("This field is required")).toBeInTheDocument(); + }); + + test("does not display error message when not touched", () => { + render( + +
+ + +
+ ); + + expect( + screen.queryByText("This field is required") + ).not.toBeInTheDocument(); + }); + + test("passes additional props to RadioGroup", () => { + renderWithFormik({ options, row: true }); + + // The RadioGroup should have the row prop + const radioGroup = screen.getByRole("radiogroup"); + expect(radioGroup).toHaveClass("MuiFormGroup-row"); + }); + + test("generates correct key for each radio option", () => { + renderWithFormik({ options }); + + // Check that the keys are generated correctly by inspecting the rendered DOM + const radioLabels = screen.getAllByRole("radio"); + expect(radioLabels).toHaveLength(3); + + const radioButtons = screen.getAllByRole("radio"); + // Check that the values match what we expect + expect(radioButtons[0]).toHaveAttribute("value", "option1"); + expect(radioButtons[1]).toHaveAttribute("value", "option2"); + expect(radioButtons[2]).toHaveAttribute("value", "option3"); + }); + + test("defaults to empty when no label is provided", () => { + renderWithFormik({ options }); + + // There should be no FormLabel element + const formLabels = document.querySelectorAll(".MuiFormLabel-root"); + expect(formLabels.length).toBe(0); + }); +}); diff --git a/src/components/mui/__tests__/mui-sponsor-input.test.js b/src/components/mui/__tests__/mui-sponsor-input.test.js new file mode 100644 index 00000000..11869edc --- /dev/null +++ b/src/components/mui/__tests__/mui-sponsor-input.test.js @@ -0,0 +1,298 @@ +/** + * 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. + * */ + +// mui-sponsor-input.test.js +import React from "react"; +import { render, screen, waitFor, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiSponsorInput from "../formik-inputs/mui-sponsor-input"; + +// Mock the query actions +jest.mock("../../../utils/query-actions", () => ({ + querySponsorsV2: jest.fn((query, summitId, callback) => { + // Simulate API response based on query + const mockResults = [ + { id: 1, name: "Sponsor One" }, + { id: 2, name: "Sponsor Two" }, + { id: 3, name: "Another Sponsor" } + ].filter((s) => s.name.toLowerCase().includes(query.toLowerCase())); + + // Simulate async response + setTimeout(() => callback(mockResults), 100); + return Promise.resolve(); + }) +})); + +// Helper function to render the component with Formik +const renderWithFormik = (props, initialValues = { sponsor: "" }) => + render( + +
+ + +
+ ); + +describe("MuiSponsorInput", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("renders the component with placeholder", () => { + renderWithFormik(); + + // Placeholder should be visible + expect( + screen.getByPlaceholderText("Search sponsors...") + ).toBeInTheDocument(); + }); + + test("opens dropdown and fetches options when typing", async () => { + const { querySponsorsV2 } = require("../../../utils/query-actions"); + + // Create a controlled resolution for the mock + let resolveAPICall; + const apiPromise = new Promise((resolve) => { + resolveAPICall = resolve; + }); + + // Mock with a controlled promise + querySponsorsV2.mockImplementation((query, summitId, callback) => { + // Call the callback after our test code triggers resolution + apiPromise.then(() => { + callback([ + { id: 1, name: "Sponsor One" }, + { id: 2, name: "Sponsor Two" } + ]); + }); + return Promise.resolve(); + }); + + renderWithFormik(); + + // Type in the search field + const input = screen.getByPlaceholderText("Search sponsors..."); + await userEvent.click(input); + await userEvent.type(input, "Sponsor"); + + // Wait for the API call to be initiated + await waitFor( + () => { + expect(querySponsorsV2).toHaveBeenCalledWith( + "Sponsor", + 123, + expect.any(Function) + ); + }, + { timeout: 2000 } + ); // Increase timeout to account for debounce + + // Now we can check for loading state + // If the component shows "Loading..." text while fetching + expect(screen.getByText(/Loading/i)).toBeInTheDocument(); + + // Resolve the API call at a controlled time + resolveAPICall(); + + // Now wait for the options to appear with a longer timeout + await waitFor( + () => { + // Use a regex to match option text with case insensitivity + const options = screen.getAllByText(/sponsor (one|two)/i); + expect(options.length).toBeGreaterThan(0); + }, + { timeout: 3000 } + ); + + // Verify loading indicator is gone + expect(screen.queryByText(/Loading/i)).not.toBeInTheDocument(); + }); + + test("selects a sponsor in single selection mode", async () => { + renderWithFormik(); + + // Type and wait for options + const input = screen.getByPlaceholderText("Search sponsors..."); + await userEvent.click(input); + await userEvent.type(input, "Sponsor"); + + // Wait for options to appear + await waitFor( + () => { + const options = screen.getAllByText(/Sponsor (One|Two)/); + expect(options.length).toBeGreaterThan(0); + }, + { timeout: 1000 } + ); + + // Find and click the first option that matches "Sponsor One" + const option = screen.getAllByText(/Sponsor One/)[0]; + await userEvent.click(option); + + // Check the input value - this might need to be adjusted based on how selection works + await waitFor(() => { + expect(input.value).toBe("Sponsor One"); + }); + }); + + test("supports multiple selection when isMulti is true", async () => { + renderWithFormik({ isMulti: true }, { sponsor: [] }); + + // Select first option + const input = screen.getByPlaceholderText("Search sponsors..."); + await userEvent.click(input); + await userEvent.type(input, "Sponsor"); + + // Wait for options to appear + await waitFor( + () => { + const options = screen.getAllByText(/Sponsor (One|Two)/); + expect(options.length).toBeGreaterThan(0); + }, + { timeout: 1000 } + ); + + // Find and click the option + const option1 = screen.getAllByText(/Sponsor One/)[0]; + await userEvent.click(option1); + + // Clear input and search for second option + await userEvent.clear(input); + await userEvent.type(input, "Two"); + + // Wait for options to appear + await waitFor( + () => { + const options = screen.getAllByText(/Sponsor Two/); + expect(options.length).toBeGreaterThan(0); + }, + { timeout: 1000 } + ); + + // Select second option + const option2 = screen.getAllByText(/Sponsor Two/)[0]; + await userEvent.click(option2); + + // Verify both options are selected (may need adjustment based on component implementation) + expect(screen.getByText(/Sponsor One/)).toBeInTheDocument(); + expect(screen.getByText(/Sponsor Two/)).toBeInTheDocument(); + }); + + test("handles plain value format correctly", async () => { + renderWithFormik({ plainValue: true }); + + // Type and select an option + const input = screen.getByPlaceholderText("Search sponsors..."); + await userEvent.click(input); + await userEvent.type(input, "Sponsor"); + + // Wait for options to load + await waitFor(() => { + expect(screen.getByText("Sponsor One")).toBeInTheDocument(); + }); + + // Select the first option + await userEvent.click(screen.getByText("Sponsor One")); + + // Check that the value is set correctly (this would need to inspect Formik's state) + // We can't easily test this directly, but we can confirm the displayed value + expect(input.value).toBe("Sponsor One"); + }); + + test("displays error message when field has error", () => { + render( + +
+ + +
+ ); + + // Error message should be displayed + expect(screen.getByText("Sponsor is required")).toBeInTheDocument(); + }); + + test("initializes with preselected value in single selection mode", () => { + renderWithFormik( + { plainValue: false }, + { sponsor: { id: 1, name: "Sponsor One" } } + ); + + // The selected value should be displayed + expect(screen.getByDisplayValue("Sponsor One")).toBeInTheDocument(); + }); + + test("initializes with preselected values in multi selection mode", () => { + renderWithFormik( + { isMulti: true, plainValue: false }, + { + sponsor: [ + { id: 1, name: "Sponsor One" }, + { id: 2, name: "Sponsor Two" } + ] + } + ); + + // Both selected values should be displayed as chips + expect(screen.getByText("Sponsor One")).toBeInTheDocument(); + expect(screen.getByText("Sponsor Two")).toBeInTheDocument(); + }); + + test("debounces API calls", async () => { + jest.useFakeTimers(); + const user = userEvent.setup({ + advanceTimers: jest.advanceTimersByTime + }); + const { querySponsorsV2 } = require("../../../utils/query-actions"); + renderWithFormik(); + + const input = screen.getByPlaceholderText("Search sponsors..."); + await user.click(input); + + // Type characters rapidly + await user.type(input, "Spo"); + + // Debounce hasn't fired yet — timer is still pending + expect(querySponsorsV2).toHaveBeenCalledTimes(0); + + // Advance past the 250ms debounce + await act(async () => { + jest.advanceTimersByTime(300); + }); + + expect(querySponsorsV2).toHaveBeenCalledTimes(1); + expect(querySponsorsV2).toHaveBeenCalledWith( + "Spo", + 123, + expect.any(Function) + ); + + jest.useRealTimers(); + }); +}); diff --git a/src/components/mui/__tests__/mui-table-editable.test.js b/src/components/mui/__tests__/mui-table-editable.test.js new file mode 100644 index 00000000..87da923c --- /dev/null +++ b/src/components/mui/__tests__/mui-table-editable.test.js @@ -0,0 +1,380 @@ +/** + * 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. + * */ + +// ---- Mocks must come first ---- + +// i18n translate: echo the key +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +// Confirm dialog (exported mock we can control) +jest.mock("../showConfirmDialog", () => { + const mockShowConfirmDialog = jest.fn(); + return { __esModule: true, default: mockShowConfirmDialog }; +}); + +// Avoid MUI ripple noise +jest.mock("@mui/material/IconButton", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ children, onClick, ...rest }) => ( + + ) + }; +}); +jest.mock("@mui/material/ButtonBase/TouchRipple", () => ({ + __esModule: true, + default: () => null +})); + +// TableCell shim to inspect sx prop in tests +jest.mock("@mui/material/TableCell", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ children, sx, ...rest }) => ( + + {children} + + ) + }; +}); + +// TablePagination shim +jest.mock("@mui/material/TablePagination", () => { + const React = require("react"); + return { + __esModule: true, + default: function TablePaginationMock(props) { + const { + count, + rowsPerPage, + page, + rowsPerPageOptions, + onPageChange, + onRowsPerPageChange, + labelRowsPerPage + } = props; + + return ( +
+
count:{count}
+
rowsPerPage:{rowsPerPage}
+
page:{page}
+
label:{labelRowsPerPage}
+
+ options:{rowsPerPageOptions && rowsPerPageOptions.join(",")} +
+ + +
+ ); + } + }; +}); + +// TableSortLabel shim -> renders an actual + ); + } + }; +}); + +// ---- Now imports ---- +/* eslint-disable import/first */ +import React from "react"; +import "@testing-library/jest-dom"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import MuiTableEditable from "../editable-table/mui-table-editable"; +import showConfirmDialog from "../showConfirmDialog"; +/* eslint-enable import/first */ + +afterEach(() => { + jest.clearAllMocks(); +}); + +// ---- Helpers ---- +const columns = [ + { columnKey: "name", header: "Name", sortable: true, editable: true }, + { columnKey: "role", header: "Role", sortable: false, editable: false }, + { + columnKey: "age", + header: "Age", + sortable: true, + editable: true, + width: 100 + } +]; + +const data = [ + { id: 1, name: "Alice", role: "Dev", age: 35 }, + { id: 2, name: "Bob", role: "PM", age: 41 } +]; + +const setup = (overrides = {}) => { + const props = { + columns, + data, + totalRows: 2, + perPage: 10, + currentPage: 1, + onPageChange: jest.fn(), + onPerPageChange: jest.fn(), + onSort: jest.fn(), + options: { sortCol: "name", sortDir: -1 }, + getName: (item) => item.name, + onEdit: jest.fn(), + onDelete: jest.fn(), + onCellChange: jest.fn(), + ...overrides + }; + render(); + return props; +}; + +const getCellSx = (cell) => { + const rawSx = cell.getAttribute("data-sx"); + if (!rawSx) return {}; + + try { + return JSON.parse(rawSx); + } catch { + return {}; + } +}; + +// ---- Tests ---- +describe("MuiTableEditable", () => { + test("renders headers and rows", () => { + setup(); + expect(screen.getByText("Name")).toBeInTheDocument(); + expect(screen.getByText("Role")).toBeInTheDocument(); + expect(screen.getByText("Age")).toBeInTheDocument(); + expect(screen.getByText("Alice")).toBeInTheDocument(); + expect(screen.getByText("Bob")).toBeInTheDocument(); + }); + + test("click editable cell -> edit, blur -> onCellChange", async () => { + const user = userEvent.setup(); + const { onCellChange } = setup(); + + const aliceNameCell = screen.getByText("Alice").closest("td"); + await user.click(aliceNameCell); + + const input = screen.getByDisplayValue("Alice"); + await user.clear(input); + await user.type(input, "Alicia"); + input.blur(); + + expect(onCellChange).toHaveBeenCalledWith(1, "name", "Alicia"); + }); + + test("press Enter commits edit", async () => { + const user = userEvent.setup(); + const { onCellChange } = setup(); + + const bobAgeCell = screen.getByText("41").closest("td"); + await user.click(bobAgeCell); + const input = screen.getByDisplayValue("41"); + await user.clear(input); + await user.type(input, "42{enter}"); + + expect(onCellChange).toHaveBeenCalledWith(2, "age", "42"); + }); + + test("non-editable cell does not enter edit mode", async () => { + const user = userEvent.setup(); + setup(); + const roleCell = screen.getAllByText("Dev")[0].closest("td"); + await user.click(roleCell); + expect(screen.queryByRole("textbox")).not.toBeInTheDocument(); + }); + + test("edit button calls onEdit", async () => { + const user = userEvent.setup(); + const { onEdit } = setup(); + const [btn] = screen.getAllByLabelText("general.edit"); + await user.click(btn); + expect(onEdit).toHaveBeenCalledWith( + expect.objectContaining({ id: 1, name: "Alice" }) + ); + }); + + test("delete confirmed calls onDelete", async () => { + const user = userEvent.setup(); + const { onDelete } = setup(); + showConfirmDialog.mockResolvedValueOnce(true); // ✅ use the exported mock + const [btn] = screen.getAllByLabelText("general.delete"); + await user.click(btn); + expect(showConfirmDialog).toHaveBeenCalled(); + expect(onDelete).toHaveBeenCalledWith(1); + }); + + test("delete canceled does not call onDelete", async () => { + const user = userEvent.setup(); + const { onDelete } = setup(); + showConfirmDialog.mockResolvedValueOnce(false); + const [btn] = screen.getAllByLabelText("general.delete"); + await user.click(btn); + expect(showConfirmDialog).toHaveBeenCalled(); + expect(onDelete).not.toHaveBeenCalled(); + }); + + test("pagination next -> onPageChange(2) when starting at page 1", async () => { + const user = userEvent.setup(); + const { onPageChange } = setup({ currentPage: 1 }); + const next = within(screen.getByTestId("pagination")).getByRole("button", { + name: "next-page" + }); + await user.click(next); + expect(onPageChange).toHaveBeenCalledWith(2); + }); + + test("change rows per page triggers onPerPageChange", async () => { + const user = userEvent.setup(); + const { onPerPageChange } = setup({ perPage: 25 }); + const change = within(screen.getByTestId("pagination")).getByRole( + "button", + { name: "change-rows" } + ); + await user.click(change); + expect(onPerPageChange).toHaveBeenCalledWith(expect.any(Number)); + }); + + test("uses totalRows when provided", () => { + setup({ totalRows: 123 }); + expect( + within(screen.getByTestId("pagination")).getByText("count:123") + ).toBeInTheDocument(); + }); + + test("falls back to data.length when totalRows missing", () => { + setup({ totalRows: undefined, data: [{ id: 1 }, { id: 2 }, { id: 3 }] }); + expect( + within(screen.getByTestId("pagination")).getByText("count:3") + ).toBeInTheDocument(); + }); + + test("sort click triggers onSort with flipped dir", async () => { + const user = userEvent.setup(); + const { onSort } = setup({ options: { sortCol: "name", sortDir: -1 } }); + + // 1) Try our mock's testid first (desc + active when sortDir === "-1") + let sortBtn = screen.queryByTestId("sort-label-desc-active"); + + // 2) Fallback: any sort-label button whose text includes "Name" + if (!sortBtn) { + const candidates = screen.queryAllByTestId(/sort-label-/i); + sortBtn = + candidates.find( + (el) => el.textContent && el.textContent.trim().includes("Name") + ) || null; + } + + // 3) Last resort: just click the element that renders "Name" + // (in our mock, the button itself contains the text "Name") + if (!sortBtn) { + sortBtn = screen.getByText(/^Name$/); + } + + await user.click(sortBtn); + + expect(onSort).toHaveBeenCalled(); + const [colKey, newDir] = onSort.mock.calls[0]; + expect(colKey).toBe("name"); + expect(newDir).toBe(1); + }); + + test("applies archived styles to content, edit and delete cells when disableProp matches", () => { + setup({ + options: { sortCol: "name", sortDir: -1, disableProp: "is_archived" }, + data: [ + { id: 1, name: "Alice", role: "Dev", age: 35, is_archived: true }, + { id: 2, name: "Bob", role: "PM", age: 41, is_archived: false } + ], + onArchive: jest.fn() + }); + + const aliceRow = screen.getByText("Alice").closest("tr"); + const cells = within(aliceRow).getAllByTestId("mui-table-cell"); + + const archivedCellIndexes = [0, 1, 2, 3, 5]; + archivedCellIndexes.forEach((index) => { + const sx = getCellSx(cells[index]); + expect(sx.backgroundColor).toBe("background.light"); + expect(sx.color).toBe("text.disabled"); + }); + }); + + test("does not apply archived styles to archive/unarchive action cell", () => { + setup({ + options: { sortCol: "name", sortDir: -1, disableProp: "is_archived" }, + data: [{ id: 1, name: "Alice", role: "Dev", age: 35, is_archived: true }], + onArchive: jest.fn() + }); + + const unarchiveButton = screen.getByRole("button", { + name: "general.unarchive" + }); + const actionCell = unarchiveButton.closest("td"); + const sx = getCellSx(actionCell); + + expect(sx.width).toBe(80); + expect(sx.backgroundColor).toBeUndefined(); + expect(sx.color).toBeUndefined(); + }); +}); diff --git a/src/components/mui/checkbox-list.js b/src/components/mui/checkbox-list.js new file mode 100644 index 00000000..27aa6a25 --- /dev/null +++ b/src/components/mui/checkbox-list.js @@ -0,0 +1,106 @@ +/** + * 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, { useState } from "react"; +import { + Checkbox, + FormControlLabel, + FormGroup, + Box, + Divider +} from "@mui/material"; + +const CheckBoxList = ({ + items = [], + onChange, + loadMoreData, + boxHeight = "400px", + allItemsLabel = "Select All", + noItemsLabel = "No items found" +}) => { + const [selectedItems, setSelectedItems] = useState([]); + const [isAllSelected, setIsAllSelected] = useState(false); + const allItemIds = items.map((item) => item.id); + + const handleScroll = (event) => { + const { scrollTop, scrollHeight, clientHeight } = event.target; + // eslint-disable-next-line no-magic-numbers + if (scrollTop + clientHeight >= scrollHeight - 20 && loadMoreData) { + loadMoreData(); + } + }; + + const handleItemChange = (itemId) => { + let selected = []; + if (isAllSelected) { + selected = allItemIds.filter((id) => id !== itemId); + } else if (selectedItems.includes(itemId)) { + selected = selectedItems.filter((id) => id !== itemId); + } else { + selected = [...selectedItems, itemId]; + } + + setSelectedItems(selected); + // if user selects an item, then allSelected should be unchecked + setIsAllSelected(false); + onChange(selected); + }; + + const handleAllChange = () => { + // if user selects all, we should remove all other selections + setSelectedItems([]); + setIsAllSelected(!isAllSelected); + onChange([], !isAllSelected); + }; + + return ( + + {items.length === 0 ? ( +

{noItemsLabel}

+ ) : ( + + + } + label={allItemsLabel} + /> + + {items.map((item) => ( + handleItemChange(item.id)} + /> + } + label={item.name} + /> + ))} + + )} +
+ ); +}; + +export default CheckBoxList; diff --git a/src/components/mui/chip-list.js b/src/components/mui/chip-list.js new file mode 100644 index 00000000..de1c7b74 --- /dev/null +++ b/src/components/mui/chip-list.js @@ -0,0 +1,42 @@ +/** + * 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 { Box, Chip, Tooltip, Typography } from "@mui/material"; + +const ChipList = ({ chips, maxLength }) => { + const shownItems = chips.slice(0, maxLength); + const rest = chips.slice(maxLength); + + return ( + + {shownItems.map((chip) => ( + + ))} + {rest.length > 0 && ( + ( + + {r} + + ))} + arrow + > + + + )} + + ); +}; + +export default ChipList; diff --git a/src/components/mui/chip-notify.js b/src/components/mui/chip-notify.js new file mode 100644 index 00000000..e6b5a3cf --- /dev/null +++ b/src/components/mui/chip-notify.js @@ -0,0 +1,28 @@ +/** + * 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 { Chip } from "@mui/material"; +import NotificationsIcon from "@mui/icons-material/Notifications"; + +const ChipNotify = ({ label, color = "warning", Icon = NotificationsIcon, ...props }) => ( + } + color={color} + label={label.toUpperCase()} + variant="outlined" + {...props} + /> +); + +export default ChipNotify; diff --git a/src/components/mui/chip-select-input.js b/src/components/mui/chip-select-input.js new file mode 100644 index 00000000..a132922b --- /dev/null +++ b/src/components/mui/chip-select-input.js @@ -0,0 +1,148 @@ +/** + * 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 { + Box, + Chip, + FormControl, + IconButton, + InputAdornment, + InputLabel, + MenuItem, + OutlinedInput, + Select +} from "@mui/material"; +import { ClearIcon } from "@mui/x-date-pickers"; + +const ChipSelectInput = ({ + availableOptions = [], + canAdd = false, + canEdit = false, + inputLabel = "", + currentSettings = null, + onGetSettingsMeta, + onGetSettings, + onUpsertSettings, + renderSelectedOptions, + denormalizeSettings +}) => { + const [selectedColumns, setSelectedColumns] = useState([]); + const [isDirty, setIsDirty] = useState(false); + + // get current selected options + useEffect(() => { + if (onGetSettings) { + onGetSettings(); + } + }, [onGetSettings]); + + useEffect(() => { + if (currentSettings) { + if (onGetSettingsMeta) { + onGetSettingsMeta(); + } + + if (currentSettings && renderSelectedOptions && denormalizeSettings) { + const selectedColumnsTmp = renderSelectedOptions( + denormalizeSettings(currentSettings.columns) + ).map((c) => c.value); + setSelectedColumns(selectedColumnsTmp); + } + } + }, [currentSettings]); + + const submitNewColumns = (newValue) => { + setSelectedColumns(newValue); + onUpsertSettings(newValue); + setIsDirty(false); + }; + + const handleColumnChange = (value) => { + setSelectedColumns(value); + setIsDirty(true); + }; + + const handleRemoveItem = (value) => { + const newValues = selectedColumns.filter((c) => c !== value); + setSelectedColumns(newValues); + onUpsertSettings(newValues); + }; + + if (!canAdd || !canEdit || availableOptions.length === 0) { + return null; + } + + return ( + + + {inputLabel} + + + + ); +}; + +export default ChipSelectInput; diff --git a/src/components/mui/confirm-dialog.js b/src/components/mui/confirm-dialog.js new file mode 100644 index 00000000..23b81ecc --- /dev/null +++ b/src/components/mui/confirm-dialog.js @@ -0,0 +1,105 @@ +/** + * 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 { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Typography +} from "@mui/material"; +import { CheckCircle, Error, Info, Warning } from "@mui/icons-material"; + +const iconMap = { + warning: , + success: , + error: , + info: +}; + +const ConfirmDialog = ({ + open, + title, + text, + iconType = "", + onConfirm, + onCancel, + confirmButtonText = "Confirm", + confirmButtonColor = "primary", + cancelButtonText = "Cancel", + cancelButtonColor = "primary" +}) => ( + + + {title} + + + +
+ {iconMap[iconType] && ( +
{iconMap[iconType]}
+ )} + {text} +
+
+ + + + + +
+); + +ConfirmDialog.propTypes = { + open: PropTypes.bool, + title: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + iconType: PropTypes.string, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + confirmButtonText: PropTypes.string, + confirmButtonColor: PropTypes.string, + cancelButtonText: PropTypes.string, + cancelButtonColor: PropTypes.string +}; + +ConfirmDialog.defaultProps = { + open: false, + iconType: "warning", + confirmButtonText: "Confirm", + confirmButtonColor: "primary", + cancelButtonText: "Cancel", + cancelButtonColor: "default" +}; + +export default ConfirmDialog; diff --git a/src/components/mui/custom-alert.js b/src/components/mui/custom-alert.js new file mode 100644 index 00000000..56a841f9 --- /dev/null +++ b/src/components/mui/custom-alert.js @@ -0,0 +1,31 @@ +/** + * 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 { Alert } from "@mui/material"; + +const CustomAlert = ({ severity = "info", message = "", hideIcon = false }) => ( + + {message} + +); + +export default CustomAlert; diff --git a/src/components/mui/dnd-list.js b/src/components/mui/dnd-list.js new file mode 100644 index 00000000..18939b38 --- /dev/null +++ b/src/components/mui/dnd-list.js @@ -0,0 +1,102 @@ +/** + * 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 { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import { Box } from "@mui/material"; + +const reorder = (list, startIndex, endIndex, updateOrderKey) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + return result.map((item, index) => ({ + ...item, + [updateOrderKey]: index + 1 + })); +}; + +const DragAndDropList = ({ + items, + onReorder, + renderItem, + idKey, + updateOrderKey, + droppableId +}) => { + const handleDragEnd = (result) => { + if (!result.destination) return; + + const newItems = reorder( + items, + result.source.index, + result.destination.index, + updateOrderKey + ); + onReorder(newItems, result); + }; + + return ( + + + {(provided) => ( + + {items.map((item, index) => ( + + {(provided, snapshot) => ( + + {renderItem(item, index, provided, snapshot)} + + )} + + ))} + {provided.placeholder} + + )} + + + ); +}; + +DragAndDropList.propTypes = { + items: PropTypes.array.isRequired, + onReorder: PropTypes.func.isRequired, + renderItem: PropTypes.func.isRequired, + idKey: PropTypes.string, + updateOrderKey: PropTypes.string, + droppableId: PropTypes.string +}; + +DragAndDropList.defaultProps = { + idKey: "id", + updateOrderKey: "order", + droppableId: "droppable" +}; + +export default DragAndDropList; diff --git a/src/components/mui/dropdown-checkbox.js b/src/components/mui/dropdown-checkbox.js new file mode 100644 index 00000000..dc99c56b --- /dev/null +++ b/src/components/mui/dropdown-checkbox.js @@ -0,0 +1,89 @@ +/** + * 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 { + Checkbox, + Divider, + FormControl, + InputLabel, + ListItemText, + MenuItem, + OutlinedInput, + Select +} from "@mui/material"; + +const DropdownCheckbox = ({ + name, + label, + allLabel, + value = [], + options, + onChange +}) => { + const handleChange = (ev) => { + const selected = ev.target.value; + + if (selected.includes("all")) { + if (!value.includes("all")) { + // if all changed from unselected to selected we remove the rest of selections + onChange({ target: { name, value: ["all"] } }); + } else if (selected.length > 1) { + // if all was selected and now select an item, we remove "all" from selections + onChange({ + target: { name, value: selected.filter((v) => v !== "all") } + }); + } + } else { + // else if "all" is not selected we just send selection + onChange({ target: { name, value: selected } }); + } + }; + + return ( + + {label} + + + ); +}; + +export default DropdownCheckbox; diff --git a/src/components/mui/editable-table/mui-table-editable.js b/src/components/mui/editable-table/mui-table-editable.js new file mode 100644 index 00000000..9a0cf552 --- /dev/null +++ b/src/components/mui/editable-table/mui-table-editable.js @@ -0,0 +1,391 @@ +/** + * 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 * as React from "react"; +import T from "i18n-react/dist/i18n-react"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TablePagination from "@mui/material/TablePagination"; +import TableSortLabel from "@mui/material/TableSortLabel"; +import TableRow from "@mui/material/TableRow"; +import Paper from "@mui/material/Paper"; +import { IconButton, TextField } from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { visuallyHidden } from "@mui/utils"; + +import { + DEFAULT_PER_PAGE, + FIFTY_PER_PAGE, + TWENTY_PER_PAGE +} from "../../../utils/constants"; +import showConfirmDialog from "../showConfirmDialog"; + +const ARCHIVED_CELL_SX = { + backgroundColor: "background.light", + color: "text.disabled" +}; + +const validateValue = (value, validation) => { + if (!validation) return { isValid: true }; + + // validate with yup schema + if ( + validation.schema && + typeof validation.schema.validateSync === "function" + ) { + try { + validation.schema.validateSync(value); + return { isValid: true, message: null }; + } catch (err) { + return { isValid: false, message: err.message }; + } + } + + return { isValid: true }; +}; + +// Updated component to handle editable cells with hover edit icon +const EditableCell = ({ value, isEditing, onBlur, validation }) => { + const [inputValue, setInputValue] = React.useState(value); + const [isHovering, setIsHovering] = React.useState(false); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + setInputValue(value); + setError(null); + }, [value]); + + const handleValidationAndSave = (newValue) => { + const { isValid, message } = validateValue(newValue, validation); + + if (isValid) { + setError(null); + onBlur(newValue, true); + } else { + setError(message); + } + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleValidationAndSave(inputValue); + } + }; + + if (isEditing) { + return ( + { + setInputValue(e.target.value); + if (error) setError(null); + }} + onBlur={() => { + handleValidationAndSave(inputValue); + }} + onKeyDown={handleKeyDown} + size="small" + fullWidth + variant="standard" + error={!!error} + helperText={error} + /> + ); + } + + return ( + setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > + {value} + {isHovering && ( + + )} + + ); +}; + +const MuiTableEditable = ({ + columns = [], + data = [], + totalRows, + perPage, + currentPage, + onPageChange, + onPerPageChange, + onSort, + options = { sortCol: "", sortDir: 1, disableProp: null }, + getName = (item) => item.name, + onEdit, + onArchive, + onDelete, + onCellChange, // New prop for handling cell value changes + deleteDialogBody +}) => { + // State to track which cell is currently being edited + const [editingCell, setEditingCell] = React.useState(null); + + const handleChangePage = (_, newPage) => { + onPageChange(newPage + 1); + }; + + const handleChangeRowsPerPage = (ev) => { + onPerPageChange(ev.target.value); + }; + + const basePerPageOptions = [ + DEFAULT_PER_PAGE, + TWENTY_PER_PAGE, + FIFTY_PER_PAGE + ]; + + const customPerPageOptions = basePerPageOptions.includes(perPage) + ? basePerPageOptions + : [...basePerPageOptions, perPage].sort((a, b) => a - b); + + const { sortCol, sortDir } = options; + + const getArchivedCellSx = (row) => + options.disableProp && row[options.disableProp] ? ARCHIVED_CELL_SX : null; + + const getCellSx = (row, baseSx = {}) => ({ + ...baseSx, + ...(getArchivedCellSx(row) || {}) + }); + + const handleDelete = async (item) => { + const isConfirmed = await showConfirmDialog({ + title: T.translate("general.are_you_sure"), + text: deleteDialogBody + ? deleteDialogBody(getName(item)) + : `${T.translate("general.row_remove_warning")} ${getName(item)}`, + type: "warning", + showCancelButton: true, + confirmButtonColor: "#DD6B55", + confirmButtonText: T.translate("general.yes_delete") + }); + + if (isConfirmed) { + onDelete(item.id); + } + }; + + const isEditable = (col, row) => + typeof col.editable === "function" ? col.editable(row) : !!col.editable; + + // Handler for starting edit mode on a cell + const handleCellClick = (row, columnKey) => { + // Check if the column is editable + const column = columns.find((col) => col.columnKey === columnKey); + if (column && isEditable(column, row)) { + setEditingCell({ rowId: row.id, columnKey }); + } + }; + + // Handler for saving changes when editing is complete + const handleCellBlur = (rowId, columnKey, newValue, isValid) => { + if (onCellChange && isValid) { + onCellChange(rowId, columnKey, newValue); + } + setEditingCell(null); + }; + + return ( + + + + + {/* TABLE HEADER */} + + + {columns.map((col) => ( + + {col.sortable ? ( + onSort(col.columnKey, sortDir * -1)} + > + {col.header} + {sortCol === col.columnKey ? ( + + {sortDir === -1 + ? T.translate("mui_table.sorted_desc") + : T.translate("mui_table.sorted_asc")} + + ) : null} + + ) : ( + col.header + )} + + ))} + {onEdit && } + {onArchive && } + {onDelete && } + + + {/* TABLE BODY */} + + {data.map((row) => ( + + {columns.map((col) => ( + handleCellClick(row, col.columnKey)} + sx={getCellSx(row, { + cursor: isEditable(col, row) ? "pointer" : "default", + padding: isEditable(col, row) ? "8px 16px" : undefined // Ensure enough space for the edit icon + })} + > + {isEditable(col, row) ? ( + + handleCellBlur( + row.id, + col.columnKey, + newValue, + isValid + ) + } + validation={col.validation} + /> + ) : col.render ? ( + col.render(row) + ) : ( + row[col.columnKey] + )} + + ))} + {onEdit && ( + + onEdit(row)} + size="small" + aria-label={T.translate("general.edit")} + > + + + + )} + {onArchive && ( + + + + )} + {onDelete && ( + + handleDelete(row)} + size="small" + aria-label={T.translate("general.delete")} + > + + + + )} + + ))} + +
+
+ +
+
+ ); +}; + +export default MuiTableEditable; diff --git a/src/components/mui/editable-table/styles.module.less b/src/components/mui/editable-table/styles.module.less new file mode 100644 index 00000000..d056f4b5 --- /dev/null +++ b/src/components/mui/editable-table/styles.module.less @@ -0,0 +1,14 @@ +.dottedBorderLeft { + position: relative; + border-left: none; + &::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + border-left: 1px dashed #e0e0e0; + height: auto; + margin: 20px 0; + } +} diff --git a/src/components/mui/formik-inputs/additional-input/additional-input-list.js b/src/components/mui/formik-inputs/additional-input/additional-input-list.js new file mode 100644 index 00000000..6450fdba --- /dev/null +++ b/src/components/mui/formik-inputs/additional-input/additional-input-list.js @@ -0,0 +1,122 @@ +/** + * 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 { useFormikContext, getIn } from "formik"; +import T from "i18n-react"; +import AdditionalInput from "./additional-input"; +import showConfirmDialog from "../../showConfirmDialog"; +import { METAFIELD_TYPES_WITH_OPTIONS } from "../../../../utils/constants"; + +const DEFAULT_META_FIELD = { + name: "", + type: "", + is_required: false, + minimum_quantity: 0, + maximum_quantity: 0, + values: [] +}; + +const AdditionalInputList = ({ name, onDelete, onDeleteValue, entityId }) => { + const { values, setFieldValue, errors } = useFormikContext(); + + const metaFields = values[name] || []; + + useEffect(() => { + if (metaFields.length === 0) { + setFieldValue(name, [ + { ...DEFAULT_META_FIELD, _key: `draft_${Date.now()}` } + ]); + } + }, [metaFields.length]); + + const handleAddItem = () => { + setFieldValue(name, [ + ...metaFields, + { ...DEFAULT_META_FIELD, _key: `draft_${Date.now()}` } + ]); + }; + + const handleRemove = async (item, index) => { + const isConfirmed = await showConfirmDialog({ + title: T.translate("general.are_you_sure"), + text: `${T.translate("additional_inputs.delete_warning")} ${ + item.name + }`, + type: "warning", + confirmButtonColor: "#DD6B55", + confirmButtonText: T.translate("general.yes_delete") + }); + + if (!isConfirmed) return; + + const removeFromUI = () => { + const newValues = metaFields.filter((_, idx) => idx !== index); + if (newValues.length === 0) { + newValues.push({ ...DEFAULT_META_FIELD, _key: `draft_${Date.now()}` }); + } + setFieldValue(name, newValues); + }; + + if (item.id && onDelete) { + onDelete(entityId, item.id) + .then(() => removeFromUI()) + .catch((err) => console.error("Error deleting field from API", err)); + } else { + removeFromUI(); + } + }; + + const areMetafieldsIncomplete = () => { + const fieldErrors = getIn(errors, name); + if (fieldErrors && Array.isArray(fieldErrors)) { + const hasRealErrors = fieldErrors.some( + (err) => err && Object.keys(err).length > 0 + ); + if (hasRealErrors) return true; + } + + return metaFields.some((field) => { + if (!field.name?.trim() || !field.type) return true; + if (METAFIELD_TYPES_WITH_OPTIONS.includes(field.type)) { + if (!field.values || field.values.length === 0) return true; + const hasIncompleteValues = field.values.some( + (v) => !v.name?.trim() || !v.value?.trim() + ); + if (hasIncompleteValues) return true; + } + + return false; + }); + }; + + return ( + <> + {metaFields.map((item, itemIdx) => ( + + ))} + + ); +}; + +export default AdditionalInputList; diff --git a/src/components/mui/formik-inputs/additional-input/additional-input.js b/src/components/mui/formik-inputs/additional-input/additional-input.js new file mode 100644 index 00000000..ed2ca8ef --- /dev/null +++ b/src/components/mui/formik-inputs/additional-input/additional-input.js @@ -0,0 +1,198 @@ +/** + * 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 { + Box, + Button, + Divider, + FormHelperText, + Grid2, + InputLabel, + MenuItem +} from "@mui/material"; +import { useFormikContext, getIn } from "formik"; +import T from "i18n-react/dist/i18n-react"; +import DeleteIcon from "@mui/icons-material/Delete"; +import AddIcon from "@mui/icons-material/Add"; +import MetaFieldValues from "./meta-field-values"; +import MuiFormikTextField from "../mui-formik-textfield"; +import MuiFormikSelect from "../mui-formik-select"; +import MuiFormikCheckbox from "../mui-formik-checkbox"; +import { + METAFIELD_TYPES, + METAFIELD_TYPES_WITH_OPTIONS +} from "../../../../utils/constants"; + +const AdditionalInput = ({ + item, + itemIdx, + baseName, + onAdd, + onDelete, + onDeleteValue, + entityId, + isAddDisabled +}) => { + const { errors, touched, values } = useFormikContext(); + + const buildFieldName = (fieldName) => `${baseName}[${itemIdx}].${fieldName}`; + const currentType = getIn(values, buildFieldName("type")); + + const fieldErrors = getIn(errors, `${baseName}[${itemIdx}]`); + const fieldTouched = getIn(touched, `${baseName}[${itemIdx}]`); + + const showValuesError = + fieldTouched?.values && + fieldErrors?.values && + typeof fieldErrors.values === "string"; + + return ( + + + + + + + {T.translate("additional_inputs.title")} + + + + + + {T.translate("additional_inputs.type")} + + + {METAFIELD_TYPES.map((fieldType) => ( + + {fieldType} + + ))} + + + + + + + {METAFIELD_TYPES_WITH_OPTIONS.includes(currentType) && ( + <> + + + {showValuesError && ( + + {fieldErrors.values} + + )} + + )} + {currentType === "Quantity" && ( + + + + + + + + + )} + + + + + + + + + + ); +}; + +export default AdditionalInput; diff --git a/src/components/mui/formik-inputs/additional-input/meta-field-values.js b/src/components/mui/formik-inputs/additional-input/meta-field-values.js new file mode 100644 index 00000000..de179930 --- /dev/null +++ b/src/components/mui/formik-inputs/additional-input/meta-field-values.js @@ -0,0 +1,193 @@ +/** + * 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 T from "i18n-react/dist/i18n-react"; +import { useFormikContext } from "formik"; +import { Box, Button, Grid2, Divider, IconButton } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import AddIcon from "@mui/icons-material/Add"; +import DragAndDropList from "../../dnd-list"; +import showConfirmDialog from "../../showConfirmDialog"; +import MuiFormikTextField from "../mui-formik-textfield"; +import MuiFormikCheckbox from "../mui-formik-checkbox"; + +const MetaFieldValues = ({ + field, + fieldIndex, + baseName = "meta_fields", + onMetaFieldTypeValueDeleted, + entityId +}) => { + const { values, setFieldValue } = useFormikContext(); + + const metaFields = values[baseName] || []; + const sortedValues = [...field.values].sort((a, b) => a.order - b.order); + + const buildValueFieldName = (valueIndex, fieldName) => + `${baseName}[${fieldIndex}].values[${valueIndex}].${fieldName}`; + + const onReorder = (newValues) => { + const newMetaFields = [...metaFields]; + newMetaFields[fieldIndex].values = newValues; + setFieldValue(baseName, newMetaFields); + }; + + const handleAddValue = () => { + const newFields = [...metaFields]; + newFields[fieldIndex].values.push({ + value: "", + name: "", + is_default: false + }); + setFieldValue(baseName, newFields); + }; + + const handleDefaultChange = (valueIndex, checked) => { + const newFields = [...metaFields]; + if (checked) { + newFields[fieldIndex].values.forEach((v) => { + v.is_default = false; + }); + } + newFields[fieldIndex].values[valueIndex].is_default = checked; + setFieldValue(baseName, newFields); + }; + + const isMetafieldValueIncomplete = () => { + if (field.values.length > 0) { + return field.values.some((v) => !v.name?.trim() || !v.value?.trim()); + } + return false; + }; + + const handleRemoveValue = async (metaFieldValue, valueIndex) => { + const isConfirmed = await showConfirmDialog({ + title: T.translate("general.are_you_sure"), + text: T.translate("meta_fields.delete_value_warning"), + type: "warning", + confirmButtonColor: "#DD6B55", + confirmButtonText: T.translate("general.yes_delete") + }); + + if (!isConfirmed) return; + + const removeValueFromFields = () => { + const newFields = [...metaFields]; + newFields[fieldIndex].values = newFields[fieldIndex].values.filter( + (_, index) => index !== valueIndex + ); + setFieldValue(baseName, newFields); + }; + + if (field.id && metaFieldValue.id && onMetaFieldTypeValueDeleted) { + onMetaFieldTypeValueDeleted(entityId, field.id, metaFieldValue.id).then( + () => removeValueFromFields() + ); + } else { + removeValueFromFields(); + } + }; + + const renderMetaFieldValue = (val, sortedIndex, provided, snapshot) => { + const originalIndex = field.values.findIndex( + (v) => (v.id && v.id === val.id) || v === val + ); + const valueIndex = originalIndex !== -1 ? originalIndex : sortedIndex; + + return ( + + + + + + + handleRemoveValue(val, valueIndex)} + aria-label="remove value" + > + + + ) + }} + /> + + + + handleDefaultChange(valueIndex, e.target.checked) + } + /> + + + + + ); + }; + + return ( + + + + + + + ); +}; + +export default MetaFieldValues; diff --git a/src/components/mui/formik-inputs/company-input-mui.js b/src/components/mui/formik-inputs/company-input-mui.js new file mode 100644 index 00000000..8edfeefb --- /dev/null +++ b/src/components/mui/formik-inputs/company-input-mui.js @@ -0,0 +1,206 @@ +/** + * 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, { useState, useEffect, useMemo } from "react"; +import PropTypes from "prop-types"; +import TextField from "@mui/material/TextField"; +import Autocomplete, { createFilterOptions } from "@mui/material/Autocomplete"; +import CircularProgress from "@mui/material/CircularProgress"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { useField } from "formik"; +import { queryCompanies } from "../../../utils/query-actions"; +import { DEBOUNCE_WAIT_250 } from "../../../utils/constants"; + +const filter = createFilterOptions(); + +const CompanyInputMUI = ({ + id, + name, + placeholder, + plainValue, + isMulti = false, + allowCreate = false, + ...rest +}) => { + const [field, meta, helpers] = useField(name); + const [options, setOptions] = useState([]); + const [open, setOpen] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [loading, setLoading] = useState(false); + const [isDebouncing, setIsDebouncing] = useState(false); + + const { value } = field; + const error = meta.touched && meta.error; + + const fetchOptions = async (input) => { + if (!input) { + setOptions([]); + return; + } + + setIsDebouncing(false); + setLoading(true); + + const normalize = (results) => + results.map((r) => ({ + value: r.id.toString(), + label: r.name + })); + + await queryCompanies(input, (results) => { + setOptions(normalize(results)); + setLoading(false); + }); + }; + + useEffect(() => { + if (inputValue) { + setIsDebouncing(true); + const delayDebounce = setTimeout(() => { + fetchOptions(inputValue); + }, DEBOUNCE_WAIT_250); + return () => clearTimeout(delayDebounce); + } + setIsDebouncing(false); + }, [inputValue]); + + const selectedValue = useMemo(() => { + if (!value) return isMulti ? [] : null; + + if (isMulti) { + return value.map((v) => + plainValue + ? { value: v, label: v } + : { value: v.id?.toString(), label: v.name } + ); + } + return plainValue + ? { value, label: value } + : { value: value.id?.toString(), label: value.name }; + }, [value, plainValue, isMulti]); + + const handleChange = (_, newValue) => { + let theValue; + + if (!newValue || (Array.isArray(newValue) && newValue.length === 0)) { + theValue = isMulti ? [] : plainValue ? "" : { id: "", name: "" }; + } else if (isMulti) { + theValue = plainValue + ? newValue.map((v) => v.label) + : newValue.map((v) => ({ + id: parseInt(v.value), + name: v.label + })); + } else { + theValue = plainValue + ? newValue.inputValue || newValue.label + : { + id: newValue.inputValue ? 0 : parseInt(newValue.value), + name: newValue.inputValue || newValue.label + }; + } + + helpers.setValue(theValue); + }; + + const handleFilterOptions = (options, params) => { + const filtered = filter(options, params); + + if (!allowCreate || loading || isDebouncing) return filtered; + + const { inputValue } = params; + const isExisting = options.some( + (option) => inputValue.toLowerCase() === option.label.toLowerCase() + ); + + if (inputValue !== "" && !isExisting) { + filtered.push({ + inputValue, + value: null, + label: `Create "${inputValue}"` + }); + } + return filtered; + }; + + const getOptionLabel = (option) => { + if (option.inputValue) { + return option.inputValue; + } + return option.label || ""; + }; + + return ( + setOpen(true)} + onClose={() => setOpen(false)} + options={options} + value={selectedValue} + getOptionLabel={getOptionLabel} + isOptionEqualToValue={(option, value) => option.value === value.value} + onInputChange={(_, newInputValue) => { + setInputValue(newInputValue); + }} + filterOptions={handleFilterOptions} + multiple={isMulti} + onChange={handleChange} + loading={loading} + fullWidth + popupIcon={} + renderOption={(props, option) => ( +
  • + {option.label} +
  • + )} + renderInput={(params) => ( + + {loading && } + {params.InputProps?.endAdornment} + + ) + }} + /> + )} + {...rest} + /> + ); +}; + +CompanyInputMUI.propTypes = { + id: PropTypes.string, + name: PropTypes.string.isRequired, + placeholder: PropTypes.string, + plainValue: PropTypes.bool, + isMulti: PropTypes.bool, + allowCreate: PropTypes.bool +}; + +export default CompanyInputMUI; diff --git a/src/components/mui/formik-inputs/item-price-tiers.js b/src/components/mui/formik-inputs/item-price-tiers.js new file mode 100644 index 00000000..16718d97 --- /dev/null +++ b/src/components/mui/formik-inputs/item-price-tiers.js @@ -0,0 +1,103 @@ +/** + * 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 T from "i18n-react"; +import { useFormikContext } from "formik"; +import { + Box, + Checkbox, + FormControlLabel, + Grid2, + InputLabel, + TextField +} from "@mui/material"; +import MuiFormikPriceField from "./mui-formik-pricefield"; +import { RATE_FIELDS } from "../../../utils/constants"; +import { isRateEnabled } from "../../../utils/methods"; + +const TIERS = [ + { field: RATE_FIELDS.EARLY_BIRD, label: "rates.early_bird_rate" }, + { field: RATE_FIELDS.STANDARD, label: "rates.standard_rate" }, + { field: RATE_FIELDS.ONSITE, label: "rates.onsite_rate" } +]; + +const ItemPriceTiers = ({ readOnly = false }) => { + const { values, setFieldValue } = useFormikContext(); + + const enabled = { + [RATE_FIELDS.EARLY_BIRD]: isRateEnabled(values[RATE_FIELDS.EARLY_BIRD]), + [RATE_FIELDS.STANDARD]: isRateEnabled(values[RATE_FIELDS.STANDARD]), + [RATE_FIELDS.ONSITE]: isRateEnabled(values[RATE_FIELDS.ONSITE]) + }; + + const handleToggle = (field, checked) => { + setFieldValue(field, checked ? null : 0); + }; + + return ( + + {TIERS.map(({ field, label }) => { + const isEnabled = enabled[field]; + return ( + + + {T.translate(label)} + handleToggle(field, ev.target.checked)} + size="small" + disabled={readOnly} + inputProps={{ + "aria-label": `${T.translate(label)} ${T.translate( + "general.not_available" + )}` + }} + /> + } + label={T.translate("general.not_available")} + /> + + + {isEnabled ? ( + + ) : ( + + )} + + + ); + })} + + ); +}; + +export default ItemPriceTiers; diff --git a/src/components/mui/formik-inputs/mui-formik-async-select.js b/src/components/mui/formik-inputs/mui-formik-async-select.js new file mode 100644 index 00000000..8f724fb6 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-async-select.js @@ -0,0 +1,140 @@ +/** + * 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, { useState, useEffect } from "react"; +import { + Autocomplete, + TextField, + Checkbox, + CircularProgress +} from "@mui/material"; +import { useField } from "formik"; +import { DEBOUNCE_WAIT_250 } from "../../../utils/constants"; + +const MuiFormikAsyncAutocomplete = ({ + name, + queryFunction, + multiple = false, + placeholder = "Select...", + plainValue = false, + hiddenOptions = [], + formatOption = (item) => ({ value: item.id.toString(), label: item.name }), + formatSelectedValue = null, + queryParams = [], + isMulti = false +}) => { + const [field, meta, helpers] = useField(name); + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + + const value = field.value || (multiple ? [] : null); + const error = meta.touched && meta.error; + + const fetchOptions = async (input = "") => { + setLoading(true); + try { + await queryFunction(input, ...queryParams, (results) => { + const normalized = results + .filter((r) => !hiddenOptions.includes(r.id)) + .map(formatOption); + setOptions(normalized); + setLoading(false); + }); + } catch (err) { + console.error("Error fetching options:", err); + setLoading(false); + } + }; + + useEffect(() => { + if (searchTerm) { + const delayDebounce = setTimeout(() => { + fetchOptions(searchTerm); + }, DEBOUNCE_WAIT_250); + return () => clearTimeout(delayDebounce); + } + }, [searchTerm]); + + // preload empty + useEffect(() => { + fetchOptions(""); + }, []); + + const handleChange = (event, selected) => { + if (!multiple) { + const selectedValue = plainValue ? selected?.value || "" : selected; + helpers.setValue(selectedValue); + return; + } + + const selectedItems = plainValue + ? selected.map((s) => s.value) + : selected.map((s) => + formatSelectedValue + ? formatSelectedValue(s) + : { id: parseInt(s.value), name: s.label } + ); + + helpers.setValue(selectedItems); + }; + + return ( + option.label || ""} + isOptionEqualToValue={(option, value) => option.value === value.value} + onInputChange={(e, newInput) => setSearchTerm(newInput)} + renderInput={(params) => ( + + {loading && } + {params.InputProps?.endAdornment} + + ) + }, + inputLabel: { shrink: false } + }} + sx={{ + "& input::placeholder": { + color: "#00000061", + opacity: 1 + } + }} + /> + )} + renderOption={(props, option, { selected }) => ( +
  • + {multiple && } + {option.label} +
  • + )} + /> + ); +}; + +export default MuiFormikAsyncAutocomplete; diff --git a/src/components/mui/formik-inputs/mui-formik-checkbox-group.js b/src/components/mui/formik-inputs/mui-formik-checkbox-group.js new file mode 100644 index 00000000..e2c186cd --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-checkbox-group.js @@ -0,0 +1,88 @@ +/** + * 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 { + Checkbox, + FormControl, + FormControlLabel, + FormGroup, + FormHelperText, + FormLabel +} from "@mui/material"; +import { useField } from "formik"; +import { INT_BASE } from "../../../utils/constants"; + +const MuiFormikCheckboxGroup = ({ name, label, options, ...props }) => { + const [field, meta, helpers] = useField({ name }); + + // Ensure field.value is an array + const values = Array.isArray(field.value) ? field.value : []; + + const handleChange = (ev) => { + const { value, checked } = ev.target; + + if (checked) { + // Add the value to the array if it's checked + helpers.setValue([...values, parseInt(value, INT_BASE)]); + } else { + // Remove the value from the array if it's unchecked + helpers.setValue( + values.filter((val) => val !== parseInt(value, INT_BASE)) + ); + } + }; + + return ( + + {label && {label}} + + {options.map((op) => ( + + } + label={op.label} + /> + ))} + + {meta.touched && meta.error && ( + {meta.error} + )} + + ); +}; + +MuiFormikCheckboxGroup.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string, + options: PropTypes.array.isRequired +}; + +export default MuiFormikCheckboxGroup; diff --git a/src/components/mui/formik-inputs/mui-formik-checkbox.js b/src/components/mui/formik-inputs/mui-formik-checkbox.js new file mode 100644 index 00000000..667e493a --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-checkbox.js @@ -0,0 +1,57 @@ +/** + * 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 { + Checkbox, + FormControl, + FormControlLabel, + FormHelperText +} from "@mui/material"; +import { useField } from "formik"; + +const MuiFormikCheckbox = ({ name, label, ...props }) => { + const [field, meta] = useField({ name, type: "checkbox" }); + + return ( + + + } + label={label} + /> + {meta.touched && meta.error && ( + {meta.error} + )} + + ); +}; + +MuiFormikCheckbox.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired +}; + +export default MuiFormikCheckbox; diff --git a/src/components/mui/formik-inputs/mui-formik-datepicker.js b/src/components/mui/formik-inputs/mui-formik-datepicker.js new file mode 100644 index 00000000..3e212133 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-datepicker.js @@ -0,0 +1,79 @@ +/** + * 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 { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; +import { useField } from "formik"; + +const MuiFormikDatepicker = ({ + name, + label, + required, + disabled = false, + ...props +}) => { + const [field, meta, helpers] = useField(name); + const requiredLabel = `${label} *`; + const handleBlur = () => { + helpers.setTouched(true, true); + }; + + return ( + + + + ); +}; + +MuiFormikDatepicker.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string, + required: PropTypes.bool, + disabled: PropTypes.bool +}; + +export default MuiFormikDatepicker; diff --git a/src/components/mui/formik-inputs/mui-formik-discountfield.js b/src/components/mui/formik-inputs/mui-formik-discountfield.js new file mode 100644 index 00000000..81bde06c --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-discountfield.js @@ -0,0 +1,110 @@ +/** + * 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, { useState } from "react"; +import PropTypes from "prop-types"; +import { InputAdornment } from "@mui/material"; +import { useField } from "formik"; +import MuiFormikTextField from "./mui-formik-textfield"; +import { DISCOUNT_TYPES, ONE_HUNDRED } from "../../../utils/constants"; + +const BLOCKED_KEYS = ["e", "E", "+", "-"]; + +const MuiFormikDiscountField = ({ + name, + label, + discountType, + inCents = false, + disabled = false, + ...props +}) => { + // eslint-disable-next-line no-unused-vars + const [field, meta, helpers] = useField(name); + const [cleared, setCleared] = useState(false); + const emptyValue = meta.initialValue === null ? null : 0; + + const getDisplayValue = () => { + if (cleared) return ""; + if (field.value == null || field.value === 0) { + return field.value === 0 ? 0 : ""; + } + return inCents ? field.value / ONE_HUNDRED : field.value; + }; + + const adornment = + discountType === DISCOUNT_TYPES.RATE + ? { + endAdornment: % + } + : { + startAdornment: $ + }; + + const inputProps = + discountType === DISCOUNT_TYPES.RATE + ? { max: 100, inputMode: "numeric", step: 1 } + : { inputMode: "decimal", step: 1 }; + + const handleChange = (e) => { + const newVal = e.target.value; + + if (newVal === "") { + setCleared(true); + helpers.setValue(emptyValue); + return; + } + + setCleared(false); + const numericValue = Number(newVal); + const newDiscount = inCents ? numericValue * ONE_HUNDRED : numericValue; + + helpers.setValue(newDiscount); + }; + + const handleKeyDown = (e) => { + if (BLOCKED_KEYS.includes(e.key)) { + e.preventDefault(); + e.stopPropagation(); + } + }; + + return ( + + ); +}; + +MuiFormikDiscountField.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string +}; + +export default MuiFormikDiscountField; diff --git a/src/components/mui/formik-inputs/mui-formik-dropdown-checkbox.js b/src/components/mui/formik-inputs/mui-formik-dropdown-checkbox.js new file mode 100644 index 00000000..52c5d169 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-dropdown-checkbox.js @@ -0,0 +1,88 @@ +/** + * 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 { + Checkbox, + Divider, + FormControl, + ListItemText, + MenuItem, + Select +} from "@mui/material"; +import { useField } from "formik"; +import T from "i18n-react/dist/i18n-react"; + +const MuiFormikDropdownCheckbox = ({ name, options, ...rest }) => { + const [field, meta, helpers] = useField(name); + const allSelected = options.every(({ value }) => + field.value?.includes(value) + ); + + const handleChange = (event) => { + const { value } = event.target; + + // If "all" was clicked + if (value.includes("all")) { + if (allSelected) { + helpers.setValue([]); + } else { + helpers.setValue(options.map((opt) => opt.value)); + } + } else { + helpers.setValue(value); + } + }; + + return ( + + + + ); +}; + +export default MuiFormikDropdownCheckbox; diff --git a/src/components/mui/formik-inputs/mui-formik-dropdown-radio.js b/src/components/mui/formik-inputs/mui-formik-dropdown-radio.js new file mode 100644 index 00000000..222d1eef --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-dropdown-radio.js @@ -0,0 +1,65 @@ +/** + * 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 { + FormControl, + ListItemText, + MenuItem, + Radio, + Select +} from "@mui/material"; +import { useField } from "formik"; +import T from "i18n-react/dist/i18n-react"; + +const MuiFormikDropdownRadio = ({ name, options, placeholder, ...rest }) => { + const finalPlaceholder = + placeholder || T.translate("general.select_an_option"); + const [field, meta, helpers] = useField(name); + + const handleChange = (event) => { + helpers.setValue(event.target.value); + }; + + return ( + + + + ); +}; + +export default MuiFormikDropdownRadio; diff --git a/src/components/mui/formik-inputs/mui-formik-file-size-field.js b/src/components/mui/formik-inputs/mui-formik-file-size-field.js new file mode 100644 index 00000000..6fea7904 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-file-size-field.js @@ -0,0 +1,96 @@ +/** + * 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, { useState } from "react"; +import PropTypes from "prop-types"; +import { InputAdornment } from "@mui/material"; +import { useField } from "formik"; +import MuiFormikTextField from "./mui-formik-textfield"; +import { BYTES_PER_MB } from "../../../utils/constants"; + +const BLOCKED_KEYS = ["e", "E", "+", "-", ".", ","]; + +const bytesToMb = (bytes) => Math.floor(bytes / BYTES_PER_MB); + +const MuiFormikFilesizeField = ({ name, label, ...props }) => { + const [field, meta, helpers] = useField(name); + const [cleared, setCleared] = useState(false); + + const emptyValue = meta.initialValue === null ? null : 0; + + const getDisplayValue = () => { + if (cleared) return ""; + if (field.value == null || field.value === 0) { + return field.value === 0 ? 0 : ""; + } + return bytesToMb(field.value); + }; + + const handleChange = (e) => { + const mbValue = e.target.value; + + if (mbValue === "") { + setCleared(true); + helpers.setValue(emptyValue); + return; + } + + setCleared(false); + const bytes = Number(mbValue) * BYTES_PER_MB; + helpers.setValue(bytes); + }; + + const handleKeyDown = (e) => { + if (BLOCKED_KEYS.includes(e.key)) { + e.preventDefault(); + e.stopPropagation(); + return; + } + // Block "0" as first character — only 1-9 are valid leading digits. + // When value is empty or already "0", prevent any "0" keypress. + if (e.key === "0" && (e.target.value === "" || e.target.value === "0")) { + e.preventDefault(); + e.stopPropagation(); + } + }; + + return ( + MB + }, + htmlInput: { + min: 0, + inputMode: "numeric", + step: 1 + } + }} + onKeyDown={handleKeyDown} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> + ); +}; + +MuiFormikFilesizeField.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired +}; + +export default MuiFormikFilesizeField; diff --git a/src/components/mui/formik-inputs/mui-formik-pricefield.js b/src/components/mui/formik-inputs/mui-formik-pricefield.js new file mode 100644 index 00000000..088b2d25 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-pricefield.js @@ -0,0 +1,96 @@ +/** + * 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, { useState } from "react"; +import PropTypes from "prop-types"; +import { InputAdornment } from "@mui/material"; +import { useField } from "formik"; +import MuiFormikTextField from "./mui-formik-textfield"; +import { ONE_HUNDRED } from "../../../utils/constants"; + +const BLOCKED_KEYS = ["e", "E", "+", "-"]; + +const MuiFormikPriceField = ({ + name, + label, + inCents = false, + inputProps = { step: 0.01 }, + ...props +}) => { + // eslint-disable-next-line no-unused-vars + const [field, meta, helpers] = useField(name); + const [cleared, setCleared] = useState(false); + + const emptyValue = meta.initialValue === null ? null : 0; + + const getDisplayValue = () => { + if (cleared) return ""; + if (field.value == null || field.value === 0) { + return field.value === 0 ? 0 : ""; + } + return inCents ? field.value / ONE_HUNDRED : field.value; + }; + + const handleChange = (e) => { + const newVal = e.target.value; + + if (newVal === "") { + setCleared(true); + helpers.setValue(emptyValue); + return; + } + + setCleared(false); + const numericValue = Number(newVal); + const newPrice = inCents ? numericValue * ONE_HUNDRED : numericValue; + + helpers.setValue(newPrice); + }; + + const handleKeyDown = (e) => { + if (BLOCKED_KEYS.includes(e.key)) { + e.preventDefault(); + e.stopPropagation(); + } + }; + + return ( + $ + } + }} + inputProps={{ + min: 0, + inputMode: "decimal", + ...inputProps + }} + onKeyDown={handleKeyDown} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> + ); +}; + +MuiFormikPriceField.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string +}; + +export default MuiFormikPriceField; diff --git a/src/components/mui/formik-inputs/mui-formik-quantity-field.js b/src/components/mui/formik-inputs/mui-formik-quantity-field.js new file mode 100644 index 00000000..bd95f5e6 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-quantity-field.js @@ -0,0 +1,43 @@ +/** + * 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 MuiFormikTextField from "./mui-formik-textfield"; + +const BLOCKED_KEYS = ["e", "E", "+", "-"]; + +const MuiFormikQuantityField = ({ ...props }) => ( + { + if (BLOCKED_KEYS.includes(e.key)) { + e.nativeEvent.preventDefault(); + e.nativeEvent.stopImmediatePropagation(); + } + }} + inputProps={{ + min: 0, + inputMode: "numeric" + }} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> +); + +MuiFormikQuantityField.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string +}; + +export default MuiFormikQuantityField; diff --git a/src/components/mui/formik-inputs/mui-formik-radio-group.js b/src/components/mui/formik-inputs/mui-formik-radio-group.js new file mode 100644 index 00000000..d2b48474 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-radio-group.js @@ -0,0 +1,82 @@ +/** + * 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 { + FormControl, + FormControlLabel, + FormHelperText, + FormLabel, + Radio, + RadioGroup +} from "@mui/material"; +import { useField } from "formik"; + +const MuiFormikRadioGroup = ({ + name, + label, + marginWrapper = "normal", + options, + ...props +}) => { + const [field, meta] = useField({ name }); + + return ( + + {label && {label}} + + {options.map((op) => ( + + } + label={op.label} + /> + ))} + + {meta.touched && meta.error && ( + {meta.error} + )} + + ); +}; + +MuiFormikRadioGroup.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string, + marginWrapper: PropTypes.string, + options: PropTypes.array.isRequired +}; + +export default MuiFormikRadioGroup; diff --git a/src/components/mui/formik-inputs/mui-formik-select-group.js b/src/components/mui/formik-inputs/mui-formik-select-group.js new file mode 100644 index 00000000..5e8c02a3 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-select-group.js @@ -0,0 +1,352 @@ +/** + * 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, { useState, useEffect, useMemo } from "react"; +import { + MenuItem, + Checkbox, + ListItemText, + CircularProgress, + Select, + OutlinedInput, + Box, + ListSubheader, + Divider +} from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import PropTypes from "prop-types"; +import { useField } from "formik"; + +const getCustomIcon = (loading) => () => + ( + + {loading && } + + + ); + +const MuiFormikSelectGroup = ({ + name, + queryFunction, + queryParams = [], + placeholder = "Select options", + showSelectAll = false, + selectAllLabel = "Select All", + getOptionLabel = (item) => item.name, + getOptionValue = (item) => item.id, + noOptionsLabel = "No items", + getGroupId = null, + getGroupLabel = null, + disabled = false +}) => { + const [field, meta, helpers] = useField(name); + const [options, setOptions] = useState([]); + const [groupedOptions, setGroupedOptions] = useState([]); + const [loading, setLoading] = useState(false); + + const isAllSelected = + Array.isArray(field.value) && field.value.includes("all"); + const value = isAllSelected ? options : field.value || []; + const error = meta.touched && meta.error; + + const fetchOptions = async () => { + setLoading(true); + try { + await queryFunction(...queryParams, (results) => { + setOptions(results); + + if (getGroupId && getGroupLabel) { + // using map no avoid duplicate groups + const groupsMap = new Map(); + + results.forEach((item) => { + const groupId = getGroupId(item); + const groupLabel = getGroupLabel(item); + + if (!groupsMap.has(groupId)) { + groupsMap.set(groupId, { + id: groupId, + label: groupLabel, + options: [] + }); + } + + groupsMap.get(groupId).options.push(item); + }); + + setGroupedOptions(Array.from(groupsMap.values())); + } else { + setGroupedOptions([ + { + id: "default", + label: null, + options: results + } + ]); + } + setLoading(false); + }); + } catch (error) { + setLoading(false); + } + }; + + useEffect(() => { + fetchOptions(); + }, []); + + const handleChange = (event) => { + const selectedValues = event.target.value; + + if (selectedValues.includes("selectAll")) { + const currentValues = Array.isArray(value) + ? value.map(getOptionValue) + : []; + + if (isAllSelected || currentValues.length === options.length) { + helpers.setValue([]); + } else { + helpers.setValue(["all"]); + } + return; + } + + const filteredValues = selectedValues.filter((val) => val !== "selectAll"); + + const selectedItems = filteredValues + .map((val) => { + const found = options.find((item) => getOptionValue(item) === val); + return found; + }) + .filter(Boolean); + + helpers.setValue(selectedItems); + }; + + const selectedValues = isAllSelected + ? options.map(getOptionValue) + : Array.isArray(value) + ? value.map((item) => getOptionValue(item)) + : []; + + const renderGroupedOptions = () => + groupedOptions + .map((group, groupIndex) => [ + group.label && ( + + {group.label} + + ), + ...group.options.map((option) => { + const optionValue = getOptionValue(option); + const isChecked = selectedValues.includes(optionValue); + + return ( + { + let newValues; + if (isAllSelected) { + newValues = options + .filter((opt) => getOptionValue(opt) !== optionValue) + .map(getOptionValue); + } else { + newValues = isChecked + ? selectedValues.filter((v) => v !== optionValue) + : [...selectedValues, optionValue]; + } + handleChange({ target: { value: newValues } }); + }} + > + + + + ); + }), + group.label && groupIndex < groupedOptions.length - 1 && ( + + ) + ]) + .flat() + .filter(Boolean); + + const renderMenuContent = () => { + if (loading) { + return ( + + + + ); + } + + if (options.length === 0) { + return ( + + + + ); + } + + return ( + <> + {showSelectAll && ( + <> + { + // custom event value to select all + handleChange({ target: { value: ["selectAll"] } }); + }} + > + 0 && !isAllSelected} + sx={{ + p: 1, + "& svg": { + fontSize: 24 + } + }} + /> + + + + + )} + {renderGroupedOptions()} + + ); + }; + + const IconWithLoading = useMemo(() => getCustomIcon(loading), [loading]); + + return ( + <> + + {error && ( +
    + {error} +
    + )} + + ); +}; + +MuiFormikSelectGroup.propTypes = { + name: PropTypes.string.isRequired, + queryFunction: PropTypes.func.isRequired, + queryParams: PropTypes.array, + placeholder: PropTypes.string, + showSelectAll: PropTypes.bool, + selectAllLabel: PropTypes.string, + getOptionLabel: PropTypes.func, + getOptionValue: PropTypes.func, + getGroupId: PropTypes.func, + getGroupLabel: PropTypes.func, + noOptionsLabel: PropTypes.string, + disabled: PropTypes.bool +}; + +export default MuiFormikSelectGroup; diff --git a/src/components/mui/formik-inputs/mui-formik-select.js b/src/components/mui/formik-inputs/mui-formik-select.js new file mode 100644 index 00000000..32294681 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-select.js @@ -0,0 +1,95 @@ +/** + * 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 { + Select, + FormHelperText, + FormControl, + InputAdornment, + IconButton, + InputLabel +} from "@mui/material"; +import ClearIcon from "@mui/icons-material/Clear"; +import { useField } from "formik"; + +const MuiFormikSelect = ({ + name, + label, + placeholder, + children, + isClearable, + ...rest +}) => { + const [field, meta, helpers] = useField(name); + + const handleClear = (ev) => { + ev.stopPropagation(); + helpers.setValue(""); + }; + + const hasValue = field?.value && field.value !== ""; + const shouldShrink = hasValue || Boolean(placeholder); + + return ( + + {label && ( + + {label} + + )} + + {meta.touched && meta.error && ( + {meta.error} + )} + + ); +}; + +MuiFormikSelect.propTypes = { + name: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + placeholder: PropTypes.string, + isClearable: PropTypes.bool +}; + +export default MuiFormikSelect; diff --git a/src/components/mui/formik-inputs/mui-formik-summit-addon-select.js b/src/components/mui/formik-inputs/mui-formik-summit-addon-select.js new file mode 100644 index 00000000..2216d9f8 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-summit-addon-select.js @@ -0,0 +1,49 @@ +/** + * 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 { useField } from "formik"; +import SummitAddonSelect from "../summit-addon-select"; + +const MuiFormikSummitAddonSelect = ({ + name, + summitId, + placeholder = "Select...", + inputProps = {} +}) => { + const [field, meta, helpers] = useField(name); + + return ( + + ); +}; + +MuiFormikSummitAddonSelect.propTypes = { + name: PropTypes.string.isRequired, + summitId: PropTypes.number.isRequired, + placeholder: PropTypes.string, + inputProps: PropTypes.object +}; + +export default MuiFormikSummitAddonSelect; diff --git a/src/components/mui/formik-inputs/mui-formik-switch.js b/src/components/mui/formik-inputs/mui-formik-switch.js new file mode 100644 index 00000000..26cce437 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-switch.js @@ -0,0 +1,57 @@ +/** + * 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 { + FormControl, + FormControlLabel, + FormHelperText, + Switch +} from "@mui/material"; +import { useField } from "formik"; + +const MuiFormikSwitch = ({ name, label, ...props }) => { + const [field, meta] = useField({ name }); + + return ( + + + } + label={label} + /> + {meta.touched && meta.error && ( + {meta.error} + )} + + ); +}; + +MuiFormikSwitch.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired +}; + +export default MuiFormikSwitch; diff --git a/src/components/mui/formik-inputs/mui-formik-textfield.js b/src/components/mui/formik-inputs/mui-formik-textfield.js new file mode 100644 index 00000000..b8526fb3 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-textfield.js @@ -0,0 +1,69 @@ +/** + * 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 { Box, TextField, Typography } from "@mui/material"; +import { useField } from "formik"; + +const MuiFormikTextField = ({ + name, + label, + maxLength, + required = false, + ...props +}) => { + const [field, meta] = useField(name); + const currentLength = field.value?.length || 0; + + let finalLabel = ""; + + if (label) { + finalLabel = required ? `${label} *` : label; + } + + return ( + + + {maxLength && ( + + {`${maxLength - currentLength} characters left`} + + )} + + ); +}; + +MuiFormikTextField.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string, + maxLength: PropTypes.number, + required: PropTypes.bool +}; + +export default MuiFormikTextField; diff --git a/src/components/mui/formik-inputs/mui-formik-timepicker.js b/src/components/mui/formik-inputs/mui-formik-timepicker.js new file mode 100644 index 00000000..5b98355b --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-timepicker.js @@ -0,0 +1,69 @@ +/** + * 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 { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; +import { TimePicker } from "@mui/x-date-pickers/TimePicker"; + +import { useField } from "formik"; + +const MuiFormikTimepicker = ({ + name, + minTime, + maxTime, + timeZone, + disabled = false +}) => { + const [field, meta, helpers] = useField(name); + + return ( + + + + ); +}; + +MuiFormikTimepicker.propTypes = { + name: PropTypes.string.isRequired +}; + +export default MuiFormikTimepicker; diff --git a/src/components/mui/formik-inputs/mui-formik-upload.js b/src/components/mui/formik-inputs/mui-formik-upload.js new file mode 100644 index 00000000..bec4e427 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-upload.js @@ -0,0 +1,112 @@ +/** + * 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 { FormHelperText } from "@mui/material"; +import { UploadInputV2 } from "../../inputs/upload-input-v2"; +import { useField } from "formik"; +import { + ALLOWED_INVENTORY_IMAGE_FORMATS, + MAX_INVENTORY_IMAGE_UPLOAD_SIZE, + MAX_INVENTORY_IMAGES_UPLOAD_QTY +} from "../../../utils/constants"; + +const MuiFormikUpload = ({ + id, + name, + onDelete, + maxFiles = MAX_INVENTORY_IMAGES_UPLOAD_QTY +}) => { + const [field, meta, helpers] = useField(name); + + const mediaType = { + max_size: MAX_INVENTORY_IMAGE_UPLOAD_SIZE, + max_uploads_qty: maxFiles, + type: { + allowed_extensions: ALLOWED_INVENTORY_IMAGE_FORMATS + } + }; + + const getInputValue = () => + field.value?.length > 0 + ? field.value.map((img) => ({ + ...img, + filename: + img.file_name ?? img.filename ?? img.file_path ?? img.file_url + })) + : []; + + const buildFileObject = (response) => { + const file = {}; + if (response.id !== undefined) file.id = response.id; + if (response.name) file.file_name = response.name; + if (response.md5) file.md5 = response.md5; + if (response.mime_type) file.mime_type = response.mime_type; + if (response.source_bucket) file.bucket = response.source_bucket; + if (response.size) file.size = response.size; + if (response.path && response.name) + file.file_path = `${response.path}${response.name}`; + return file; + }; + + const handleUploadComplete = (response) => { + if (response) { + const image = buildFileObject(response); + helpers.setValue([...(field.value || []), image]); + helpers.setTouched(true); + } + }; + + const handleRemove = (imageFile) => { + const updated = (field.value || []).filter( + (i) => i.filename !== imageFile.name + ); + helpers.setValue(updated); + if (onDelete) { + onDelete(imageFile.id); + } + }; + + const canAddMore = () => (field.value?.length || 0) < maxFiles; + + return ( + <> + {meta.touched && meta.error && ( + {meta.error} + )} + + + ); +}; + +MuiFormikUpload.propTypes = { + id: PropTypes.string, + name: PropTypes.string.isRequired, + onDelete: PropTypes.func, + maxFiles: PropTypes.number +}; + +export default MuiFormikUpload; diff --git a/src/components/mui/formik-inputs/mui-sponsor-input.js b/src/components/mui/formik-inputs/mui-sponsor-input.js new file mode 100644 index 00000000..aa8d6a45 --- /dev/null +++ b/src/components/mui/formik-inputs/mui-sponsor-input.js @@ -0,0 +1,166 @@ +/** + * 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, { useState, useEffect, useMemo } from "react"; +import PropTypes from "prop-types"; +import TextField from "@mui/material/TextField"; +import Autocomplete from "@mui/material/Autocomplete"; +import CircularProgress from "@mui/material/CircularProgress"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { useField } from "formik"; +import { querySponsorsV2 } from "../../../utils/query-actions"; +import { DEBOUNCE_WAIT_250 } from "../../../utils/constants"; + +const MuiSponsorInput = ({ + id, + name, + placeholder, + plainValue, + isMulti = false, + summitId, + ...rest +}) => { + const [field, meta, helpers] = useField(name); + const [options, setOptions] = useState([]); + const [open, setOpen] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [loading, setLoading] = useState(false); + + const { value } = field; + const error = meta.touched && meta.error; + + const fetchOptions = async (input) => { + if (!input) { + setOptions([]); + return; + } + + setLoading(true); + + const normalize = (results) => + results.map((r) => ({ + value: r.id.toString(), + label: r.name + })); + + await querySponsorsV2(input, summitId, (results) => { + setOptions(normalize(results)); + setLoading(false); + }); + }; + + useEffect(() => { + if (inputValue) { + const delayDebounce = setTimeout(() => { + fetchOptions(inputValue); + }, DEBOUNCE_WAIT_250); + return () => clearTimeout(delayDebounce); + } + }, [inputValue]); + + const selectedValue = useMemo(() => { + if (!value) return isMulti ? [] : null; + + if (isMulti) { + return value.map((v) => + plainValue + ? { value: v, label: v } + : { value: v.id?.toString(), label: v.name } + ); + } + return plainValue + ? { value, label: value } + : { value: value.id?.toString(), label: value.name }; + }, [value, plainValue, isMulti]); + + const handleChange = (_, newValue) => { + let theValue; + + if (!newValue || (Array.isArray(newValue) && newValue.length === 0)) { + theValue = isMulti ? [] : plainValue ? "" : { id: "", name: "" }; + } else if (isMulti) { + theValue = plainValue + ? newValue.map((v) => v.label) + : newValue.map((v) => ({ + id: parseInt(v.value), + name: v.label + })); + } else { + theValue = plainValue + ? newValue.label + : { id: parseInt(newValue.value), name: newValue.label }; + } + + helpers.setValue(theValue); + }; + + const errorMessage = + error && (typeof error === "string" ? error : error[Object.keys(error)[0]]); + + return ( + setOpen(true)} + onClose={() => setOpen(false)} + options={options} + value={selectedValue} + getOptionLabel={(option) => option.label} + isOptionEqualToValue={(option, value) => option.value === value.value} + onInputChange={(_, newInputValue) => { + setInputValue(newInputValue); + }} + multiple={isMulti} + onChange={handleChange} + loading={loading} + fullWidth + popupIcon={} + renderInput={(params) => ( + + {loading && } + {params.InputProps?.endAdornment} + + ) + }} + /> + )} + {...rest} + /> + ); +}; + +MuiSponsorInput.propTypes = { + id: PropTypes.string, + name: PropTypes.string.isRequired, + placeholder: PropTypes.string, + plainValue: PropTypes.bool, + isMulti: PropTypes.bool +}; + +export default MuiSponsorInput; diff --git a/src/components/mui/formik-inputs/sponsorship-input-mui.js b/src/components/mui/formik-inputs/sponsorship-input-mui.js new file mode 100644 index 00000000..358eb192 --- /dev/null +++ b/src/components/mui/formik-inputs/sponsorship-input-mui.js @@ -0,0 +1,162 @@ +/** + * 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, { useState, useEffect, useMemo } from "react"; +import PropTypes from "prop-types"; +import TextField from "@mui/material/TextField"; +import Autocomplete from "@mui/material/Autocomplete"; +import CircularProgress from "@mui/material/CircularProgress"; +import { useField } from "formik"; +import { querySponsorships } from "../../../utils/query-actions"; +import { DEBOUNCE_WAIT_250 } from "../../../utils/constants"; + +const SponsorshipTypeInputMUI = ({ + id, + name, + placeholder, + plainValue, + isMulti = false, + ...rest +}) => { + const [field, meta, helpers] = useField(name); + const [options, setOptions] = useState([]); + const [open, setOpen] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [loading, setLoading] = useState(false); + + const { value } = field; + const error = meta.touched && meta.error; + + const errorMessage = + typeof error === "object" ? error?.id || error?.name || "" : error; + + const fetchOptions = async (input) => { + if (!input) { + setOptions([]); + return; + } + + setLoading(true); + + const normalize = (results) => + results.map((r) => ({ + value: r.id.toString(), + label: r.name + })); + + await querySponsorships(input, (results) => { + setOptions(normalize(results)); + setLoading(false); + }); + }; + + useEffect(() => { + if (inputValue) { + const delayDebounce = setTimeout(() => { + fetchOptions(inputValue); + }, DEBOUNCE_WAIT_250); + return () => clearTimeout(delayDebounce); + } + }, [inputValue]); + + const selectedValue = useMemo(() => { + if (!value) return isMulti ? [] : null; + + if (isMulti) { + return value.map((v) => + plainValue + ? { value: v, label: v } + : { value: v.id?.toString(), label: v.name } + ); + } + return plainValue + ? { value, label: value } + : { value: value.id?.toString(), label: value.name }; + }, [value, plainValue, isMulti]); + + const handleChange = (_, newValue) => { + let theValue; + + if (!newValue || (Array.isArray(newValue) && newValue.length === 0)) { + theValue = isMulti ? [] : plainValue ? "" : { id: "", name: "" }; + } else if (isMulti) { + theValue = plainValue + ? newValue.map((v) => v.label) + : newValue.map((v) => ({ + id: parseInt(v.value), + name: v.label + })); + } else { + theValue = plainValue + ? newValue.label + : { id: parseInt(newValue.value), name: newValue.label }; + } + + helpers.setValue(theValue); + }; + + return ( + setOpen(true)} + onClose={() => setOpen(false)} + options={options} + value={selectedValue} + getOptionLabel={(option) => option.label} + isOptionEqualToValue={(option, value) => option.value === value.value} + onInputChange={(_, newInputValue) => { + setInputValue(newInputValue); + }} + multiple={isMulti} + onChange={handleChange} + loading={loading} + fullWidth + renderInput={(params) => ( + + {loading && } + {params.InputProps?.endAdornment} + + ) + }} + /> + )} + {...rest} + /> + ); +}; + +SponsorshipTypeInputMUI.propTypes = { + id: PropTypes.string, + name: PropTypes.string.isRequired, + placeholder: PropTypes.string, + plainValue: PropTypes.bool, + isMulti: PropTypes.bool +}; + +export default SponsorshipTypeInputMUI; diff --git a/src/components/mui/formik-inputs/sponsorship-summit-select-mui.js b/src/components/mui/formik-inputs/sponsorship-summit-select-mui.js new file mode 100644 index 00000000..56ff4f09 --- /dev/null +++ b/src/components/mui/formik-inputs/sponsorship-summit-select-mui.js @@ -0,0 +1,166 @@ +/** + * 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, { useState, useEffect, useMemo } from "react"; +import { + MenuItem, + Checkbox, + ListItemText, + CircularProgress, + Select, + OutlinedInput, + Box +} from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import PropTypes from "prop-types"; +import { useField } from "formik"; +import { querySponsorshipsBySummit } from "../../../utils/query-actions"; + +const getCustomIcon = (loading) => { + const Icon = () => ( + + {loading && } + + + ); + return Icon; +}; + +const SponsorshipsBySummitSelectMUI = ({ + name, + summitId, + placeholder, + plainValue, + hiddenOptions = [] +}) => { + const [field, meta, helpers] = useField(name); + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + + const value = field.value || []; + const error = meta.touched && meta.error; + + const fetchOptions = async () => { + setLoading(true); + await querySponsorshipsBySummit("", summitId, (results) => { + const normalized = results + .filter((r) => !hiddenOptions.includes(r.id)) + .map((r) => ({ + value: r.id.toString(), + label: r.type.name + })); + setOptions(normalized); + setLoading(false); + }); + }; + + useEffect(() => { + fetchOptions(); + }, []); + + const handleChange = (event) => { + const selected = event.target.value; + const selectedItems = plainValue + ? selected + : selected.map((id) => { + const match = options.find((o) => o.value === id); + return { id: parseInt(match.value), name: match.label }; + }); + + helpers.setValue(selectedItems); + }; + + const selectedValues = plainValue + ? value + : value.map((v) => v.id?.toString()); + + const IconWithLoading = useMemo(() => getCustomIcon(loading), [loading]); + + return ( + <> + + {error && ( +
    + {error} +
    + )} + + ); +}; + +SponsorshipsBySummitSelectMUI.propTypes = { + name: PropTypes.string.isRequired, + summitId: PropTypes.number.isRequired, + placeholder: PropTypes.string, + plainValue: PropTypes.bool +}; + +export default SponsorshipsBySummitSelectMUI; diff --git a/src/components/mui/infinite-table/index.js b/src/components/mui/infinite-table/index.js new file mode 100644 index 00000000..eb678ff3 --- /dev/null +++ b/src/components/mui/infinite-table/index.js @@ -0,0 +1,161 @@ +/** + * 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 * as React from "react"; +import PropTypes from "prop-types"; +import T from "i18n-react/dist/i18n-react"; +import { visuallyHidden } from "@mui/utils"; +import Box from "@mui/material/Box"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableSortLabel from "@mui/material/TableSortLabel"; +import Paper from "@mui/material/Paper"; +import TableRow from "@mui/material/TableRow"; +import styles from "./styles.module.less"; +import { MILLISECONDS_TO_SECONDS } from "../../../utils/constants"; + +const MuiInfiniteTable = ({ + boxHeight = "400px", + columns = [], + data = [], + loadMoreData, + onRowEdit, + onSort, + options = { sortCol: "", sortDir: "" } +}) => { + const { sortCol, sortDir } = options; + + const isLoadingRef = React.useRef(false); + + const handleScroll = (event) => { + if (isLoadingRef.current) return; + + const { scrollTop, scrollHeight, clientHeight } = event.target; + // eslint-disable-next-line no-magic-numbers + if (scrollTop + clientHeight >= scrollHeight - 20) { + isLoadingRef.current = true; + loadMoreData(); + + setTimeout(() => { + isLoadingRef.current = false; + }, MILLISECONDS_TO_SECONDS); + } + }; + + return ( + + + + + {/* TABLE HEADER */} + + + {columns.map((col) => ( + + {col.sortable ? ( + onSort(col.columnKey, sortDir * -1)} + > + {col.header} + {sortCol === col.columnKey ? ( + + {sortDir === "-1" + ? T.translate("mui_table.sorted_desc") + : T.translate("mui_table.sorted_asc")} + + ) : null} + + ) : ( + col.header + )} + + ))} + + + + {/* TABLE BODY */} + + {data.map((row, rowIndex) => ( + + {columns.map((col) => { + const cellContent = col.render + ? col.render(row, { onRowEdit }) + : row[col.columnKey]; + + const cellClassName = col.className + ? styles[col.className] || col.className + : ""; + + return ( + + {cellContent} + + ); + })} + + ))} + + {/* No items */} + {data.length === 0 && ( + + + {T.translate("mui_table.no_data")} + + + )} + +
    +
    +
    +
    + ); +}; + +MuiInfiniteTable.propTypes = { + boxHeight: PropTypes.string, + columns: PropTypes.array, + data: PropTypes.array, + loadMoreData: PropTypes.func, + onRowEdit: PropTypes.func, + onSort: PropTypes.func, + options: PropTypes.object +}; + +export default MuiInfiniteTable; diff --git a/src/components/mui/infinite-table/styles.module.less b/src/components/mui/infinite-table/styles.module.less new file mode 100644 index 00000000..c32a7be9 --- /dev/null +++ b/src/components/mui/infinite-table/styles.module.less @@ -0,0 +1,14 @@ +.dottedBorderLeft { + position: relative; + border-left: none; + &::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + border-left: 1px dashed #e0e0e0; + height: 3em; + margin-top: 2em; + } +} diff --git a/src/components/mui/menu-button.js b/src/components/mui/menu-button.js new file mode 100644 index 00000000..70c8bb40 --- /dev/null +++ b/src/components/mui/menu-button.js @@ -0,0 +1,122 @@ +/** + * 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, { useState } from "react"; +import PropTypes from "prop-types"; +import { Badge, Button, Menu, MenuItem } from "@mui/material"; + +const MenuButton = ({ + buttonId, + menuId, + menuItems, + hasBadge, + children, + ...rest +}) => { + const [anchorEl, setAnchorEl] = useState(null); + const [selectedItems, setSelectedItems] = useState([]); + + const handleButtonClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleOptionClick = (item) => { + if (hasBadge) { + const newOptions = selectedItems.includes(item.label) + ? selectedItems.filter((key) => key !== item.label) + : [...selectedItems, item.label]; + item.onClick(); + setSelectedItems(newOptions); + } + item.onClick(); + handleClose(); + }; + + const badgeCount = hasBadge ? selectedItems.length : undefined; + + return ( + <> + + + {menuItems.map((item) => { + const isSelected = hasBadge && selectedItems.includes(item.label); + return ( + handleOptionClick(item)} + sx={{ + borderBottom: 1, + borderColor: "divider", + "&:last-of-type": { borderBottom: 0 }, + color: isSelected ? "--variant-textColor" : "#000" + }} + > + {item.label} + + ); + })} + + + ); +}; + +MenuButton.propTypes = { + buttonId: PropTypes.string, + menuId: PropTypes.string, + menuItems: PropTypes.array.isRequired, + hasBadge: PropTypes.bool +}; + +export default MenuButton; diff --git a/src/components/mui/search-input.js b/src/components/mui/search-input.js new file mode 100644 index 00000000..e83e2499 --- /dev/null +++ b/src/components/mui/search-input.js @@ -0,0 +1,71 @@ +/** + * 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 { TextField, IconButton } from "@mui/material"; +import SearchIcon from "@mui/icons-material/Search"; +import ClearIcon from "@mui/icons-material/Clear"; + +const SearchInput = ({ term, onSearch, placeholder = "Search..." }) => { + const [searchTerm, setSearchTerm] = useState(term); + + useEffect(() => { + setSearchTerm(term || ""); + }, [term]); + + const handleSearch = (ev) => { + if (ev.key === "Enter") { + onSearch(searchTerm); + } + }; + + const handleClear = () => { + setSearchTerm(""); + onSearch(""); + }; + + return ( + + + + ) : ( + + ) + } + }} + onChange={(event) => setSearchTerm(event.target.value)} + onKeyDown={handleSearch} + fullWidth + sx={{ + "& .MuiOutlinedInput-root": { + height: "36px" + } + }} + /> + ); +}; + +export default SearchInput; diff --git a/src/components/mui/showConfirmDialog.js b/src/components/mui/showConfirmDialog.js new file mode 100644 index 00000000..b5591153 --- /dev/null +++ b/src/components/mui/showConfirmDialog.js @@ -0,0 +1,57 @@ +/** + * 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 ReactDOM from "react-dom"; +import React from "react"; +import ConfirmDialog from "./confirm-dialog"; + +const showConfirmDialog = ({ + title, + text, + iconType = "", + confirmButtonText = "Confirm", + cancelButtonText = "Cancel", + confirmButtonColor = "primary", + cancelButtonColor = "primary" +}) => + new Promise((resolve) => { + const container = document.createElement("div"); + document.body.appendChild(container); + + const close = (answer) => { + ReactDOM.unmountComponentAtNode(container); + container.remove(); + resolve(answer); + }; + + const handleConfirm = () => close(true); + const handleCancel = () => close(false); + + ReactDOM.render( + , + container + ); + }); + +export default showConfirmDialog; diff --git a/src/components/mui/sortable-table/mui-table-sortable.js b/src/components/mui/sortable-table/mui-table-sortable.js new file mode 100644 index 00000000..7b1dbe17 --- /dev/null +++ b/src/components/mui/sortable-table/mui-table-sortable.js @@ -0,0 +1,315 @@ +/** + * 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 * as React from "react"; +import T from "i18n-react/dist/i18n-react"; +import Box from "@mui/material/Box"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TablePagination from "@mui/material/TablePagination"; +import TableSortLabel from "@mui/material/TableSortLabel"; +import TableRow from "@mui/material/TableRow"; +import Paper from "@mui/material/Paper"; +import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; +import { IconButton } from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd"; +import { visuallyHidden } from "@mui/utils"; + +import styles from "./styles.module.less"; + +import { + DEFAULT_PER_PAGE, + FIFTY_PER_PAGE, + TWENTY_PER_PAGE +} from "../../../utils/constants"; +import showConfirmDialog from "../showConfirmDialog"; + +const MuiTableSortable = ({ + columns = [], + data = [], + totalRows, + perPage, + currentPage, + onPageChange, + onPerPageChange, + onSort, + options = { sortCol: "", sortDir: 1 }, + getName = (item) => item.name, + onEdit, + onDelete, + deleteDialogTitle = null, + deleteDialogBody = null, + onReorder, + idKey = "id", + updateOrderKey = "order" +}) => { + const handleChangePage = (_, newPage) => { + onPageChange(newPage + 1); + }; + + const handleChangeRowsPerPage = (ev) => { + onPerPageChange(ev.target.value); + }; + + const basePerPageOptions = [ + DEFAULT_PER_PAGE, + TWENTY_PER_PAGE, + FIFTY_PER_PAGE + ]; + + const customPerPageOptions = basePerPageOptions.includes(perPage) + ? basePerPageOptions + : [...basePerPageOptions, perPage].sort((a, b) => a - b); + + const { sortCol, sortDir } = options; + + const handleDragEnd = (result) => { + if (!result.destination || result.source.index === result.destination.index) + return; + + const reordered = [...data]; + const [movedItem] = reordered.splice(result.source.index, 1); + reordered.splice(result.destination.index, 0, movedItem); + + // change value based on updateOrderKey + if (updateOrderKey) { + reordered.forEach((item, idx) => { + item[updateOrderKey] = idx + 1; + }); + } + + const movedItemId = movedItem.id; + const newOrder = reordered.find( + (item) => item[idKey || "id"] === movedItemId + )?.[updateOrderKey]; + + onReorder?.(reordered, movedItemId, newOrder); + }; + + const handleDelete = async (item) => { + const isConfirmed = await showConfirmDialog({ + title: deleteDialogTitle || T.translate("general.are_you_sure"), + text: + typeof deleteDialogBody === "function" + ? deleteDialogBody(getName(item)) + : deleteDialogBody || + `${T.translate("general.row_remove_warning")} ${getName(item)}`, + type: "warning", + showCancelButton: true, + confirmButtonColor: "#DD6B55", + confirmButtonText: T.translate("general.yes_delete") + }); + + if (isConfirmed) { + onDelete(item.id); + } + }; + + return ( + + + + + {/* TABLE HEADER */} + + + {columns.map((col) => ( + + {col.sortable ? ( + onSort(col.columnKey, sortDir * -1)} + > + {col.header} + {sortCol === col.columnKey ? ( + + {sortDir === -1 + ? T.translate("mui_table.sorted_desc") + : T.translate("mui_table.sorted_asc")} + + ) : null} + + ) : ( + col.header + )} + + ))} + {onEdit && } + {onDelete && } + {onReorder && } + + + + {/* TABLE BODY */} + + + {(droppableProvided) => ( + + {data.map((row, rowIndex) => ( + + {(provided, snapshot) => ( + + {/* Main content columns */} + {columns.map((col) => ( + + {col.render?.(row) || row[col.columnKey]} + + ))} + {/* Edit column */} + {onEdit && ( + + onEdit(row)} + > + + + + )} + {/* Delete column */} + {onDelete && ( + + handleDelete(row)} + > + + + + )} + {/* Re order column */} + {onReorder && ( + + + + + + )} + + )} + + ))} + {droppableProvided.placeholder} + {data.length === 0 && ( + + + {T.translate("mui_table.no_items")} + + + )} + + )} + + +
    +
    + + {/* PAGINATION */} + {onPerPageChange && onPageChange && ( + + )} +
    +
    + ); +}; + +export default MuiTableSortable; diff --git a/src/components/mui/sortable-table/styles.module.less b/src/components/mui/sortable-table/styles.module.less new file mode 100644 index 00000000..d056f4b5 --- /dev/null +++ b/src/components/mui/sortable-table/styles.module.less @@ -0,0 +1,14 @@ +.dottedBorderLeft { + position: relative; + border-left: none; + &::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + border-left: 1px dashed #e0e0e0; + height: auto; + margin: 20px 0; + } +} diff --git a/src/components/mui/sponsor-addon-select.js b/src/components/mui/sponsor-addon-select.js new file mode 100644 index 00000000..a4e8a5da --- /dev/null +++ b/src/components/mui/sponsor-addon-select.js @@ -0,0 +1,78 @@ +/** + * 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 { MenuItem, Select } from "@mui/material"; +import PropTypes from "prop-types"; +import { querySponsorAddons } from "../../utils/query-actions"; + +const SponsorAddonSelect = ({ + value, + summitId, + sponsor, + placeholder = "Select...", + onChange, + inputProps = {} +}) => { + const [options, setOptions] = useState([]); + const sponsorshipIds = sponsor.sponsorships.map((e) => e.id); + + useEffect(() => { + querySponsorAddons(summitId, sponsor.id, sponsorshipIds, (results) => { + const normalized = results.map((r) => ({ + value: r.id, + label: r.name + })); + setOptions(normalized); + }); + }, []); + + const handleChange = (ev) => { + const addon = options.find((o) => o.value === ev.target.value); + onChange({ id: addon.value, name: addon.label }); + }; + + return ( + + ); +}; + +SponsorAddonSelect.propTypes = { + value: PropTypes.number, + summitId: PropTypes.number.isRequired, + sponsor: PropTypes.object.isRequired, + placeholder: PropTypes.string, + onChange: PropTypes.func.isRequired +}; + +export default SponsorAddonSelect; diff --git a/src/components/mui/summit-addon-select.js b/src/components/mui/summit-addon-select.js new file mode 100644 index 00000000..92927685 --- /dev/null +++ b/src/components/mui/summit-addon-select.js @@ -0,0 +1,74 @@ +/** + * 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 { MenuItem, Select } from "@mui/material"; +import PropTypes from "prop-types"; +import { querySummitAddons } from "../../utils/query-actions"; + +const SummitAddonSelect = ({ + value, + summitId, + placeholder = "Select...", + onChange, + inputProps = {} +}) => { + const [options, setOptions] = useState([]); + + useEffect(() => { + querySummitAddons(summitId, (results) => { + const normalized = results.map((r) => ({ + value: r, + label: r + })); + setOptions(normalized); + }); + }, []); + + const handleChange = (event) => { + onChange(event.target.value); + }; + + return ( + + ); +}; + +SummitAddonSelect.propTypes = { + value: PropTypes.string, + summitId: PropTypes.number.isRequired, + placeholder: PropTypes.string, + onChange: PropTypes.func.isRequired +}; + +export default SummitAddonSelect; diff --git a/src/components/mui/summits-dropdown.js b/src/components/mui/summits-dropdown.js new file mode 100644 index 00000000..372cf87a --- /dev/null +++ b/src/components/mui/summits-dropdown.js @@ -0,0 +1,58 @@ +/** + * 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 {FormControl, InputLabel, MenuItem, Select} from "@mui/material"; +import T from "i18n-react"; +import {fetchAllSummits} from "../../utils/query-actions"; + +const SummitsDropdown = ({ + onlyActive = false, + label = "Search by show", + onChange, + summits, + excludeSummitIds = [] + }) => { + const [summitOptions, setSummitOptions] = useState(summits); + + useEffect(() => { + if (summits.length === 0) { + fetchAllSummits(onlyActive).then((summits) => { + const summitOptions = summits.filter( + (s) => excludeSummitIds.indexOf(s.id) === -1); + setSummitOptions(summitOptions); + }); + } + }, []); + + + return ( + + {label} + + + ); +}; + +export default SummitsDropdown; diff --git a/src/components/mui/table/extra-rows/NotesRow.jsx b/src/components/mui/table/extra-rows/NotesRow.jsx new file mode 100644 index 00000000..b25cfdf2 --- /dev/null +++ b/src/components/mui/table/extra-rows/NotesRow.jsx @@ -0,0 +1,29 @@ +/** + * 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 TableCell from "@mui/material/TableCell"; +import TableRow from "@mui/material/TableRow"; +import * as React from "react"; +import { Typography } from "@mui/material"; + +const NotesRow = ({ colCount, note }) => ( + + + + {note} + + + + ); + +export default NotesRow; diff --git a/src/components/mui/table/extra-rows/TotalRow.jsx b/src/components/mui/table/extra-rows/TotalRow.jsx new file mode 100644 index 00000000..e72701cd --- /dev/null +++ b/src/components/mui/table/extra-rows/TotalRow.jsx @@ -0,0 +1,45 @@ +/** + * 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 TableCell from "@mui/material/TableCell"; +import TableRow from "@mui/material/TableRow"; +import * as React from "react"; +import T from "i18n-react/dist/i18n-react"; + +const TotalRow = ({ columns, targetCol, total, trailing = 0 }) => { + return ( + + {columns.map((col, i) => { + if (i === 0) + return ( + + {T.translate("mui_table.total")} + + ); + if (col.columnKey === targetCol) + return ( + + {total} + + ); + return ; + })} + {[...Array(trailing)].map((_, i) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + ); +}; + +export default TotalRow; diff --git a/src/components/mui/table/extra-rows/index.js b/src/components/mui/table/extra-rows/index.js new file mode 100644 index 00000000..6fdc4792 --- /dev/null +++ b/src/components/mui/table/extra-rows/index.js @@ -0,0 +1,15 @@ +/** + * 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. + * */ + +export { default as TotalRow } from "./TotalRow"; +export { default as NotesRow } from "./NotesRow"; diff --git a/src/components/mui/table/mui-table.js b/src/components/mui/table/mui-table.js new file mode 100644 index 00000000..ff3831a1 --- /dev/null +++ b/src/components/mui/table/mui-table.js @@ -0,0 +1,308 @@ +/** + * 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 * as React from "react"; +import T from "i18n-react/dist/i18n-react"; +import { isBoolean } from "lodash"; +import { + Box, + Button, + IconButton, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TablePagination, + TableRow, + TableSortLabel +} from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; +import CheckIcon from "@mui/icons-material/Check"; +import CloseIcon from "@mui/icons-material/Close"; +import { visuallyHidden } from "@mui/utils"; +import { + DEFAULT_PER_PAGE, + FIFTY_PER_PAGE, + TWENTY_PER_PAGE +} from "../../../utils/constants"; +import showConfirmDialog from "../showConfirmDialog"; +import styles from "./mui-table.module.less"; + +const ARCHIVED_CELL_SX = { + backgroundColor: "background.light", + color: "text.disabled" +}; + +const MuiTable = ({ + columns = [], + data = [], + children, + totalRows, + perPage, + currentPage, + onPageChange, + onPerPageChange, + onSort, + options = { sortCol: "", sortDir: 1, disableProp: null }, // disableProp is the prop that will disable the row + getName = (item) => item.name, + onEdit, + onArchive, + onDelete, + canDelete = () => true, + deleteDialogTitle = null, + deleteDialogBody = null, + deleteDialogConfirmText = null, + confirmButtonColor = null +}) => { + const handleChangePage = (_, newPage) => { + onPageChange(newPage + 1); + }; + + const handleChangeRowsPerPage = (ev) => { + onPerPageChange(ev.target.value); + }; + + const basePerPageOptions = [ + DEFAULT_PER_PAGE, + TWENTY_PER_PAGE, + FIFTY_PER_PAGE + ]; + + const initialPerPage = React.useRef(perPage); + + let customPerPageOptions = basePerPageOptions.includes(initialPerPage.current) + ? basePerPageOptions + : [...basePerPageOptions, initialPerPage.current].sort((a, b) => a - b); + + // remove per page selection if no action passed + if (!onPerPageChange) { + customPerPageOptions = [initialPerPage.current]; + } + + const { sortCol, sortDir } = options; + + const getArchivedCellSx = (row) => + options.disableProp && row[options.disableProp] ? ARCHIVED_CELL_SX : null; + + const getCellSx = (row, baseSx = {}) => ({ + ...baseSx, + ...(getArchivedCellSx(row) || {}) + }); + + const handleDelete = async (item) => { + const isConfirmed = await showConfirmDialog({ + title: deleteDialogTitle || T.translate("general.are_you_sure"), + text: + typeof deleteDialogBody === "function" + ? deleteDialogBody(getName(item)) + : deleteDialogBody || + `${T.translate("general.row_remove_warning")} ${getName(item)}`, + type: "warning", + showCancelButton: true, + confirmButtonColor: confirmButtonColor || "#DD6B55", + confirmButtonText: + deleteDialogConfirmText || T.translate("general.yes_delete") + }); + + if (isConfirmed) { + onDelete(item.id); + } + }; + + const renderCell = (row, col) => { + if (col.render) { + return col.render(row); + } + + if (isBoolean(row[col.columnKey])) { + return row[col.columnKey] ? ( + + ) : ( + + ); + } + + return row[col.columnKey]; + }; + + return ( + + + + + {/* TABLE HEADER */} + + + {columns.map((col) => ( + + {col.sortable ? ( + onSort(col.columnKey, sortDir * -1)} + > + {col.header} + {sortCol === col.columnKey ? ( + + {sortDir === -1 + ? T.translate("mui_table.sorted_desc") + : T.translate("mui_table.sorted_asc")} + + ) : null} + + ) : ( + col.header + )} + + ))} + {onEdit && } + {onArchive && } + {onDelete && } + + + + {/* TABLE BODY */} + + {data.map((row) => ( + + {/* Main content columns */} + {columns.map((col) => ( + + {renderCell(row, col)} + + ))} + {/* Edit column */} + {onEdit && ( + + onEdit(row)}> + + + + )} + {/* Archive column */} + {onArchive && ( + + + + )} + {/* Delete column */} + {onDelete && ( + + {canDelete(row) && ( + handleDelete(row)} + > + + + )} + + )} + + ))} + {/* Here we inject extra rows passed as children */} + {children} + {data.length === 0 && ( + + + {T.translate("mui_table.no_items")} + + + )} + +
    +
    + + {/* PAGINATION */} + {perPage && currentPage && ( + + )} +
    +
    + ); +}; + +export default MuiTable; diff --git a/src/components/mui/table/mui-table.module.less b/src/components/mui/table/mui-table.module.less new file mode 100644 index 00000000..f77d0c5f --- /dev/null +++ b/src/components/mui/table/mui-table.module.less @@ -0,0 +1,14 @@ +.dottedBorderLeft { + position: relative; + border-left: none; + &::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + border-left: 1px dashed #e0e0e0; + height: 60%; + align-self: center; + } +} diff --git a/src/i18n/en.json b/src/i18n/en.json index c2c3d0d5..e1f0c840 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1,24 +1,83 @@ { - "errors": { - "user_not_auth" : "Hold on. Your user is not authenticated!.", - "user_not_authz" : "Hold on. Your user is not authorized!.", - "session_expired" : "Hold on. Your session expired!.", - "server_error" : "There was a problem with our server, please contact admin." - }, - "general": { - "select_summit" : "Select a Summit...", - "add" : "Add", - "clear" : "Clear", - "remove" : "Remove", - "drop_files" : "Drop images or click to select files to upload.", - "search" : "Search", - "save" : "Save", - "done" : "Done!", - "drag_and_drop" : "Drag and Drop to sort items.", - "other" : "Other", - "type_something" : "Type something and press tab or enter.", - "are_you_sure" : "Are you sure?", - "yes_delete" : "Yes, delete.", - "remove_warning" : "Are you sure you want to delete this?" - } + "errors": { + "user_not_auth": "Hold on. Your user is not authenticated!.", + "user_not_authz": "Hold on. Your user is not authorized!.", + "session_expired": "Hold on. Your session expired!.", + "server_error": "There was a problem with our server, please contact admin." + }, + "general": { + "select_summit": "Select a Summit...", + "add": "Add", + "clear": "Clear", + "remove": "Remove", + "drop_files": "Drop images or click to select files to upload.", + "search": "Search", + "save": "Save", + "done": "Done!", + "unarchive": "Unarchive", + "archive": "Archive", + "settings": "Settings", + "drag_and_drop": "Drag and Drop to sort items.", + "other": "Other", + "type_something": "Type something and press tab or enter.", + "are_you_sure": "Are you sure?", + "yes_delete": "Yes, delete.", + "remove_warning": "Are you sure you want to delete this?", + "select_an_option": "Select an option...", + "all": "All", + "row_remove_warning": "Are you sure you want to delete row ", + "edit": "Edit", + "delete": "Delete", + "n_a": "N/A", + "not_available": "N/A" + }, + "mui_table": { + "no_items": "No items found.", + "no_data": "No data found.", + "rows_per_page": "Rows per page", + "sorted_desc": "sorted descending", + "sorted_asc": "sorted ascending", + "total": "Total" + }, + "meta_fields": { + "delete_value_warning": "Please verify you want to delete the added value", + "is_default": "Is Default?", + "add_value": "Add a value", + "placeholders": { + "name": "Name", + "value": "Value" + } + }, + "rates": { + "early_bird": "Early bird", + "standard_rate": "Standard", + "onsite_rate": "Onsite" + }, + "additional_inputs": { + "title": "Field Title", + "type": "Field Type", + "required": "Required", + "delete_warning": "Are you sure you want to delete meta field ", + "placeholders": { + "title": "Field Title", + "type": "Select...", + "minimum_quantity": "Minimum", + "maximum_quantity": "Maximum" + } + }, + "sponsor_edit_form": { + "code": "Code", + "description": "Description", + "custom_rate": "Custom Rate", + "early_bird_rate": "Early Bird Rate", + "standard_rate": "Standard Rate", + "onsite_rate": "Onsite Rate", + "qty": "Quantity", + "total": "Total", + "notes": "Notes", + "notes_placeholder": "Enter your notes here...", + "additional_info": "Additional Info", + "discount": "Discount", + "total_on_caps": "TOTAL" + } } diff --git a/src/utils/actions.js b/src/utils/actions.js index 1d7ea724..0f8b415d 100644 --- a/src/utils/actions.js +++ b/src/utils/actions.js @@ -29,6 +29,8 @@ export const STOP_LOADING = 'STOP_LOADING'; export const VALIDATE = 'VALIDATE'; export const CLEAR_MESSAGE = 'CLEAR_MESSAGE'; export const SHOW_MESSAGE = 'SHOW_MESSAGE'; +export const SET_SNACKBAR_MESSAGE = "SET_SNACKBAR_MESSAGE"; +export const CLEAR_SNACKBAR_MESSAGE = "CLEAR_SNACKBAR_MESSAGE"; export const createAction = type => payload => ({ type, @@ -39,6 +41,24 @@ export const resetLoading = createAction(RESET_LOADING); export const startLoading = createAction(START_LOADING); export const stopLoading = createAction(STOP_LOADING); +export const clearSnackbarMessage = () => (dispatch) => { + dispatch(createAction(CLEAR_SNACKBAR_MESSAGE)({})); +}; + +export const setSnackbarMessage = (message) => (dispatch) => { + dispatch(createAction(SET_SNACKBAR_MESSAGE)(message)); +}; + +export const snackbarErrorHandler = (err, res) => (dispatch, state) => { + authErrorHandler(err, res, setSnackbarMessage)(dispatch, state); +}; + +export const snackbarSuccessHandler = (message) => (dispatch, state) => + setSnackbarMessage({ ...message, type: "success", code: CODE_200 })( + dispatch, + state + ); + const xhrs = {}; const etagCache = {}; diff --git a/src/utils/constants.js b/src/utils/constants.js index e2505eff..28bf8626 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -3,3 +3,52 @@ export const TWO_DECIMAL_PLACES = 2; export const THREE_DECIMAL_PLACES = 3; export const ONE_CENT = 1n; export const ZERO_INT = 0; + +export const DEBOUNCE_WAIT_250 = 250; +export const DEBOUNCE_WAIT = 500; + +export const NOTIFICATION_TIMEOUT = 2000; +export const DEFAULT_PER_PAGE = 10; +export const TWENTY_PER_PAGE = 20; +export const FIFTY_PER_PAGE = 50; +export const MAX_PER_PAGE = 100; + +export const INT_BASE = 10; + +export const ONE_HUNDRED = 100; +export const MILLISECONDS_IN_SECOND = 1000; + +export const MILLISECONDS_TO_SECONDS = 1000; + +export const BYTES_PER_MB = 1_048_576; // 1024 * 1024 + +export const MAX_INVENTORY_IMAGE_UPLOAD_SIZE = 512000; +export const MAX_INVENTORY_IMAGES_UPLOAD_QTY = 5; +export const ALLOWED_INVENTORY_IMAGE_FORMATS = ["jpg", "jpeg", "png"]; + +export const METAFIELD_TYPES_WITH_OPTIONS = [ + "CheckBoxList", + "ComboBox", + "RadioButtonList" +]; + +export const METAFIELD_TYPES = [ + "CheckBox", + ...METAFIELD_TYPES_WITH_OPTIONS, + "Text", + "TextArea", + "Quantity", + "DateTime", + "Time" +]; + +export const DISCOUNT_TYPES = { + AMOUNT: "Amount", + RATE: "Rate" +}; + +export const RATE_FIELDS = { + EARLY_BIRD: "early_bird_rate", + STANDARD: "standard_rate", + ONSITE: "onsite_rate" +}; diff --git a/src/utils/methods.js b/src/utils/methods.js index fb3a51a4..1b88802f 100644 --- a/src/utils/methods.js +++ b/src/utils/methods.js @@ -294,3 +294,6 @@ export const convertSVGtoImg = async (svgUrl) => { console.log(url, newWidth, newHeight); return {url, width: newWidth, height: newHeight} } + +export const isRateEnabled = (value) => + value !== null && value !== undefined && value !== ""; diff --git a/src/utils/query-actions.js b/src/utils/query-actions.js index c41084e8..388bfd7a 100644 --- a/src/utils/query-actions.js +++ b/src/utils/query-actions.js @@ -15,11 +15,13 @@ import { fetchErrorHandler, fetchResponseHandler, escapeFilterValue } from "./ac import { getAccessToken } from '../components/security/methods'; import { buildAPIBaseUrl } from "./methods"; import _ from 'lodash'; +import moment from 'moment-timezone' + export const RECEIVE_COUNTRIES = 'RECEIVE_COUNTRIES'; -const callDelay = 500; // milliseconds import URI from "urijs"; +import {DEBOUNCE_WAIT, DEFAULT_PER_PAGE, MAX_PER_PAGE} from "./constants"; + URI.escapeQuerySpace = false; -export const DEFAULT_PAGE_SIZE = 10; const _fetchPublic = async (endpoint, callback, options = {}) => { return fetch(buildAPIBaseUrl(endpoint.toString()), options) @@ -36,6 +38,22 @@ const _fetchPublic = async (endpoint, callback, options = {}) => { .catch(fetchErrorHandler); } +const _fetchPromise = async (endpoint, options = {}) => { + let accessToken; + + try { + accessToken = await getAccessToken(); + } catch (e) { + return Promise.reject(); + } + + endpoint.addQuery('access_token', accessToken); + + return fetch(buildAPIBaseUrl(endpoint.toString()), options) + .then(fetchResponseHandler) + .catch(fetchErrorHandler); +} + /** * @param endpoint * @param callback @@ -64,7 +82,7 @@ const _fetch = async (endpoint, callback, options = {}) => { * * @type {DebouncedFunc<(function(*, *, *=): Promise)|*>} */ -export const queryMembers = _.debounce(async (input, callback, per_page= DEFAULT_PAGE_SIZE) => { +export const queryMembers = _.debounce(async (input, callback, per_page= DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/members`); @@ -80,13 +98,13 @@ export const queryMembers = _.debounce(async (input, callback, per_page= DEFAULT _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * * @type {DebouncedFunc<(function(*, *, *=): Promise)|*>} */ -export const queryAttendees = _.debounce(async (summitId, input, callback, per_page= DEFAULT_PAGE_SIZE) => { +export const queryAttendees = _.debounce(async (summitId, input, callback, per_page= DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/summits/${summitId}/attendees`); @@ -101,12 +119,12 @@ export const queryAttendees = _.debounce(async (summitId, input, callback, per_p _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *=): Promise)|*>} */ -export const querySummits = _.debounce(async (input, callback, per_page= DEFAULT_PAGE_SIZE) => { +export const querySummits = _.debounce(async (input, callback, per_page= DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/summits/all`); @@ -122,12 +140,33 @@ export const querySummits = _.debounce(async (input, callback, per_page= DEFAULT _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); + + +export const fetchAllSummits = async (onlyActive) => { + + let endpoint = URI(`/api/v2/summits/all`); + + endpoint.addQuery('fields', 'id,name,start_date,end_date'); + endpoint.addQuery('expand', 'none'); + endpoint.addQuery('relations', 'none'); + endpoint.addQuery('order','-start_date'); + endpoint.addQuery('page', 1); + endpoint.addQuery('per_page', MAX_PER_PAGE); + + if (onlyActive) { + const now = moment().tz("UTC").unix(); + endpoint.addQuery('filter[]', `end_date<=${now}`); + } + + return _fetchPromise(endpoint, callback) + .then((json) => json.data); +}; /** * @type {DebouncedFunc<(function(*, *, *, *=): Promise)|*>} */ -export const querySpeakers = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PAGE_SIZE ) => { +export const querySpeakers = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PER_PAGE ) => { let endpoint = URI(`/api/v1/${summitId ? `summits/${summitId}/speakers`:`speakers`}`); @@ -144,7 +183,7 @@ export const querySpeakers = _.debounce(async (summitId, input, callback, per_pa _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *, *=): Promise)|*>} @@ -167,12 +206,12 @@ export const queryTags = _.debounce(async (summitId, input, callback, per_page = _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *, *=): Promise)|*>} */ -export const queryTracks = _.debounce(async (summitId, input, callback, excludedIds = [], per_page = DEFAULT_PAGE_SIZE) => { +export const queryTracks = _.debounce(async (summitId, input, callback, excludedIds = [], per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/summits/${summitId}/tracks`); @@ -190,12 +229,12 @@ export const queryTracks = _.debounce(async (summitId, input, callback, excluded } _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *, *=): Promise)|*>} */ -export const queryTrackGroups = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PAGE_SIZE) => { +export const queryTrackGroups = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/summits/${summitId}/track-groups`); @@ -210,12 +249,12 @@ export const queryTrackGroups = _.debounce(async (summitId, input, callback, per _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *=, *): Promise)|*>} */ -export const queryEvents = _.debounce(async (summitId, input, onlyPublished = false, callback, per_page = DEFAULT_PAGE_SIZE) => { +export const queryEvents = _.debounce(async (summitId, input, onlyPublished = false, callback, per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/summits/${summitId}/events` + (onlyPublished ? '/published' : '')); @@ -229,12 +268,12 @@ export const queryEvents = _.debounce(async (summitId, input, onlyPublished = fa } _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *, *=, *=): Promise)|*>} */ -export const queryEventTypes = _.debounce(async (summitId, input, callback, eventTypeClassName = null, per_page = DEFAULT_PAGE_SIZE) => { +export const queryEventTypes = _.debounce(async (summitId, input, callback, eventTypeClassName = null, per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/summits/${summitId}/event-types`); @@ -254,13 +293,13 @@ export const queryEventTypes = _.debounce(async (summitId, input, callback, even _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *=): Promise)|*>} */ -export const queryGroups = _.debounce(async (input, callback, per_page = DEFAULT_PAGE_SIZE) => { +export const queryGroups = _.debounce(async (input, callback, per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/groups`); @@ -275,12 +314,12 @@ export const queryGroups = _.debounce(async (input, callback, per_page = DEFAULT _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *=): Promise)|*>} */ -export const queryCompanies = _.debounce(async (input, callback, per_page = DEFAULT_PAGE_SIZE) => { +export const queryCompanies = _.debounce(async (input, callback, per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/companies`); @@ -294,12 +333,12 @@ export const queryCompanies = _.debounce(async (input, callback, per_page = DEFA } _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *, *=): Promise)|*>} */ -export const queryRegistrationCompanies = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PAGE_SIZE) => { +export const queryRegistrationCompanies = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/summits/${summitId}/registration-companies`); @@ -314,12 +353,12 @@ export const queryRegistrationCompanies = _.debounce(async (summitId, input, cal _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *, *=): Promise)|*>} */ -export const querySponsors = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PAGE_SIZE) => { +export const querySponsors = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/summits/${summitId}/sponsors`); @@ -335,12 +374,35 @@ export const querySponsors = _.debounce(async (summitId, input, callback, per_pa _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); + +export const querySponsorsV2 = _.debounce(async (input, summitId, callback) => { + const endpoint = URI( + `/api/v2/summits/${summitId}/sponsors` + ); + const escapedInput = escapeFilterValue(input); + endpoint.addQuery("fields", "id,company.name,company.id"); + endpoint.addQuery("relations", "company"); + endpoint.addQuery("expand", "company"); + if (escapedInput) { + endpoint.addQuery("filter", `company_name=@${escapedInput}`); + } + _fetch(endpoint) + .then(fetchResponseHandler) + .then((json) => { + const options = [...json.data].map((sp) => ({ + id: sp.id, + name: sp.company.name + })); + callback(options); + }) + .catch(fetchErrorHandler); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *, *=): Promise)|*>} */ -export const querySponsorsWithBadgeScans = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PAGE_SIZE) => { +export const querySponsorsWithBadgeScans = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/summits/${summitId}/sponsors`); @@ -359,12 +421,12 @@ export const querySponsorsWithBadgeScans = _.debounce(async (summitId, input, ca _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *, *=): Promise)|*>} */ -export const queryAccessLevels = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PAGE_SIZE) => { +export const queryAccessLevels = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/summits/${summitId}/access-level-types`); @@ -379,12 +441,12 @@ export const queryAccessLevels = _.debounce(async (summitId, input, callback, pe _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *=): Promise)|*>} */ -export const queryOrganizations = _.debounce(async (input, callback, per_page = DEFAULT_PAGE_SIZE) => { +export const queryOrganizations = _.debounce(async (input, callback, per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/v1/organizations`); @@ -399,7 +461,7 @@ export const queryOrganizations = _.debounce(async (input, callback, per_page = _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); export const getLanguageList = (callback, signal) => { return _fetchPublic(new URI(`/api/public/v1/languages`), callback, { signal }); @@ -451,7 +513,7 @@ export const geoCodeLatLng = (lat, lng) => { /** * @type {DebouncedFunc<(function(*, *=, *, *=, *=): Promise)|*>} */ -export const queryTicketTypes = _.debounce(async (summitId, filters = {}, callback, version = 'v1', per_page = DEFAULT_PAGE_SIZE) => { +export const queryTicketTypes = _.debounce(async (summitId, filters = {}, callback, version = 'v1', per_page = DEFAULT_PER_PAGE) => { let endpoint = URI(`/api/${version}/summits/${summitId}/ticket-types`); @@ -473,12 +535,12 @@ export const queryTicketTypes = _.debounce(async (summitId, filters = {}, callba _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *=): Promise)|*>} */ -export const querySponsoredProjects = _.debounce(async (input, callback, per_page = DEFAULT_PAGE_SIZE) => { +export const querySponsoredProjects = _.debounce(async (input, callback, per_page = DEFAULT_PER_PAGE) => { const endpoint = URI(`/api/v1/sponsored-projects`); @@ -494,12 +556,12 @@ export const querySponsoredProjects = _.debounce(async (input, callback, per_pag _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); /** * @type {DebouncedFunc<(function(*, *, *, *=): Promise)|*>} */ -export const queryPromocodes = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PAGE_SIZE, extraFilters = []) => { +export const queryPromocodes = _.debounce(async (summitId, input, callback, per_page = DEFAULT_PER_PAGE, extraFilters = []) => { let endpoint = URI(`/api/v1/summits/${summitId}/promo-codes`); @@ -520,4 +582,102 @@ export const queryPromocodes = _.debounce(async (summitId, input, callback, per_ _fetch(endpoint, callback); -}, callDelay); +}, DEBOUNCE_WAIT); + + +export const querySponsorAddons = async ( + summitId, + sponsorId, + sponsorshipIds, + callback +) => { + try { + const promises = sponsorshipIds.map((sponsorshipId) => { + const endpoint = URI( + `/api/v1/summits/${summitId}/sponsors/${sponsorId}/sponsorships/${sponsorshipId}/add-ons` + ); + endpoint.addQuery( + "fields", + "id,name,sponsorship.type,sponsorship.type.id,sponsorship.type.type.name" + ); + endpoint.addQuery( + "expand", + "sponsorship,sponsorship.type,sponsorship.type.type" + ); + endpoint.addQuery("relations", "sponsorship.none"); + return _fetch(endpoint) + .then(fetchResponseHandler) + .then((json) => json.data) + .catch((error) => { + fetchErrorHandler(error); + return []; + }); + }); + const results = await Promise.all(promises); + const allAddons = results.flat(); + callback(allAddons); + } catch (error) { + fetchErrorHandler(error); + } +}; + + +export const querySummitAddons = async ( + summitId, + callback +) => { + const endpoint = URI( + `/api/v1/summits/${summitId}/add-ons/metadata` + ); + endpoint.addQuery("page", 1); + endpoint.addQuery("per_page", MAX_PER_PAGE); + + return _fetch(endpoint) + .then(fetchResponseHandler) + .then((data) => callback(data)) + .catch((error) => { + fetchErrorHandler(error); + return []; + }); +}; + + +export const querySponsorships = _.debounce(async (input, callback) => { + const endpoint = URI(`/api/v1/sponsorship-types`); + input = escapeFilterValue(input); + if (input) { + endpoint.addQuery("filter", `name=@${input}`); + } + _fetch(endpoint) + .then(fetchResponseHandler) + .then((json) => { + const options = [...json.data]; + callback(options); + }) + .catch(fetchErrorHandler); +}, DEBOUNCE_WAIT); + + +export const querySponsorshipsBySummit = _.debounce( + async (input, summitId, callback) => { + const endpoint = URI( + `/api/v1/summits/${summitId}/sponsorships-types` + ); + input = escapeFilterValue(input); + endpoint.addQuery("page", 1); + endpoint.addQuery("per_page", MAX_PER_PAGE); + endpoint.addQuery("expand", "type"); + endpoint.addQuery("order", "+name"); + if (input) { + endpoint.addQuery("filter", `name=@${input}`); + } + _fetch(endpoint) + .then(fetchResponseHandler) + .then((json) => { + const options = [...json.data]; + callback(options); + }) + .catch(fetchErrorHandler); + }, + DEBOUNCE_WAIT +); diff --git a/src/utils/reducers.js b/src/utils/reducers.js index df4d1f3d..178838a5 100644 --- a/src/utils/reducers.js +++ b/src/utils/reducers.js @@ -15,6 +15,8 @@ import { CLEAR_MESSAGE, SHOW_MESSAGE, STOP_LOADING, + SET_SNACKBAR_MESSAGE, + CLEAR_SNACKBAR_MESSAGE } from './actions'; const DEFAULT_STATE = { @@ -39,6 +41,13 @@ export const genericReducers = function ( state = DEFAULT_STATE, action = {}) { case STOP_LOADING: return { ...state, loading: false }; + case SET_SNACKBAR_MESSAGE: { + return { ...state, snackbarMessage: payload }; + } + case CLEAR_SNACKBAR_MESSAGE: { + return { ...state, snackbarMessage: DEFAULT_STATE.snackbarMessage }; + } + default: return state; } diff --git a/webpack.common.js b/webpack.common.js index 6fc5e8c1..7b9ecd99 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -78,6 +78,55 @@ module.exports = { 'components/inputs/editor-input-v2' : './src/components/inputs/editor-input-v2.js', 'components/inputs/editor-input-v3' : './src/components/inputs/editor-input-v3.js', + // mui components + 'components/mui/checkbox-list': './src/components/mui/checkbox-list.js', + 'components/mui/chip-list': './src/components/mui/chip-list.js', + 'components/mui/chip-notify': './src/components/mui/chip-notify.js', + 'components/mui/chip-select-input': './src/components/mui/chip-select-input.js', + 'components/mui/confirm-dialog': './src/components/mui/confirm-dialog.js', + 'components/mui/custom-alert': './src/components/mui/custom-alert.js', + 'components/mui/dnd-list': './src/components/mui/dnd-list.js', + 'components/mui/dropdown-checkbox': './src/components/mui/dropdown-checkbox.js', + 'components/mui/menu-button': './src/components/mui/menu-button.js', + 'components/mui/search-input': './src/components/mui/search-input.js', + 'components/mui/show-confirm-dialog': './src/components/mui/showConfirmDialog.js', + 'components/mui/sponsor-addon-select': './src/components/mui/sponsor-addon-select.js', + 'components/mui/summit-addon-select': './src/components/mui/summit-addon-select.js', + 'components/mui/summits-dropdown': './src/components/mui/summits-dropdown.js', + 'components/mui/form-item-table': './src/components/mui/FormItemTable/index.js', + 'components/mui/item-settings-modal': './src/components/mui/ItemSettingsModal/index.js', + 'components/mui/notes-modal': './src/components/mui/NotesModal/index.js', + 'components/mui/snackbar-notification': './src/components/mui/SnackbarNotification/index.js', + 'components/mui/infinite-table': './src/components/mui/infinite-table/index.js', + 'components/mui/editable-table': './src/components/mui/editable-table/mui-table-editable.js', + 'components/mui/sortable-table': './src/components/mui/sortable-table/mui-table-sortable.js', + 'components/mui/table': './src/components/mui/table/mui-table.js', + 'components/mui/formik-inputs/additional-input': './src/components/mui/formik-inputs/additional-input/additional-input.js', + 'components/mui/formik-inputs/additional-input-list': './src/components/mui/formik-inputs/additional-input/additional-input-list.js', + 'components/mui/formik-inputs/async-select': './src/components/mui/formik-inputs/mui-formik-async-select.js', + 'components/mui/formik-inputs/checkbox-group': './src/components/mui/formik-inputs/mui-formik-checkbox-group.js', + 'components/mui/formik-inputs/checkbox': './src/components/mui/formik-inputs/mui-formik-checkbox.js', + 'components/mui/formik-inputs/datepicker': './src/components/mui/formik-inputs/mui-formik-datepicker.js', + 'components/mui/formik-inputs/discount-field': './src/components/mui/formik-inputs/mui-formik-discountfield.js', + 'components/mui/formik-inputs/dropdown-checkbox': './src/components/mui/formik-inputs/mui-formik-dropdown-checkbox.js', + 'components/mui/formik-inputs/dropdown-radio': './src/components/mui/formik-inputs/mui-formik-dropdown-radio.js', + 'components/mui/formik-inputs/file-size-field': './src/components/mui/formik-inputs/mui-formik-file-size-field.js', + 'components/mui/formik-inputs/price-field': './src/components/mui/formik-inputs/mui-formik-pricefield.js', + 'components/mui/formik-inputs/quantity-field': './src/components/mui/formik-inputs/mui-formik-quantity-field.js', + 'components/mui/formik-inputs/radio-group': './src/components/mui/formik-inputs/mui-formik-radio-group.js', + 'components/mui/formik-inputs/select-group': './src/components/mui/formik-inputs/mui-formik-select-group.js', + 'components/mui/formik-inputs/select': './src/components/mui/formik-inputs/mui-formik-select.js', + 'components/mui/formik-inputs/summit-addon-select': './src/components/mui/formik-inputs/mui-formik-summit-addon-select.js', + 'components/mui/formik-inputs/switch': './src/components/mui/formik-inputs/mui-formik-switch.js', + 'components/mui/formik-inputs/textfield': './src/components/mui/formik-inputs/mui-formik-textfield.js', + 'components/mui/formik-inputs/timepicker': './src/components/mui/formik-inputs/mui-formik-timepicker.js', + 'components/mui/formik-inputs/upload': './src/components/mui/formik-inputs/mui-formik-upload.js', + 'components/mui/formik-inputs/company-input': './src/components/mui/formik-inputs/company-input-mui.js', + 'components/mui/formik-inputs/item-price-tiers': './src/components/mui/formik-inputs/item-price-tiers.js', + 'components/mui/formik-inputs/sponsor-input': './src/components/mui/formik-inputs/mui-sponsor-input.js', + 'components/mui/formik-inputs/sponsorship-input': './src/components/mui/formik-inputs/sponsorship-input-mui.js', + 'components/mui/formik-inputs/sponsorship-summit-select': './src/components/mui/formik-inputs/sponsorship-summit-select-mui.js', + // models 'models/index': './src/models', 'models/summit-event' : './src/models/summit-event.js', diff --git a/yarn.lock b/yarn.lock index 09f736a1..f1efa948 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adobe/css-tools@^4.0.1": + version "4.4.4" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.4.tgz#2856c55443d3d461693f32d2b96fb6ea92e1ffa9" + integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== + "@ampproject/remapping@^2.1.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -17,6 +22,15 @@ dependencies: "@babel/highlight" "^7.16.7" +"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.27.1": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.17.10": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" @@ -277,6 +291,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + "@babel/helper-validator-option@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" @@ -1020,7 +1039,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.18.3", "@babel/runtime@^7.23.9", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.18.3", "@babel/runtime@^7.8.7": version "7.27.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.4.tgz#a91ec580e6c00c67118127777c316dfd5a5a6abf" integrity sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA== @@ -1039,6 +1058,11 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e" + integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g== + "@babel/runtime@^7.28.6": version "7.28.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" @@ -1338,6 +1362,11 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/diff-sequences@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz#25b0818d3d83f00b9c7b04e069b8810f9014b143" + integrity sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA== + "@jest/environment@^28.1.0": version "28.1.0" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-28.1.0.tgz#dedf7d59ec341b9292fcf459fd0ed819eb2e228a" @@ -1348,6 +1377,13 @@ "@types/node" "*" jest-mock "^28.1.0" +"@jest/expect-utils@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.3.0.tgz#c45b2da9802ffed33bf43b3e019ddb95e5ad95e8" + integrity sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA== + dependencies: + "@jest/get-type" "30.1.0" + "@jest/expect-utils@^28.1.0": version "28.1.0" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-28.1.0.tgz#a5cde811195515a9809b96748ae8bcc331a3538a" @@ -1375,6 +1411,11 @@ jest-mock "^28.1.0" jest-util "^28.1.0" +"@jest/get-type@30.1.0": + version "30.1.0" + resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.1.0.tgz#4fcb4dc2ebcf0811be1c04fd1cb79c2dba431cbc" + integrity sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA== + "@jest/globals@^28.1.0": version "28.1.0" resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-28.1.0.tgz#a4427d2eb11763002ff58e24de56b84ba79eb793" @@ -1384,6 +1425,14 @@ "@jest/expect" "^28.1.0" "@jest/types" "^28.1.0" +"@jest/pattern@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.0.1.tgz#d5304147f49a052900b4b853dedb111d080e199f" + integrity sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA== + dependencies: + "@types/node" "*" + jest-regex-util "30.0.1" + "@jest/reporters@^28.1.0": version "28.1.0" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-28.1.0.tgz#5183a28b9b593b6000fa9b89b031c7216b58a9a0" @@ -1414,6 +1463,13 @@ terminal-link "^2.0.0" v8-to-istanbul "^9.0.0" +"@jest/schemas@30.0.5": + version "30.0.5" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.0.5.tgz#7bdf69fc5a368a5abdb49fd91036c55225846473" + integrity sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA== + dependencies: + "@sinclair/typebox" "^0.34.0" + "@jest/schemas@^28.0.2": version "28.0.2" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.0.2.tgz#08c30df6a8d07eafea0aef9fb222c5e26d72e613" @@ -1478,6 +1534,19 @@ slash "^3.0.0" write-file-atomic "^4.0.1" +"@jest/types@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.3.0.tgz#cada800d323cb74945c24ac74615fdb312a6c85f" + integrity sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw== + dependencies: + "@jest/pattern" "30.0.1" + "@jest/schemas" "30.0.5" + "@types/istanbul-lib-coverage" "^2.0.6" + "@types/istanbul-reports" "^3.0.4" + "@types/node" "*" + "@types/yargs" "^17.0.33" + chalk "^4.1.2" + "@jest/types@^28.1.0": version "28.1.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.0.tgz#508327a89976cbf9bd3e1cc74641a29fd7dfd519" @@ -1607,86 +1676,128 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== -"@mui/core-downloads-tracker@^5.17.1": - version "5.17.1" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.17.1.tgz#49b88ecb68b800431b5c2f2bfb71372d1f1478fa" - integrity sha512-OcZj+cs6EfUD39IoPBOgN61zf1XFVY+imsGoBDwXeSq2UHJZE3N59zzBOVjclck91Ne3e9gudONOeILvHCIhUA== +"@mui/core-downloads-tracker@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz#e9f7049d7e7bb1ee05839f7a0ce813755f137432" + integrity sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q== -"@mui/icons-material@^7.3.9": - version "7.3.9" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-7.3.9.tgz#4f6dc62bfe8954f3848b0eecb3650cff10f6a7ec" - integrity sha512-BT+zPJXss8Hg/oEMRmHl17Q97bPACG4ufFSfGEdhiE96jOyR5Dz1ty7ZWt1fVGR0y1p+sSgEwQT/MNZQmoWDCw== +"@mui/icons-material@^6.4.3": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-6.5.0.tgz#26bfa7c8574cc4e57c2f2835bfd6b1efa7f310fa" + integrity sha512-VPuPqXqbBPlcVSA0BmnoE4knW4/xG6Thazo8vCLWkOKusko6DtwFV6B665MMWJ9j0KFohTIf3yx2zYtYacvG1g== dependencies: - "@babel/runtime" "^7.28.6" + "@babel/runtime" "^7.26.0" -"@mui/material@^5.15.20": - version "5.17.1" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.17.1.tgz#596f542a51fc74db75da2df66565b4874ce4049d" - integrity sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw== - dependencies: - "@babel/runtime" "^7.23.9" - "@mui/core-downloads-tracker" "^5.17.1" - "@mui/system" "^5.17.1" - "@mui/types" "~7.2.15" - "@mui/utils" "^5.17.1" +"@mui/material@^6.4.3": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.5.0.tgz#c7eccfe260030433c51b7aec17574bae4504cacc" + integrity sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow== + dependencies: + "@babel/runtime" "^7.26.0" + "@mui/core-downloads-tracker" "^6.5.0" + "@mui/system" "^6.5.0" + "@mui/types" "~7.2.24" + "@mui/utils" "^6.4.9" "@popperjs/core" "^2.11.8" - "@types/react-transition-group" "^4.4.10" - clsx "^2.1.0" + "@types/react-transition-group" "^4.4.12" + clsx "^2.1.1" csstype "^3.1.3" prop-types "^15.8.1" react-is "^19.0.0" react-transition-group "^4.4.5" -"@mui/private-theming@^5.17.1": - version "5.17.1" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.17.1.tgz#b4b6fbece27830754ef78186e3f1307dca42f295" - integrity sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ== +"@mui/private-theming@^6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-6.4.9.tgz#0c1d65a638a1740aad0eb715d79e76471abe8175" + integrity sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw== dependencies: - "@babel/runtime" "^7.23.9" - "@mui/utils" "^5.17.1" + "@babel/runtime" "^7.26.0" + "@mui/utils" "^6.4.9" prop-types "^15.8.1" -"@mui/styled-engine@^5.16.14": - version "5.16.14" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.16.14.tgz#f90fef5b4f8ebf11d48e1b1df8854a45bb31a9f5" - integrity sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw== +"@mui/styled-engine@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.5.0.tgz#cf9b3e706517f5f2989df92d2aea0d2917a77c8a" + integrity sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw== dependencies: - "@babel/runtime" "^7.23.9" + "@babel/runtime" "^7.26.0" "@emotion/cache" "^11.13.5" + "@emotion/serialize" "^1.3.3" + "@emotion/sheet" "^1.4.0" csstype "^3.1.3" prop-types "^15.8.1" -"@mui/system@^5.17.1": - version "5.17.1" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.17.1.tgz#1f987cce91bf738545a8cf5f99152cd2728e6077" - integrity sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg== - dependencies: - "@babel/runtime" "^7.23.9" - "@mui/private-theming" "^5.17.1" - "@mui/styled-engine" "^5.16.14" - "@mui/types" "~7.2.15" - "@mui/utils" "^5.17.1" - clsx "^2.1.0" +"@mui/system@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.5.0.tgz#52751ac4e3a546f53bc34fd2ef2731c28a824b92" + integrity sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w== + dependencies: + "@babel/runtime" "^7.26.0" + "@mui/private-theming" "^6.4.9" + "@mui/styled-engine" "^6.5.0" + "@mui/types" "~7.2.24" + "@mui/utils" "^6.4.9" + clsx "^2.1.1" csstype "^3.1.3" prop-types "^15.8.1" -"@mui/types@~7.2.15": +"@mui/types@^7.4.12": + version "7.4.12" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.4.12.tgz#e4eba37a7506419ea5c5e0604322ba82b271bf46" + integrity sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w== + dependencies: + "@babel/runtime" "^7.28.6" + +"@mui/types@~7.2.24": version "7.2.24" resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.24.tgz#5eff63129d9c29d80bbf2d2e561bd0690314dec2" integrity sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw== -"@mui/utils@^5.17.1": - version "5.17.1" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.17.1.tgz#72ba4ffa79f7bdf69d67458139390f18484b6e6b" - integrity sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg== +"@mui/utils@^5.16.6 || ^6.0.0 || ^7.0.0": + version "7.3.9" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-7.3.9.tgz#8af5093fc93c2e582fa3d047f561c7b690509bc2" + integrity sha512-U6SdZaGbfb65fqTsH3V5oJdFj9uYwyLE2WVuNvmbggTSDBb8QHrFsqY8BN3taK9t3yJ8/BPHD/kNvLNyjwM7Yw== dependencies: - "@babel/runtime" "^7.23.9" - "@mui/types" "~7.2.15" - "@types/prop-types" "^15.7.12" + "@babel/runtime" "^7.28.6" + "@mui/types" "^7.4.12" + "@types/prop-types" "^15.7.15" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^19.2.3" + +"@mui/utils@^6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.9.tgz#b0df01daa254c7c32a1a30b30a5179e19ef071a7" + integrity sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg== + dependencies: + "@babel/runtime" "^7.26.0" + "@mui/types" "~7.2.24" + "@types/prop-types" "^15.7.14" clsx "^2.1.1" prop-types "^15.8.1" react-is "^19.0.0" +"@mui/x-date-pickers@^7.26.0": + version "7.29.4" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.29.4.tgz#b8808cb8e28c1d4e528b37b336effc8074e65faf" + integrity sha512-wJ3tsqk/y6dp+mXGtT9czciAMEO5Zr3IIAHg9x6IL0Eqanqy0N3chbmQQZv3iq0m2qUpQDLvZ4utZBUTJdjNzw== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" + "@mui/x-internals" "7.29.0" + "@types/react-transition-group" "^4.4.11" + clsx "^2.1.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + +"@mui/x-internals@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.29.0.tgz#1f353b697ed1bf5594ac549556ade2e6841f4bf5" + integrity sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" + "@npmcli/fs@^1.0.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" @@ -1871,6 +1982,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== +"@sinclair/typebox@^0.34.0": + version "0.34.49" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.49.tgz#4f1369234f2ecf693866476c3b2e1b54d2a9d68e" + integrity sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -1892,6 +2008,49 @@ dependencies: tslib "^2.4.0" +"@testing-library/dom@^8.0.0": + version "8.20.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f" + integrity sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.1.3" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz#5e97c8f9a15ccf4656da00fecab505728de81e0c" + integrity sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg== + dependencies: + "@adobe/css-tools" "^4.0.1" + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.5.6" + lodash "^4.17.15" + redent "^3.0.0" + +"@testing-library/react@12.1.5": + version "12.1.5" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" + integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.0.0" + "@types/react-dom" "<18.0.0" + +"@testing-library/user-event@14.5.2": + version "14.5.2" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" + integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ== + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -1907,6 +2066,11 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/babel__core@^7.1.14": version "7.1.19" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" @@ -2002,6 +2166,13 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": + version "3.3.7" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz#306e3a3a73828522efa1341159da4846e7573a6c" + integrity sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g== + dependencies: + hoist-non-react-statics "^3.3.0" + "@types/http-errors@*": version "2.0.5" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" @@ -2019,6 +2190,11 @@ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== +"@types/istanbul-lib-coverage@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + "@types/istanbul-lib-report@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" @@ -2033,6 +2209,21 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@*": + version "30.0.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-30.0.0.tgz#5e85ae568006712e4ad66f25433e9bdac8801f1d" + integrity sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA== + dependencies: + expect "^30.0.0" + pretty-format "^30.0.0" + "@types/jsdom@^16.2.4": version "16.2.14" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.14.tgz#26fe9da6a8870715b154bb84cd3b2e53433d8720" @@ -2089,10 +2280,10 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.1.tgz#76e72d8a775eef7ce649c63c8acae1a0824bbaed" integrity sha512-XFjFHmaLVifrAKaZ+EKghFHtHSUonyw8P2Qmy2/+osBnrKbH9UYtlK10zg8/kCt47MFilll/DEDKy3DHfJ0URw== -"@types/prop-types@^15.7.12": - version "15.7.14" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" - integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== +"@types/prop-types@^15.7.14", "@types/prop-types@^15.7.15": + version "15.7.15" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== "@types/qs@*": version "6.9.7" @@ -2104,11 +2295,33 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-transition-group@^4.4.10": +"@types/react-dom@<18.0.0": + version "17.0.26" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.26.tgz#fa7891ba70fd39ddbaa7e85b6ff9175bb546bc1b" + integrity sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg== + +"@types/react-redux@^7.1.20": + version "7.1.34" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.34.tgz#83613e1957c481521e6776beeac4fd506d11bd0e" + integrity sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react-transition-group@^4.4.11", "@types/react-transition-group@^4.4.12": version "4.4.12" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== +"@types/react@*": + version "19.2.14" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" + integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== + dependencies: + csstype "^3.2.2" + "@types/retry@0.12.2": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" @@ -2150,6 +2363,18 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/stack-utils@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + +"@types/testing-library__jest-dom@^5.9.1": + version "5.14.9" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz#0fb1e6a0278d87b6737db55af5967570b67cb466" + integrity sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw== + dependencies: + "@types/jest" "*" + "@types/tough-cookie@*": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" @@ -2167,6 +2392,13 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== +"@types/yargs@^17.0.33": + version "17.0.35" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.35.tgz#07013e46aa4d7d7d50a49e15604c1c5340d4eb24" + integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^17.0.8": version "17.0.10" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.10.tgz#591522fce85d8739bca7b8bb90d048e4478d186a" @@ -2558,7 +2790,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: +ansi-styles@^5.0.0, ansi-styles@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== @@ -2612,6 +2844,18 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-query@5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== + dependencies: + deep-equal "^2.0.5" + +aria-query@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== + arr-diff@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" @@ -2634,6 +2878,14 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== +array-buffer-byte-length@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== + dependencies: + call-bound "^1.0.3" + is-array-buffer "^3.0.5" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -2739,6 +2991,13 @@ autobind-decorator@^2.4.0: resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-2.4.0.tgz#ea9e1c98708cf3b5b356f7cf9f10f265ff18239c" integrity sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + awesome-bootstrap-checkbox@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/awesome-bootstrap-checkbox/-/awesome-bootstrap-checkbox-1.0.1.tgz#dab10146b6001129ab0a0ec1e54bb77c6c30457a" @@ -3316,7 +3575,7 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== @@ -3332,7 +3591,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" -call-bound@^1.0.2: +call-bind@^1.0.5, call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== @@ -3414,7 +3683,15 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.2: +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3514,6 +3791,11 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.1.tgz#58331f6f472a25fe3a50a351ae3052936c2c7f32" integrity sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg== +ci-info@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" + integrity sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg== + cjs-module-lexer@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" @@ -3572,7 +3854,7 @@ clone@^2.1.2: resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== -clsx@^2.1.0, clsx@^2.1.1: +clsx@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== @@ -3866,6 +4148,13 @@ crypto-js@^4.0.0, crypto-js@^4.1.1: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-declaration-sorter@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec" @@ -3936,6 +4225,11 @@ css-what@^6.0.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4024,6 +4318,11 @@ csstype@^3.0.2, csstype@^3.1.3: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -4089,11 +4388,40 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-equal@^2.0.5: + version "2.2.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" + integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.5" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.2" + is-arguments "^1.1.1" + is-array-buffer "^3.0.2" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.13" + deep-is@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" @@ -4112,6 +4440,15 @@ default-browser@^5.2.1: bundle-name "^4.1.0" default-browser-id "^5.0.0" +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" @@ -4125,6 +4462,15 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -4228,6 +4574,11 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + dom-helpers@^3.2.0, dom-helpers@^3.2.1, dom-helpers@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" @@ -4602,7 +4953,7 @@ es-array-method-boxes-properly@^1.0.0: resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== -es-define-property@^1.0.1: +es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== @@ -4612,6 +4963,21 @@ es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +es-get-iterator@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + es-module-lexer@^1.2.1: version "1.5.4" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" @@ -4795,6 +5161,18 @@ expect@^28.1.0: jest-message-util "^28.1.0" jest-util "^28.1.0" +expect@^30.0.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-30.3.0.tgz#1b82111517d1ab030f3db0cf1b4061c8aa644f61" + integrity sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q== + dependencies: + "@jest/expect-utils" "30.3.0" + "@jest/get-type" "30.1.0" + jest-matcher-utils "30.3.0" + jest-message-util "30.3.0" + jest-mock "30.3.0" + jest-util "30.3.0" + express@^4.21.2: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" @@ -5072,6 +5450,13 @@ fontkit@^2.0.2: unicode-properties "^1.4.0" unicode-trie "^2.0.0" +for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -5117,6 +5502,20 @@ formidable@^2.1.2: once "^1.4.0" qs "^6.11.0" +formik@^2.4.6: + version "2.4.9" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.9.tgz#7e5b81e9c9e215d0ce2ac8fed808cf7fba0cd204" + integrity sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og== + dependencies: + "@types/hoist-non-react-statics" "^3.3.1" + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.21" + lodash-es "^4.17.21" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^2.0.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5184,7 +5583,7 @@ function.prototype.name@^1.1.2, function.prototype.name@^1.1.3, function.prototy es-abstract "^1.19.0" functions-have-names "^1.2.2" -functions-have-names@^1.2.2: +functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -5244,7 +5643,7 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" -get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.2, get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -5386,7 +5785,7 @@ google-maps-infobox@^2.0.0: resolved "https://registry.yarnpkg.com/google-maps-infobox/-/google-maps-infobox-2.0.0.tgz#1ea6de93c0cdf4138c2d586331835c83dcc59dc2" integrity sha512-hTuWmWZZSOxf5D/z7l3/hTF1grgRvLG53BEKMdjiKOG+FcK/kH7vqseUeyIU9Zj2ZIqKTOaro0nknxpAuRq4Vw== -gopd@^1.2.0: +gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== @@ -5453,6 +5852,13 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -5470,6 +5876,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -5879,6 +6292,15 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -5920,6 +6342,23 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-arguments@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" + integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-array-buffer@^3.0.2, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -5969,6 +6408,11 @@ is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.4: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-core-module@^2.16.0: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" @@ -6004,6 +6448,14 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" +is-date-object@^1.0.5: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -6107,6 +6559,11 @@ is-lambda@^1.0.1: resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= +is-map@^2.0.2, is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" @@ -6188,6 +6645,11 @@ is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-set@^2.0.2, is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + is-shared-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" @@ -6234,6 +6696,11 @@ is-url@^1.2.4: resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -6241,6 +6708,14 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakset@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== + dependencies: + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + is-what@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" @@ -6263,6 +6738,11 @@ isarray@1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -6414,6 +6894,16 @@ jest-config@^28.1.0: slash "^3.0.0" strip-json-comments "^3.1.1" +jest-diff@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.3.0.tgz#e0a4c84ef350ffd790ffd5b0016acabeecf5f759" + integrity sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ== + dependencies: + "@jest/diff-sequences" "30.3.0" + "@jest/get-type" "30.1.0" + chalk "^4.1.2" + pretty-format "30.3.0" + jest-diff@^28.1.0: version "28.1.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-28.1.0.tgz#77686fef899ec1873dbfbf9330e37dd429703269" @@ -6500,6 +6990,16 @@ jest-leak-detector@^28.1.0: jest-get-type "^28.0.2" pretty-format "^28.1.0" +jest-matcher-utils@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz#d6c739fec1ecd33809f2d2b1348f6ab01d2f2493" + integrity sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA== + dependencies: + "@jest/get-type" "30.1.0" + chalk "^4.1.2" + jest-diff "30.3.0" + pretty-format "30.3.0" + jest-matcher-utils@^28.1.0: version "28.1.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.0.tgz#2ae398806668eeabd293c61712227cb94b250ccf" @@ -6510,6 +7010,21 @@ jest-matcher-utils@^28.1.0: jest-get-type "^28.0.2" pretty-format "^28.1.0" +jest-message-util@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.3.0.tgz#4d723544d36890ba862ac3961db52db5b0d1ba39" + integrity sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@jest/types" "30.3.0" + "@types/stack-utils" "^2.0.3" + chalk "^4.1.2" + graceful-fs "^4.2.11" + picomatch "^4.0.3" + pretty-format "30.3.0" + slash "^3.0.0" + stack-utils "^2.0.6" + jest-message-util@^28.1.0: version "28.1.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-28.1.0.tgz#7e8f0b9049e948e7b94c2a52731166774ba7d0af" @@ -6525,6 +7040,15 @@ jest-message-util@^28.1.0: slash "^3.0.0" stack-utils "^2.0.3" +jest-mock@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-30.3.0.tgz#e0fa4184a596a6c4fdec53d4f412158418923747" + integrity sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog== + dependencies: + "@jest/types" "30.3.0" + "@types/node" "*" + jest-util "30.3.0" + jest-mock@^28.1.0: version "28.1.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-28.1.0.tgz#ccc7cc12a9b330b3182db0c651edc90d163ff73e" @@ -6538,6 +7062,11 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== +jest-regex-util@30.0.1: + version "30.0.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz#f17c1de3958b67dfe485354f5a10093298f2a49b" + integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA== + jest-regex-util@^28.0.2: version "28.0.2" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-28.0.2.tgz#afdc377a3b25fb6e80825adcf76c854e5bf47ead" @@ -6655,6 +7184,18 @@ jest-transform-stub@^2.0.0: resolved "https://registry.yarnpkg.com/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz#19018b0851f7568972147a5d60074b55f0225a7d" integrity sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg== +jest-util@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.3.0.tgz#95a4fbacf2dac20e768e2f1744b70519f2ba7980" + integrity sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg== + dependencies: + "@jest/types" "30.3.0" + "@types/node" "*" + chalk "^4.1.2" + ci-info "^4.2.0" + graceful-fs "^4.2.11" + picomatch "^4.0.3" + jest-util@^28.1.0: version "28.1.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.0.tgz#d54eb83ad77e1dd441408738c5a5043642823be5" @@ -7039,6 +7580,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash-es@^4.17.21: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0" + integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg== + lodash-es@^4.2.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" @@ -7084,6 +7630,11 @@ lodash@>=4.17.21, lodash@^4.15.0, lodash@^4.16.2, lodash@^4.17.11, lodash@^4.17. resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.17.21: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -7105,6 +7656,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + m3u8-parser@4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.7.0.tgz#e01e8ce136098ade1b14ee691ea20fc4dc60abf6" @@ -7225,7 +7781,7 @@ memfs@^4.6.0: tree-dump "^1.0.1" tslib "^2.0.0" -memoize-one@^5.0.0: +memoize-one@^5.0.0, memoize-one@^5.1.1: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== @@ -7806,6 +8362,14 @@ object-is@^1.0.2, object-is@^1.1.2: call-bind "^1.0.2" define-properties "^1.1.3" +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -7828,6 +8392,18 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" +object.assign@^4.1.4: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + object.entries@^1.1.1, object.entries@^1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" @@ -8106,11 +8682,21 @@ picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" + integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -8140,6 +8726,11 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +possible-typed-array-names@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== + postcss-calc@^8.2.3: version "8.2.4" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" @@ -8415,6 +9006,24 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= +pretty-format@30.3.0, pretty-format@^30.0.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.3.0.tgz#e977eed4bcd1b6195faed418af8eac68b9ea1f29" + integrity sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ== + dependencies: + "@jest/schemas" "30.0.5" + ansi-styles "^5.2.0" + react-is "^18.3.1" + +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + pretty-format@^28.1.0: version "28.1.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.0.tgz#8f5836c6a0dfdb834730577ec18029052191af55" @@ -8548,6 +9157,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + raf@^3.4.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -8599,6 +9213,19 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +react-beautiful-dnd@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-bootstrap@^0.31.5: version "0.31.5" resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.31.5.tgz#57040fa8b1274e1e074803c21a1b895fdabea05a" @@ -8661,6 +9288,11 @@ react-dropzone@^4.2.9: attr-accept "^1.1.3" prop-types "^15.5.7" +react-fast-compare@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + react-final-form@^6.5.9: version "6.5.9" resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-6.5.9.tgz#644797d4c122801b37b58a76c87761547411190b" @@ -8697,16 +9329,31 @@ react-is@^16.13.1, react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-i resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1, react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-is@^18.0.0: version "18.1.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== +react-is@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + react-is@^19.0.0: version "19.1.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.1.0.tgz#805bce321546b7e14c084989c77022351bbdd11b" integrity sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg== +react-is@^19.2.3: + version "19.2.4" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.4.tgz#a080758243c572ccd4a63386537654298c99d135" + integrity sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA== + react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -8741,6 +9388,18 @@ react-redux@^5.0.7: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" +react-redux@^7.2.0: + version "7.2.9" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" + integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" + react-rte@^0.16.3: version "0.16.5" resolved "https://registry.yarnpkg.com/react-rte/-/react-rte-0.16.5.tgz#13c230060bc82d5f0465f903cd36f53dd2fa4469" @@ -8935,6 +9594,13 @@ redux@^3.7.2: loose-envify "^1.1.0" symbol-observable "^1.0.3" +redux@^4.0.0, redux@^4.0.4: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + redux@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" @@ -9010,6 +9676,18 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" +regexp.prototype.flags@^1.5.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" + regexpu-core@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.0.1.tgz#c531122a7840de743dcf9c83e923b5560323ced3" @@ -9413,6 +10091,28 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -9500,7 +10200,7 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -side-channel@^1.0.6: +side-channel@^1.0.6, side-channel@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== @@ -9758,6 +10458,13 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +stack-utils@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -9783,6 +10490,14 @@ stdout-stream@^1.4.0: dependencies: readable-stream "^2.0.1" +stop-iteration-iterator@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== + dependencies: + es-errors "^1.3.0" + internal-slot "^1.1.0" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -10078,7 +10793,12 @@ tiny-invariant@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== -tiny-warning@^1.0.0: +tiny-invariant@^1.0.6: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + +tiny-warning@^1.0.0, tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== @@ -10417,6 +11137,11 @@ url-toolkit@^2.2.1: resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.2.5.tgz#58406b18e12c58803e14624df5e374f638b0f607" integrity sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg== +use-memo-one@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -10779,6 +11504,29 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-collection@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.13: + version "1.1.20" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.20.tgz#3fdb7adfafe0ea69157b1509f3a1cd892bd1d122" + integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" From 9495b4170438998071d35391cf871ff03ecdc0e9 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Mon, 30 Mar 2026 14:27:01 -0300 Subject: [PATCH 2/8] fix: adding tests --- .../__tests__/GlobalQuantityField.test.js | 64 ++++++ .../__tests__/ItemTableField.test.js | 152 ++++++++++++++ .../__tests__/UnderlyingAlertNote.test.js | 38 ++++ .../FormItemTable/__tests__/helpers.test.js | 159 +++++++++++++++ .../mui/__tests__/checkbox-list.test.js | 86 ++++++++ .../mui/__tests__/chip-notify.test.js | 45 ++++ .../mui/__tests__/chip-select-input.test.js | 103 ++++++++++ .../mui/__tests__/company-input-mui.test.js | 81 ++++++++ .../mui/__tests__/confirm-dialog.test.js | 81 ++++++++ .../mui/__tests__/custom-alert.test.js | 44 ++++ src/components/mui/__tests__/dnd-list.test.js | 87 ++++++++ .../mui/__tests__/dropdown-checkbox.test.js | 53 +++++ .../mui/__tests__/item-settings-modal.test.js | 98 +++++++++ .../mui/__tests__/menu-button.test.js | 78 +++++++ .../__tests__/mui-formik-async-select.test.js | 101 +++++++++ .../mui/__tests__/mui-formik-checkbox.test.js | 89 ++++++++ .../mui-formik-discountfield.test.js | 67 ++++++ .../mui-formik-dropdown-checkbox.test.js | 75 +++++++ .../mui-formik-dropdown-radio.test.js | 74 +++++++ .../__tests__/mui-formik-pricefield.test.js | 54 +++++ .../__tests__/mui-formik-select-group.test.js | 93 +++++++++ .../mui/__tests__/mui-formik-select.test.js | 84 ++++++++ .../mui-formik-summit-addon-select.test.js | 83 ++++++++ .../mui/__tests__/mui-formik-switch.test.js | 73 +++++++ .../__tests__/mui-formik-textfield.test.js | 91 +++++++++ .../__tests__/mui-formik-timepicker.test.js | 87 ++++++++ .../mui/__tests__/mui-formik-upload.test.js | 92 +++++++++ .../mui/__tests__/mui-infinite-table.test.js | 92 +++++++++ .../mui/__tests__/mui-table-sortable.test.js | 166 +++++++++++++++ .../mui/__tests__/mui-table.test.js | 192 ++++++++++++++++++ .../mui/__tests__/notes-modal.test.js | 81 ++++++++ .../mui/__tests__/notes-row.test.js | 49 +++++ .../mui/__tests__/search-input.test.js | 61 ++++++ .../mui/__tests__/show-confirm-dialog.test.js | 59 ++++++ .../snackbar-notification-context.test.js | 59 ++++++ .../__tests__/snackbar-notification.test.js | 73 +++++++ .../__tests__/sponsor-addon-select.test.js | 57 ++++++ .../__tests__/sponsorship-input-mui.test.js | 84 ++++++++ .../sponsorship-summit-select-mui.test.js | 85 ++++++++ .../mui/__tests__/summit-addon-select.test.js | 59 ++++++ .../mui/__tests__/summits-dropdown.test.js | 88 ++++++++ .../mui/__tests__/total-row.test.js | 68 +++++++ 42 files changed, 3505 insertions(+) create mode 100644 src/components/mui/FormItemTable/__tests__/GlobalQuantityField.test.js create mode 100644 src/components/mui/FormItemTable/__tests__/ItemTableField.test.js create mode 100644 src/components/mui/FormItemTable/__tests__/UnderlyingAlertNote.test.js create mode 100644 src/components/mui/FormItemTable/__tests__/helpers.test.js create mode 100644 src/components/mui/__tests__/checkbox-list.test.js create mode 100644 src/components/mui/__tests__/chip-notify.test.js create mode 100644 src/components/mui/__tests__/chip-select-input.test.js create mode 100644 src/components/mui/__tests__/company-input-mui.test.js create mode 100644 src/components/mui/__tests__/confirm-dialog.test.js create mode 100644 src/components/mui/__tests__/custom-alert.test.js create mode 100644 src/components/mui/__tests__/dnd-list.test.js create mode 100644 src/components/mui/__tests__/dropdown-checkbox.test.js create mode 100644 src/components/mui/__tests__/item-settings-modal.test.js create mode 100644 src/components/mui/__tests__/menu-button.test.js create mode 100644 src/components/mui/__tests__/mui-formik-async-select.test.js create mode 100644 src/components/mui/__tests__/mui-formik-checkbox.test.js create mode 100644 src/components/mui/__tests__/mui-formik-discountfield.test.js create mode 100644 src/components/mui/__tests__/mui-formik-dropdown-checkbox.test.js create mode 100644 src/components/mui/__tests__/mui-formik-dropdown-radio.test.js create mode 100644 src/components/mui/__tests__/mui-formik-pricefield.test.js create mode 100644 src/components/mui/__tests__/mui-formik-select-group.test.js create mode 100644 src/components/mui/__tests__/mui-formik-select.test.js create mode 100644 src/components/mui/__tests__/mui-formik-summit-addon-select.test.js create mode 100644 src/components/mui/__tests__/mui-formik-switch.test.js create mode 100644 src/components/mui/__tests__/mui-formik-textfield.test.js create mode 100644 src/components/mui/__tests__/mui-formik-timepicker.test.js create mode 100644 src/components/mui/__tests__/mui-formik-upload.test.js create mode 100644 src/components/mui/__tests__/mui-infinite-table.test.js create mode 100644 src/components/mui/__tests__/mui-table-sortable.test.js create mode 100644 src/components/mui/__tests__/mui-table.test.js create mode 100644 src/components/mui/__tests__/notes-modal.test.js create mode 100644 src/components/mui/__tests__/notes-row.test.js create mode 100644 src/components/mui/__tests__/search-input.test.js create mode 100644 src/components/mui/__tests__/show-confirm-dialog.test.js create mode 100644 src/components/mui/__tests__/snackbar-notification-context.test.js create mode 100644 src/components/mui/__tests__/snackbar-notification.test.js create mode 100644 src/components/mui/__tests__/sponsor-addon-select.test.js create mode 100644 src/components/mui/__tests__/sponsorship-input-mui.test.js create mode 100644 src/components/mui/__tests__/sponsorship-summit-select-mui.test.js create mode 100644 src/components/mui/__tests__/summit-addon-select.test.js create mode 100644 src/components/mui/__tests__/summits-dropdown.test.js create mode 100644 src/components/mui/__tests__/total-row.test.js diff --git a/src/components/mui/FormItemTable/__tests__/GlobalQuantityField.test.js b/src/components/mui/FormItemTable/__tests__/GlobalQuantityField.test.js new file mode 100644 index 00000000..1cc45b10 --- /dev/null +++ b/src/components/mui/FormItemTable/__tests__/GlobalQuantityField.test.js @@ -0,0 +1,64 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import GlobalQuantityField from "../components/GlobalQuantityField"; + +const row = { form_item_id: 1, quantity_limit_per_sponsor: 5 }; +const fieldName = `i-${row.form_item_id}-c-global-f-quantity`; + +const renderField = (props = {}) => + render( + +
    + + +
    + ); + +describe("GlobalQuantityField", () => { + test("renders a number input", () => { + renderField(); + expect(screen.getByRole("spinbutton")).toBeInTheDocument(); + }); + + test("input is not disabled by default", () => { + renderField(); + expect(screen.getByRole("spinbutton")).not.toBeDisabled(); + }); + + test("input is disabled when disabled prop is true", () => { + renderField({ disabled: true }); + expect(screen.getByRole("spinbutton")).toBeDisabled(); + }); + + test("input has readOnly when extraColumns includes Quantity type", () => { + renderField({ extraColumns: [{ type: "Quantity" }] }); + const input = screen.getByRole("spinbutton"); + expect(input).toHaveAttribute("readonly"); + }); + + test("input does not have readOnly when extraColumns has no Quantity", () => { + renderField({ extraColumns: [{ type: "Text" }] }); + const input = screen.getByRole("spinbutton"); + expect(input).not.toHaveAttribute("readonly"); + }); +}); diff --git a/src/components/mui/FormItemTable/__tests__/ItemTableField.test.js b/src/components/mui/FormItemTable/__tests__/ItemTableField.test.js new file mode 100644 index 00000000..0eda325b --- /dev/null +++ b/src/components/mui/FormItemTable/__tests__/ItemTableField.test.js @@ -0,0 +1,152 @@ +/** + * 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. + * */ + +jest.mock("../../formik-inputs/mui-formik-checkbox", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ name }) =>
    + }; +}); + +jest.mock("../../formik-inputs/mui-formik-dropdown-checkbox", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ name }) => ( +
    + ) + }; +}); + +jest.mock("../../formik-inputs/mui-formik-dropdown-radio", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ name }) => ( +
    + ) + }; +}); + +jest.mock("../../formik-inputs/mui-formik-datepicker", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ name }) =>
    + }; +}); + +jest.mock("../../formik-inputs/mui-formik-timepicker", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ name }) =>
    + }; +}); + +jest.mock("../../formik-inputs/mui-formik-textfield", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ name, multiline }) => ( +
    + ) + }; +}); + +jest.mock("../../formik-inputs/mui-formik-select", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ name, children }) => ( +
    + {children} +
    + ) + }; +}); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import ItemTableField from "../components/ItemTableField"; + +const rowId = 1; +const baseField = { class_field: "Item", type_id: 10, values: [] }; + +const renderField = (fieldOverrides = {}) => + render( + + ); + +describe("ItemTableField", () => { + test("renders MuiFormikCheckbox for CheckBox type", () => { + renderField({ type: "CheckBox" }); + expect(screen.getByTestId("checkbox")).toBeInTheDocument(); + }); + + test("renders MuiFormikDropdownCheckbox for CheckBoxList type", () => { + renderField({ type: "CheckBoxList" }); + expect(screen.getByTestId("dropdown-checkbox")).toBeInTheDocument(); + }); + + test("renders MuiFormikDropdownRadio for RadioButtonList type", () => { + renderField({ type: "RadioButtonList" }); + expect(screen.getByTestId("dropdown-radio")).toBeInTheDocument(); + }); + + test("renders MuiFormikDatepicker for DateTime type", () => { + renderField({ type: "DateTime" }); + expect(screen.getByTestId("datepicker")).toBeInTheDocument(); + }); + + test("renders MuiFormikTimepicker for Time type", () => { + renderField({ type: "Time" }); + expect(screen.getByTestId("timepicker")).toBeInTheDocument(); + }); + + test("renders MuiFormikTextField for Quantity type", () => { + renderField({ type: "Quantity", minimum_quantity: 0, maximum_quantity: 0 }); + expect(screen.getByTestId("textfield")).toBeInTheDocument(); + }); + + test("renders MuiFormikSelect for ComboBox type", () => { + renderField({ type: "ComboBox" }); + expect(screen.getByTestId("select")).toBeInTheDocument(); + }); + + test("renders MuiFormikTextField for Text type", () => { + renderField({ type: "Text" }); + expect(screen.getByTestId("textfield")).toBeInTheDocument(); + }); + + test("renders MuiFormikTextField multiline for TextArea type", () => { + renderField({ type: "TextArea" }); + expect(screen.getByTestId("textarea")).toBeInTheDocument(); + }); + + test("uses correct field name based on rowId, class_field, and type_id", () => { + renderField({ type: "Text" }); + expect(screen.getByTestId("textfield")).toHaveAttribute( + "data-name", + `i-${rowId}-c-Item-f-10` + ); + }); +}); diff --git a/src/components/mui/FormItemTable/__tests__/UnderlyingAlertNote.test.js b/src/components/mui/FormItemTable/__tests__/UnderlyingAlertNote.test.js new file mode 100644 index 00000000..7456e6d6 --- /dev/null +++ b/src/components/mui/FormItemTable/__tests__/UnderlyingAlertNote.test.js @@ -0,0 +1,38 @@ +/** + * 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. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import UnderlyingAlertNote from "../components/UnderlyingAlertNote"; + +describe("UnderlyingAlertNote", () => { + test("renders null when showAdditionalItems is false", () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); + + test("renders alert text when showAdditionalItems is true", () => { + render(); + expect( + screen.getByText("sponsor_edit_form.additional_info") + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/FormItemTable/__tests__/helpers.test.js b/src/components/mui/FormItemTable/__tests__/helpers.test.js new file mode 100644 index 00000000..fc3ad820 --- /dev/null +++ b/src/components/mui/FormItemTable/__tests__/helpers.test.js @@ -0,0 +1,159 @@ +/** + * 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. + * */ + +const mockMoment = (isSameOrBeforeFn) => ({ + isSameOrBefore: isSameOrBeforeFn, + endOf: () => ({ isSameOrBefore: isSameOrBeforeFn }), + startOf: () => ({ isSameOrBefore: isSameOrBeforeFn }) +}); + +jest.mock("../../../../utils/methods", () => ({ + epochToMomentTimeZone: jest.fn() +})); + +jest.mock("../../../../utils/constants", () => ({ + MILLISECONDS_IN_SECOND: 1000 +})); + +import { epochToMomentTimeZone } from "../../../../utils/methods"; +import { + getCurrentApplicableRate, + isItemAvailable +} from "../helpers"; + +describe("isItemAvailable", () => { + test("returns true when item has a rate for the given period", () => { + const item = { rates: { early_bird: 100 } }; + expect(isItemAvailable(item, "early_bird")).toBe(true); + }); + + test("returns false when item has no rates", () => { + const item = {}; + expect(isItemAvailable(item, "early_bird")).toBe(false); + }); + + test("returns false when item has rates but not for the given period", () => { + const item = { rates: { standard: 50 } }; + expect(isItemAvailable(item, "early_bird")).toBe(false); + }); + + test("returns false when rate value is null", () => { + const item = { rates: { early_bird: null } }; + expect(isItemAvailable(item, "early_bird")).toBe(false); + }); +}); + +describe("getCurrentApplicableRate", () => { + beforeEach(() => jest.clearAllMocks()); + + test("returns early_bird when now is before earlyBirdEnd", () => { + const nowMoment = mockMoment((other) => true); + epochToMomentTimeZone.mockReturnValue(nowMoment); + + const result = getCurrentApplicableRate("UTC", { + early_bird_end_date: 1000, + onsite_price_start_date: 2000, + onsite_price_end_date: 3000 + }); + expect(result).toBe("early_bird"); + }); + + test("returns standard when past earlyBird but before onsiteStart", () => { + let calls = 0; + const nowMock = { + isSameOrBefore: jest + .fn() + .mockReturnValueOnce(false) // not before earlyBirdEnd + .mockReturnValueOnce(true) // before onsiteStart + }; + epochToMomentTimeZone.mockImplementation(() => { + calls++; + if (calls === 1) return nowMock; + if (calls === 2) return { endOf: () => ({}) }; // earlyBirdEnd (truthy) + if (calls === 3) return { startOf: () => ({}) }; // onsiteStart (truthy) + if (calls === 4) return { endOf: () => ({}) }; // onsiteEnd (not reached) + return null; + }); + + const result = getCurrentApplicableRate("UTC", { + early_bird_end_date: 1000, + onsite_price_start_date: 2000, + onsite_price_end_date: 3000 + }); + expect(result).toBe("standard"); + }); + + test("returns onsite when in onsite period", () => { + let calls = 0; + const nowMock = { + isSameOrBefore: jest + .fn() + .mockReturnValueOnce(false) // not before earlyBirdEnd + .mockReturnValueOnce(false) // not before onsiteStart + .mockReturnValueOnce(true) // before onsiteEnd + }; + epochToMomentTimeZone.mockImplementation(() => { + calls++; + if (calls === 1) return nowMock; + if (calls === 2) return { endOf: () => ({}) }; // earlyBirdEnd (truthy) + if (calls === 3) return { startOf: () => ({}) }; // onsiteStart (truthy) + if (calls === 4) return { endOf: () => ({}) }; // onsiteEnd (truthy) + return null; + }); + + const result = getCurrentApplicableRate("UTC", { + early_bird_end_date: 1000, + onsite_price_start_date: 2000, + onsite_price_end_date: 3000 + }); + expect(result).toBe("onsite"); + }); + + test("returns expired when all dates are past", () => { + let calls = 0; + epochToMomentTimeZone.mockImplementation(() => { + calls++; + if (calls === 1) return { isSameOrBefore: () => false }; + if (calls === 2) return { endOf: () => ({ isSameOrBefore: () => false }) }; + if (calls === 3) return { startOf: () => ({ isSameOrBefore: () => false }) }; + if (calls === 4) return { endOf: () => ({ isSameOrBefore: () => false }) }; + return null; + }); + + const result = getCurrentApplicableRate("UTC", { + early_bird_end_date: 1000, + onsite_price_start_date: 2000, + onsite_price_end_date: 3000 + }); + expect(result).toBe("expired"); + }); + + test("returns onsite when onsiteEnd is not provided", () => { + let calls = 0; + epochToMomentTimeZone.mockImplementation(() => { + calls++; + if (calls === 1) return { isSameOrBefore: () => false }; + if (calls === 2) return { endOf: () => ({ isSameOrBefore: () => false }) }; + if (calls === 3) return { startOf: () => ({ isSameOrBefore: () => false }) }; + if (calls === 4) return null; // no onsiteEnd + return null; + }); + + const result = getCurrentApplicableRate("UTC", { + early_bird_end_date: 1000, + onsite_price_start_date: 2000, + onsite_price_end_date: null + }); + expect(result).toBe("onsite"); + }); +}); diff --git a/src/components/mui/__tests__/checkbox-list.test.js b/src/components/mui/__tests__/checkbox-list.test.js new file mode 100644 index 00000000..63b1e360 --- /dev/null +++ b/src/components/mui/__tests__/checkbox-list.test.js @@ -0,0 +1,86 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import CheckBoxList from "../checkbox-list"; + +describe("CheckBoxList", () => { + const items = [ + { id: 1, name: "Item A" }, + { id: 2, name: "Item B" }, + { id: 3, name: "Item C" } + ]; + + test("renders no-items label when items array is empty", () => { + render(); + expect(screen.getByText("No items found")).toBeInTheDocument(); + }); + + test("renders all items", () => { + render(); + expect(screen.getByText("Item A")).toBeInTheDocument(); + expect(screen.getByText("Item B")).toBeInTheDocument(); + expect(screen.getByText("Item C")).toBeInTheDocument(); + }); + + test("renders select-all checkbox with default label", () => { + render(); + expect(screen.getByText("Select All")).toBeInTheDocument(); + }); + + test("renders custom allItemsLabel", () => { + render( + + ); + expect(screen.getByText("Check All")).toBeInTheDocument(); + }); + + test("renders custom noItemsLabel", () => { + render( + + ); + expect(screen.getByText("Nothing here")).toBeInTheDocument(); + }); + + test("selecting an individual item calls onChange with that item id", async () => { + const onChange = jest.fn(); + render(); + const checkboxes = screen.getAllByRole("checkbox"); + // Index 0 is "Select All", index 1 is first item + await userEvent.click(checkboxes[1]); + expect(onChange).toHaveBeenCalledWith([1]); + }); + + test("clicking select-all calls onChange with empty array and true", async () => { + const onChange = jest.fn(); + render(); + const selectAllCheckbox = screen.getAllByRole("checkbox")[0]; + await userEvent.click(selectAllCheckbox); + expect(onChange).toHaveBeenCalledWith([], true); + }); + + test("deselecting an item after select-all removes it from selection", async () => { + const onChange = jest.fn(); + render(); + const checkboxes = screen.getAllByRole("checkbox"); + // Select all first + await userEvent.click(checkboxes[0]); + onChange.mockClear(); + // Now uncheck one item (isAllSelected is true, so it filters that id out) + await userEvent.click(checkboxes[1]); + expect(onChange).toHaveBeenCalledWith([2, 3]); + }); +}); diff --git a/src/components/mui/__tests__/chip-notify.test.js b/src/components/mui/__tests__/chip-notify.test.js new file mode 100644 index 00000000..c9971b24 --- /dev/null +++ b/src/components/mui/__tests__/chip-notify.test.js @@ -0,0 +1,45 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import ChipNotify from "../chip-notify"; + +describe("ChipNotify", () => { + test("renders the label in uppercase", () => { + render(); + expect(screen.getByText("ALERT")).toBeInTheDocument(); + }); + + test("renders uppercase for mixed-case label", () => { + render(); + expect(screen.getByText("NEW UPDATE")).toBeInTheDocument(); + }); + + test("renders without crashing with default props", () => { + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + }); + + test("renders with custom icon", () => { + const CustomIcon = () => ; + render(); + expect(screen.getByTestId("custom-icon")).toBeInTheDocument(); + }); + + test("renders with custom color", () => { + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/chip-select-input.test.js b/src/components/mui/__tests__/chip-select-input.test.js new file mode 100644 index 00000000..285e6c7c --- /dev/null +++ b/src/components/mui/__tests__/chip-select-input.test.js @@ -0,0 +1,103 @@ +/** + * 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. + * */ + +jest.mock("@mui/x-date-pickers", () => ({ + ClearIcon: () => { + const React = require("react"); + return X; + } +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import ChipSelectInput from "../chip-select-input"; + +describe("ChipSelectInput", () => { + const availableOptions = [ + { value: "col1", label: "Column 1" }, + { value: "col2", label: "Column 2" } + ]; + + const baseProps = { + onGetSettings: jest.fn(), + onUpsertSettings: jest.fn() + }; + + test("renders null when canAdd is false", () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); + + test("renders null when canEdit is false", () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); + + test("renders null when availableOptions is empty", () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); + + test("renders select when canAdd, canEdit, and options are all present", () => { + render( + + ); + expect(screen.getByText("Columns", { selector: "label" })).toBeInTheDocument(); + }); + + test("calls onGetSettings on mount", () => { + const onGetSettings = jest.fn(); + render( + + ); + expect(onGetSettings).toHaveBeenCalled(); + }); +}); diff --git a/src/components/mui/__tests__/company-input-mui.test.js b/src/components/mui/__tests__/company-input-mui.test.js new file mode 100644 index 00000000..de17fe60 --- /dev/null +++ b/src/components/mui/__tests__/company-input-mui.test.js @@ -0,0 +1,81 @@ +/** + * 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. + * */ + +jest.mock("../../../utils/query-actions", () => ({ + queryCompanies: jest.fn() +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import CompanyInputMUI from "../formik-inputs/company-input-mui"; + +const renderWithFormik = (props, initialValues = { company: null }) => + render( + +
    + + +
    + ); + +describe("CompanyInputMUI", () => { + beforeEach(() => jest.clearAllMocks()); + + test("renders with placeholder", () => { + renderWithFormik({}); + expect( + screen.getByPlaceholderText("Search companies...") + ).toBeInTheDocument(); + }); + + test("renders with a preselected single value", () => { + renderWithFormik({}, { company: { id: 1, name: "Acme Corp" } }); + expect(screen.getByDisplayValue("Acme Corp")).toBeInTheDocument(); + }); + + test("renders with multiple preselected values", () => { + renderWithFormik( + { isMulti: true }, + { + company: [ + { id: 1, name: "Acme Corp" }, + { id: 2, name: "Beta Inc" } + ] + } + ); + expect(screen.getByText("Acme Corp")).toBeInTheDocument(); + expect(screen.getByText("Beta Inc")).toBeInTheDocument(); + }); + + test("shows error when touched and has error", () => { + render( + +
    + + +
    + ); + expect(screen.getByText("Company is required")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/confirm-dialog.test.js b/src/components/mui/__tests__/confirm-dialog.test.js new file mode 100644 index 00000000..dce84c3e --- /dev/null +++ b/src/components/mui/__tests__/confirm-dialog.test.js @@ -0,0 +1,81 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import ConfirmDialog from "../confirm-dialog"; + +describe("ConfirmDialog", () => { + const defaultProps = { + open: true, + title: "Confirm Action", + text: "Are you sure?", + onConfirm: jest.fn(), + onCancel: jest.fn() + }; + + beforeEach(() => jest.clearAllMocks()); + + test("renders title and text when open", () => { + render(); + expect(screen.getByText("Confirm Action")).toBeInTheDocument(); + expect(screen.getByText("Are you sure?")).toBeInTheDocument(); + }); + + test("renders default confirm and cancel buttons", () => { + render(); + expect(screen.getByText("Confirm")).toBeInTheDocument(); + expect(screen.getByText("Cancel")).toBeInTheDocument(); + }); + + test("calls onConfirm when confirm button is clicked", async () => { + render(); + await userEvent.click(screen.getByText("Confirm")); + expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1); + }); + + test("calls onCancel when cancel button is clicked", async () => { + render(); + await userEvent.click(screen.getByText("Cancel")); + expect(defaultProps.onCancel).toHaveBeenCalledTimes(1); + }); + + test("renders custom button text", () => { + render( + + ); + expect(screen.getByText("Yes, delete")).toBeInTheDocument(); + expect(screen.getByText("No, keep")).toBeInTheDocument(); + }); + + test("does not render content when open is false", () => { + render(); + expect(screen.queryByText("Confirm Action")).not.toBeInTheDocument(); + }); + + test("renders warning icon when iconType is warning", () => { + render(); + expect(screen.getByText("Are you sure?")).toBeInTheDocument(); + }); + + test("renders error icon when iconType is error", () => { + render(); + expect(screen.getByText("Are you sure?")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/custom-alert.test.js b/src/components/mui/__tests__/custom-alert.test.js new file mode 100644 index 00000000..33be0106 --- /dev/null +++ b/src/components/mui/__tests__/custom-alert.test.js @@ -0,0 +1,44 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import CustomAlert from "../custom-alert"; + +describe("CustomAlert", () => { + test("renders the message", () => { + render(); + expect(screen.getByText("Hello world")).toBeInTheDocument(); + }); + + test("renders without crashing when no props provided", () => { + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + }); + + test("renders with error severity", () => { + render(); + expect(screen.getByText("Error message")).toBeInTheDocument(); + }); + + test("renders with success severity", () => { + render(); + expect(screen.getByText("Success!")).toBeInTheDocument(); + }); + + test("renders with warning severity", () => { + render(); + expect(screen.getByText("Warning!")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/dnd-list.test.js b/src/components/mui/__tests__/dnd-list.test.js new file mode 100644 index 00000000..b98c3a34 --- /dev/null +++ b/src/components/mui/__tests__/dnd-list.test.js @@ -0,0 +1,87 @@ +/** + * 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. + * */ + +jest.mock("react-beautiful-dnd", () => { + const React = require("react"); + return { + DragDropContext: ({ children }) => <>{children}, + Droppable: ({ children }) => + children( + { innerRef: jest.fn(), droppableProps: {}, placeholder: null }, + {} + ), + Draggable: ({ children }) => + children( + { innerRef: jest.fn(), draggableProps: {}, dragHandleProps: {} }, + { isDragging: false } + ) + }; +}); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import DragAndDropList from "../dnd-list"; + +describe("DragAndDropList", () => { + const items = [ + { id: 1, order: 1, name: "Item 1" }, + { id: 2, order: 2, name: "Item 2" }, + { id: 3, order: 3, name: "Item 3" } + ]; + + test("renders all items via renderItem", () => { + render( +
    {item.name}
    } + droppableId="test-list" + /> + ); + expect(screen.getByText("Item 1")).toBeInTheDocument(); + expect(screen.getByText("Item 2")).toBeInTheDocument(); + expect(screen.getByText("Item 3")).toBeInTheDocument(); + }); + + test("renders empty list without errors", () => { + const { container } = render( +
    {item.name}
    } + droppableId="test-list" + /> + ); + expect(container).toBeInTheDocument(); + }); + + test("calls renderItem with item, index, provided, snapshot", () => { + const renderItem = jest.fn((item) =>
    {item.name}
    ); + render( + + ); + expect(renderItem).toHaveBeenCalledTimes(3); + expect(renderItem).toHaveBeenCalledWith( + items[0], + 0, + expect.any(Object), + expect.any(Object) + ); + }); +}); diff --git a/src/components/mui/__tests__/dropdown-checkbox.test.js b/src/components/mui/__tests__/dropdown-checkbox.test.js new file mode 100644 index 00000000..060506ce --- /dev/null +++ b/src/components/mui/__tests__/dropdown-checkbox.test.js @@ -0,0 +1,53 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import DropdownCheckbox from "../dropdown-checkbox"; + +describe("DropdownCheckbox", () => { + const options = [ + { id: 1, name: "Option A" }, + { id: 2, name: "Option B" } + ]; + + const defaultProps = { + name: "test", + label: "Test Label", + allLabel: "All Options", + value: [], + options, + onChange: jest.fn() + }; + + test("renders with label", () => { + render(); + expect(screen.getByText("Test Label", { selector: "label" })).toBeInTheDocument(); + }); + + test("renders without crashing when value is empty", () => { + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + }); + + test("renders allLabel when value contains 'all'", () => { + render(); + expect(screen.getByText("All Options")).toBeInTheDocument(); + }); + + test("renders combobox role", () => { + render(); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/item-settings-modal.test.js b/src/components/mui/__tests__/item-settings-modal.test.js new file mode 100644 index 00000000..4eef34ca --- /dev/null +++ b/src/components/mui/__tests__/item-settings-modal.test.js @@ -0,0 +1,98 @@ +/** + * 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. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +jest.mock("../FormItemTable/components/ItemTableField", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ field }) => ( +
    {field.name}
    + ) + }; +}); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import ItemSettingsModal from "../ItemSettingsModal/index"; + +const item = { + form_item_id: 1, + name: "My Item", + meta_fields: [ + { name: "Field A", class_field: "Item", type_id: 10, type: "Text" }, + { name: "Field B", class_field: "Item", type_id: 11, type: "CheckBox" }, + { name: "Global Field", class_field: "Global", type_id: 12, type: "Text" } + ] +}; + +const renderModal = (props) => + render( + + ); + +describe("ItemSettingsModal", () => { + test("renders settings title when open", () => { + renderModal({}); + expect(screen.getByText("general.settings")).toBeInTheDocument(); + }); + + test("renders item name", () => { + renderModal({}); + expect(screen.getByText("My Item")).toBeInTheDocument(); + }); + + test("renders only Item class_field meta_fields", () => { + renderModal({}); + expect(screen.getByTestId("item-field-10")).toBeInTheDocument(); + expect(screen.getByTestId("item-field-11")).toBeInTheDocument(); + // Global field should NOT be rendered + expect(screen.queryByTestId("item-field-12")).not.toBeInTheDocument(); + }); + + test("renders field names", () => { + renderModal({}); + expect(screen.getByText("Field A")).toBeInTheDocument(); + expect(screen.getByText("Field B")).toBeInTheDocument(); + }); + + test("calls onClose when close icon is clicked", async () => { + const onClose = jest.fn(); + renderModal({ onClose }); + await userEvent.click(screen.getByRole("button", { name: /close/i })); + expect(onClose).toHaveBeenCalled(); + }); + + test("calls onClose when save button is clicked", async () => { + const onClose = jest.fn(); + renderModal({ onClose }); + await userEvent.click(screen.getByText("general.save")); + expect(onClose).toHaveBeenCalled(); + }); + + test("does not render dialog when open is false", () => { + renderModal({ open: false }); + expect(screen.queryByText("general.settings")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/menu-button.test.js b/src/components/mui/__tests__/menu-button.test.js new file mode 100644 index 00000000..2613f11e --- /dev/null +++ b/src/components/mui/__tests__/menu-button.test.js @@ -0,0 +1,78 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import MenuButton from "../menu-button"; + +describe("MenuButton", () => { + const menuItems = [ + { label: "Edit", onClick: jest.fn() }, + { label: "Delete", onClick: jest.fn() } + ]; + + beforeEach(() => jest.clearAllMocks()); + + test("renders button with children text", () => { + render( + + Options + + ); + expect(screen.getByText("Options")).toBeInTheDocument(); + }); + + test("menu is not visible before clicking", () => { + render( + + Options + + ); + expect(screen.queryByRole("menu")).not.toBeInTheDocument(); + }); + + test("clicking button opens the menu with items", async () => { + render( + + Options + + ); + await userEvent.click(screen.getByText("Options")); + expect(screen.getByText("Edit")).toBeInTheDocument(); + expect(screen.getByText("Delete")).toBeInTheDocument(); + }); + + test("clicking a menu item calls its onClick handler", async () => { + const onEdit = jest.fn(); + const items = [{ label: "Edit", onClick: onEdit }]; + render( + + Options + + ); + await userEvent.click(screen.getByText("Options")); + await userEvent.click(screen.getByText("Edit")); + expect(onEdit).toHaveBeenCalled(); + }); + + test("renders with badge when hasBadge is true", () => { + render( + + Filters + + ); + expect(screen.getByText("Filters")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-async-select.test.js b/src/components/mui/__tests__/mui-formik-async-select.test.js new file mode 100644 index 00000000..a0473b10 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-async-select.test.js @@ -0,0 +1,101 @@ +/** + * 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 { render, screen, waitFor } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikAsyncAutocomplete from "../formik-inputs/mui-formik-async-select"; + +const mockQueryFunction = jest.fn((input, callback) => { + callback([ + { id: 1, name: "Option A" }, + { id: 2, name: "Option B" } + ]); + return Promise.resolve(); +}); + +const renderWithFormik = (props, initialValues = { field: null }) => + render( + +
    + + +
    + ); + +describe("MuiFormikAsyncAutocomplete", () => { + beforeEach(() => jest.clearAllMocks()); + + test("renders with placeholder", () => { + renderWithFormik({}); + expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument(); + }); + + test("calls queryFunction on mount with empty string", async () => { + renderWithFormik({}); + await waitFor(() => { + expect(mockQueryFunction).toHaveBeenCalledWith( + "", + expect.any(Function) + ); + }); + }); + + test("renders with preselected single value", () => { + renderWithFormik( + {}, + { field: { value: "1", label: "Option A" } } + ); + expect(screen.getByDisplayValue("Option A")).toBeInTheDocument(); + }); + + test("renders with multiple preselected values when multiple is true", () => { + renderWithFormik( + { isMulti: true }, + { + field: [ + { value: "1", label: "Option A" }, + { value: "2", label: "Option B" } + ] + } + ); + expect(screen.getByText("Option A")).toBeInTheDocument(); + expect(screen.getByText("Option B")).toBeInTheDocument(); + }); + + test("shows error when touched and has error", () => { + render( + +
    + + +
    + ); + expect(screen.getByText("Required")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-checkbox.test.js b/src/components/mui/__tests__/mui-formik-checkbox.test.js new file mode 100644 index 00000000..1d5a72c2 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-checkbox.test.js @@ -0,0 +1,89 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikCheckbox from "../formik-inputs/mui-formik-checkbox"; + +const renderWithFormik = (props, initialValues = { agreed: false }) => + render( + +
    + + +
    + ); + +describe("MuiFormikCheckbox", () => { + test("renders with label", () => { + renderWithFormik({}); + expect(screen.getByText("I Agree")).toBeInTheDocument(); + }); + + test("renders a checkbox input", () => { + renderWithFormik({}); + expect(screen.getByRole("checkbox")).toBeInTheDocument(); + }); + + test("is unchecked when initial value is false", () => { + renderWithFormik({}); + expect(screen.getByRole("checkbox")).not.toBeChecked(); + }); + + test("is checked when initial value is true", () => { + renderWithFormik({}, { agreed: true }); + expect(screen.getByRole("checkbox")).toBeChecked(); + }); + + test("toggles checked state on click", async () => { + renderWithFormik({}); + const checkbox = screen.getByRole("checkbox"); + await userEvent.click(checkbox); + expect(checkbox).toBeChecked(); + }); + + test("shows error when touched and has error", () => { + render( + +
    + + +
    + ); + expect(screen.getByText("You must agree")).toBeInTheDocument(); + }); + + test("does not show error when not touched", () => { + render( + +
    + + +
    + ); + expect(screen.queryByText("You must agree")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-discountfield.test.js b/src/components/mui/__tests__/mui-formik-discountfield.test.js new file mode 100644 index 00000000..1be27d01 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-discountfield.test.js @@ -0,0 +1,67 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikDiscountField from "../formik-inputs/mui-formik-discountfield"; +import { DISCOUNT_TYPES } from "../../../utils/constants"; + +const renderWithFormik = (props, initialValues = { discount: 0 }) => + render( + +
    + + +
    + ); + +describe("MuiFormikDiscountField", () => { + test("renders % adornment for RATE discount type", () => { + renderWithFormik({ discountType: DISCOUNT_TYPES.RATE }); + expect(screen.getByText("%")).toBeInTheDocument(); + }); + + test("renders $ adornment for AMOUNT discount type", () => { + renderWithFormik({ discountType: DISCOUNT_TYPES.AMOUNT }); + expect(screen.getByText("$")).toBeInTheDocument(); + }); + + test("renders with label", () => { + renderWithFormik({ discountType: DISCOUNT_TYPES.RATE }); + expect(screen.getByText("Discount", { selector: "label" })).toBeInTheDocument(); + }); + + test("displays value as-is when inCents is false", () => { + renderWithFormik({ discountType: DISCOUNT_TYPES.RATE }, { discount: 10 }); + expect(screen.getByDisplayValue("10")).toBeInTheDocument(); + }); + + test("divides value by 100 when inCents is true", () => { + renderWithFormik( + { discountType: DISCOUNT_TYPES.AMOUNT, inCents: true }, + { discount: 500 } + ); + expect(screen.getByDisplayValue("5")).toBeInTheDocument(); + }); + + test("renders 0 when discount is 0", () => { + renderWithFormik({ discountType: DISCOUNT_TYPES.RATE }, { discount: 0 }); + expect(screen.getByDisplayValue("0")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-dropdown-checkbox.test.js b/src/components/mui/__tests__/mui-formik-dropdown-checkbox.test.js new file mode 100644 index 00000000..1a50f46c --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-dropdown-checkbox.test.js @@ -0,0 +1,75 @@ +/** + * 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. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikDropdownCheckbox from "../formik-inputs/mui-formik-dropdown-checkbox"; + +const options = [ + { value: 1, label: "Option A" }, + { value: 2, label: "Option B" }, + { value: 3, label: "Option C" } +]; + +const renderWithFormik = (props, initialValues = { testField: [] }) => + render( + +
    + + +
    + ); + +describe("MuiFormikDropdownCheckbox", () => { + test("renders a combobox", () => { + renderWithFormik({}); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); + + test("shows placeholder when no value selected", () => { + renderWithFormik({ placeholder: "Select options" }); + expect(screen.getByText("Select options")).toBeInTheDocument(); + }); + + test("shows 'all' label when all options are selected", () => { + renderWithFormik({}, { testField: [1, 2, 3] }); + expect(screen.getByText("general.all")).toBeInTheDocument(); + }); + + test("shows selected option names when some options selected", () => { + renderWithFormik({}, { testField: [1, 2] }); + expect(screen.getByText("Option A, Option B")).toBeInTheDocument(); + }); + + test("renders without errors with empty options array", () => { + const { container } = render( + +
    + + +
    + ); + expect(container.firstChild).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-dropdown-radio.test.js b/src/components/mui/__tests__/mui-formik-dropdown-radio.test.js new file mode 100644 index 00000000..9918cbc7 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-dropdown-radio.test.js @@ -0,0 +1,74 @@ +/** + * 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. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikDropdownRadio from "../formik-inputs/mui-formik-dropdown-radio"; + +const options = [ + { value: 1, label: "Option A" }, + { value: 2, label: "Option B" } +]; + +const renderWithFormik = (props, initialValues = { testField: "" }) => + render( + +
    + + +
    + ); + +describe("MuiFormikDropdownRadio", () => { + test("renders a combobox", () => { + renderWithFormik({}); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); + + test("shows default i18n placeholder when no value selected", () => { + renderWithFormik({}); + expect(screen.getByText("general.select_an_option")).toBeInTheDocument(); + }); + + test("shows custom placeholder when provided", () => { + renderWithFormik({ placeholder: "Pick one" }); + expect(screen.getByText("Pick one")).toBeInTheDocument(); + }); + + test("shows selected option label when value is set", () => { + renderWithFormik({}, { testField: 1 }); + expect(screen.getByText("Option A")).toBeInTheDocument(); + }); + + test("renders without errors with empty options", () => { + const { container } = render( + +
    + + +
    + ); + expect(container.firstChild).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-pricefield.test.js b/src/components/mui/__tests__/mui-formik-pricefield.test.js new file mode 100644 index 00000000..5a2b1104 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-pricefield.test.js @@ -0,0 +1,54 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikPriceField from "../formik-inputs/mui-formik-pricefield"; + +const renderWithFormik = (props, initialValues = { price: 0 }) => + render( + +
    + + +
    + ); + +describe("MuiFormikPriceField", () => { + test("renders with dollar sign adornment", () => { + renderWithFormik({}); + expect(screen.getByText("$")).toBeInTheDocument(); + }); + + test("renders with label", () => { + renderWithFormik({}); + expect(screen.getByText("Price", { selector: "label" })).toBeInTheDocument(); + }); + + test("displays value as-is when inCents is false", () => { + renderWithFormik({}, { price: 50 }); + expect(screen.getByDisplayValue("50")).toBeInTheDocument(); + }); + + test("divides value by 100 when inCents is true", () => { + renderWithFormik({ inCents: true }, { price: 1500 }); + expect(screen.getByDisplayValue("15")).toBeInTheDocument(); + }); + + test("renders 0 display value when price is 0", () => { + renderWithFormik({}, { price: 0 }); + expect(screen.getByDisplayValue("0")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-select-group.test.js b/src/components/mui/__tests__/mui-formik-select-group.test.js new file mode 100644 index 00000000..ceaf75b2 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-select-group.test.js @@ -0,0 +1,93 @@ +/** + * 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 { render, screen, waitFor } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikSelectGroup from "../formik-inputs/mui-formik-select-group"; + +const mockQueryFunction = jest.fn((callback) => { + callback([ + { id: 1, name: "Option A" }, + { id: 2, name: "Option B" } + ]); + return Promise.resolve(); +}); + +const renderWithFormik = (props, initialValues = { testField: [] }) => + render( + +
    + + +
    + ); + +describe("MuiFormikSelectGroup", () => { + beforeEach(() => jest.clearAllMocks()); + + test("renders with placeholder after loading", async () => { + renderWithFormik({}); + await waitFor(() => { + expect(screen.getByText("Select options")).toBeInTheDocument(); + }); + }); + + test("calls queryFunction on mount", async () => { + renderWithFormik({}); + await waitFor(() => expect(mockQueryFunction).toHaveBeenCalledTimes(1)); + }); + + test("renders a combobox", async () => { + renderWithFormik({}); + await waitFor(() => { + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); + }); + + test("shows error when touched and has error", async () => { + render( + +
    + + +
    + ); + await waitFor(() => { + expect(screen.getByText("Required")).toBeInTheDocument(); + }); + }); + + test("renders disabled when disabled prop is true", async () => { + renderWithFormik({ disabled: true }); + await waitFor(() => { + const combobox = screen.getByRole("combobox"); + expect(combobox).toHaveAttribute("aria-disabled", "true"); + }); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-select.test.js b/src/components/mui/__tests__/mui-formik-select.test.js new file mode 100644 index 00000000..c3ef7333 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-select.test.js @@ -0,0 +1,84 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import { MenuItem } from "@mui/material"; +import "@testing-library/jest-dom"; +import MuiFormikSelect from "../formik-inputs/mui-formik-select"; + +const renderWithFormik = (props, initialValues = { color: "" }) => + render( + +
    + + Red + Blue + +
    +
    + ); + +describe("MuiFormikSelect", () => { + test("renders a combobox", () => { + renderWithFormik({}); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); + + test("renders with label", () => { + renderWithFormik({ label: "Color" }); + expect(screen.getByText("Color", { selector: "label" })).toBeInTheDocument(); + }); + + test("shows placeholder when no value is selected", () => { + renderWithFormik({ placeholder: "Pick a color" }); + expect(screen.getByText("Pick a color")).toBeInTheDocument(); + }); + + test("shows error when touched and has error", () => { + render( + +
    + + Red + +
    +
    + ); + expect(screen.getByText("Required")).toBeInTheDocument(); + }); + + test("does not show error when not touched", () => { + render( + +
    + + Red + +
    +
    + ); + expect(screen.queryByText("Required")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-summit-addon-select.test.js b/src/components/mui/__tests__/mui-formik-summit-addon-select.test.js new file mode 100644 index 00000000..d610fc4c --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-summit-addon-select.test.js @@ -0,0 +1,83 @@ +/** + * 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. + * */ + +jest.mock("../summit-addon-select", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ value, placeholder, inputProps }) => ( +
    + {placeholder} +
    + ) + }; +}); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikSummitAddonSelect from "../formik-inputs/mui-formik-summit-addon-select"; + +const renderWithFormik = (props, initialValues = { addon: "" }) => + render( + +
    + + +
    + ); + +describe("MuiFormikSummitAddonSelect", () => { + test("renders the SummitAddonSelect", () => { + renderWithFormik({}); + expect(screen.getByTestId("summit-addon-select")).toBeInTheDocument(); + }); + + test("passes placeholder to SummitAddonSelect", () => { + renderWithFormik({ placeholder: "Choose addon" }); + expect(screen.getByText("Choose addon")).toBeInTheDocument(); + }); + + test("passes error to inputProps when touched and has error", () => { + render( + +
    + + +
    + ); + expect(screen.getByTestId("summit-addon-select")).toHaveAttribute( + "data-error", + "true" + ); + }); + + test("passes false error when not touched", () => { + renderWithFormik({}); + expect(screen.getByTestId("summit-addon-select")).toHaveAttribute( + "data-error", + "false" + ); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-switch.test.js b/src/components/mui/__tests__/mui-formik-switch.test.js new file mode 100644 index 00000000..d492330a --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-switch.test.js @@ -0,0 +1,73 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikSwitch from "../formik-inputs/mui-formik-switch"; + +const renderWithFormik = (props, initialValues = { enabled: false }) => + render( + +
    + + +
    + ); + +describe("MuiFormikSwitch", () => { + test("renders with label", () => { + renderWithFormik({}); + expect(screen.getByText("Enable Feature")).toBeInTheDocument(); + }); + + test("renders a switch (checkbox role)", () => { + renderWithFormik({}); + expect(screen.getByRole("checkbox")).toBeInTheDocument(); + }); + + test("is off when initial value is false", () => { + renderWithFormik({}); + expect(screen.getByRole("checkbox")).not.toBeChecked(); + }); + + test("is on when initial value is true", () => { + renderWithFormik({}, { enabled: true }); + expect(screen.getByRole("checkbox")).toBeChecked(); + }); + + test("toggles on click", async () => { + renderWithFormik({}); + const toggle = screen.getByRole("checkbox"); + await userEvent.click(toggle); + expect(toggle).toBeChecked(); + }); + + test("shows error when touched and has error", () => { + render( + +
    + + +
    + ); + expect(screen.getByText("Required")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-textfield.test.js b/src/components/mui/__tests__/mui-formik-textfield.test.js new file mode 100644 index 00000000..ba11c3c4 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-textfield.test.js @@ -0,0 +1,91 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikTextField from "../formik-inputs/mui-formik-textfield"; + +const renderWithFormik = (props, initialValues = { testField: "" }) => + render( + +
    + + +
    + ); + +describe("MuiFormikTextField", () => { + test("renders a text input", () => { + renderWithFormik({}); + expect(screen.getByRole("textbox")).toBeInTheDocument(); + }); + + test("renders with label", () => { + renderWithFormik({ label: "My Field" }); + expect(screen.getByText("My Field", { selector: "label" })).toBeInTheDocument(); + }); + + test("appends * to label when required is true", () => { + renderWithFormik({ label: "My Field", required: true }); + expect(screen.getByText("My Field *", { selector: "label" })).toBeInTheDocument(); + }); + + test("shows character count when maxLength is set", () => { + renderWithFormik({ maxLength: 100 }, { testField: "hello" }); + expect(screen.getByText("95 characters left")).toBeInTheDocument(); + }); + + test("shows full maxLength count when field is empty", () => { + renderWithFormik({ maxLength: 50 }, { testField: "" }); + expect(screen.getByText("50 characters left")).toBeInTheDocument(); + }); + + test("shows error message when touched and has error", () => { + render( + +
    + + +
    + ); + expect(screen.getByText("This field is required")).toBeInTheDocument(); + }); + + test("does not show error when field is not touched", () => { + render( + +
    + + +
    + ); + expect(screen.queryByText("Error")).not.toBeInTheDocument(); + }); + + test("renders with initial value", () => { + renderWithFormik({}, { testField: "initial value" }); + expect(screen.getByDisplayValue("initial value")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-timepicker.test.js b/src/components/mui/__tests__/mui-formik-timepicker.test.js new file mode 100644 index 00000000..d45d307c --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-timepicker.test.js @@ -0,0 +1,87 @@ +/** + * 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. + * */ + +jest.mock("@mui/x-date-pickers/LocalizationProvider", () => ({ + LocalizationProvider: ({ children }) => children +})); + +jest.mock("@mui/x-date-pickers/AdapterMoment", () => ({ + AdapterMoment: function AdapterMoment() {} +})); + +jest.mock("@mui/x-date-pickers/TimePicker", () => ({ + TimePicker: ({ slotProps }) => { + const React = require("react"); + const tf = slotProps?.textField || {}; + return ( +
    + + {tf.error && {tf.helperText}} +
    + ); + } +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikTimepicker from "../formik-inputs/mui-formik-timepicker"; + +const renderWithFormik = (props, initialValues = { timeField: null }) => + render( + +
    + + +
    + ); + +describe("MuiFormikTimepicker", () => { + test("renders the timepicker input", () => { + renderWithFormik({}); + expect(screen.getByTestId("timepicker-input")).toBeInTheDocument(); + }); + + test("renders as disabled when disabled prop is true", () => { + renderWithFormik({ disabled: true }); + expect(screen.getByTestId("timepicker-input")).toBeDisabled(); + }); + + test("renders as enabled by default", () => { + renderWithFormik({}); + expect(screen.getByTestId("timepicker-input")).not.toBeDisabled(); + }); + + test("shows error when touched and has error", () => { + render( + +
    + + +
    + ); + expect(screen.getByTestId("timepicker-error")).toHaveTextContent( + "Invalid time" + ); + }); +}); diff --git a/src/components/mui/__tests__/mui-formik-upload.test.js b/src/components/mui/__tests__/mui-formik-upload.test.js new file mode 100644 index 00000000..54cd4fc6 --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-upload.test.js @@ -0,0 +1,92 @@ +/** + * 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. + * */ + +jest.mock("../../inputs/upload-input-v2", () => ({ + UploadInputV2: ({ value, canAdd }) => { + const React = require("react"); + return ( +
    + {value.length} file(s) +
    + ); + } +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikUpload from "../formik-inputs/mui-formik-upload"; + +const renderWithFormik = (props, initialValues = { images: [] }) => + render( + +
    + + +
    + ); + +describe("MuiFormikUpload", () => { + test("renders the upload input component", () => { + renderWithFormik({}); + expect(screen.getByTestId("upload-input")).toBeInTheDocument(); + }); + + test("shows 0 files when initial value is empty", () => { + renderWithFormik({}); + expect(screen.getByText("0 file(s)")).toBeInTheDocument(); + }); + + test("shows file count matching initial value", () => { + const images = [ + { id: 1, file_name: "a.jpg" }, + { id: 2, file_name: "b.jpg" } + ]; + renderWithFormik({}, { images }); + expect(screen.getByText("2 file(s)")).toBeInTheDocument(); + }); + + test("canAdd is true when file count is below maxFiles", () => { + renderWithFormik({ maxFiles: 5 }, { images: [] }); + expect(screen.getByTestId("upload-input")).toHaveAttribute( + "data-can-add", + "true" + ); + }); + + test("canAdd is false when file count equals maxFiles", () => { + const images = [{ id: 1 }, { id: 2 }]; + renderWithFormik({ maxFiles: 2 }, { images }); + expect(screen.getByTestId("upload-input")).toHaveAttribute( + "data-can-add", + "false" + ); + }); + + test("shows error when touched and has error", () => { + render( + +
    + + +
    + ); + expect(screen.getByText("Required")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-infinite-table.test.js b/src/components/mui/__tests__/mui-infinite-table.test.js new file mode 100644 index 00000000..bd8d1bb6 --- /dev/null +++ b/src/components/mui/__tests__/mui-infinite-table.test.js @@ -0,0 +1,92 @@ +/** + * 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. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import MuiInfiniteTable from "../infinite-table/index"; + +const columns = [ + { columnKey: "name", header: "Name", sortable: false }, + { columnKey: "email", header: "Email", sortable: true } +]; + +const data = [ + { id: 1, name: "Alice", email: "alice@test.com" }, + { id: 2, name: "Bob", email: "bob@test.com" } +]; + +const setup = (overrides = {}) => { + const props = { + columns, + data, + loadMoreData: jest.fn(), + onRowEdit: jest.fn(), + onSort: jest.fn(), + options: { sortCol: "", sortDir: "" }, + ...overrides + }; + render(); + return props; +}; + +describe("MuiInfiniteTable", () => { + test("renders column headers", () => { + setup(); + expect(screen.getByText("Name")).toBeInTheDocument(); + expect(screen.getByText("Email")).toBeInTheDocument(); + }); + + test("renders data rows", () => { + setup(); + expect(screen.getByText("Alice")).toBeInTheDocument(); + expect(screen.getByText("bob@test.com")).toBeInTheDocument(); + }); + + test("shows no-data message when data is empty", () => { + setup({ data: [] }); + expect(screen.getByText("mui_table.no_data")).toBeInTheDocument(); + }); + + test("renders sortable columns with sort labels", () => { + setup({ options: { sortCol: "email", sortDir: 1 } }); + // Email column is sortable, should have a sort button + expect(screen.getByText("Email")).toBeInTheDocument(); + }); + + test("renders custom cell content via col.render", () => { + const customColumns = [ + { + columnKey: "name", + header: "Name", + render: (row) => {row.name.toUpperCase()} + } + ]; + render( + + ); + expect(screen.getByText("ALICE")).toBeInTheDocument(); + expect(screen.getByText("BOB")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/mui-table-sortable.test.js b/src/components/mui/__tests__/mui-table-sortable.test.js new file mode 100644 index 00000000..ccd7474a --- /dev/null +++ b/src/components/mui/__tests__/mui-table-sortable.test.js @@ -0,0 +1,166 @@ +/** + * 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. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +jest.mock("../showConfirmDialog", () => ({ + __esModule: true, + default: jest.fn() +})); + +jest.mock("react-beautiful-dnd", () => { + const React = require("react"); + return { + DragDropContext: ({ children }) => <>{children}, + Droppable: ({ children }) => + children( + { innerRef: jest.fn(), droppableProps: {}, placeholder: null }, + {} + ), + Draggable: ({ children }) => + children( + { innerRef: jest.fn(), draggableProps: {}, dragHandleProps: {} }, + { isDragging: false } + ) + }; +}); + +jest.mock("@mui/material/TablePagination", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ count, page, onPageChange, onRowsPerPageChange, rowsPerPageOptions }) => ( +
    + count:{count} + + +
    + ) + }; +}); + +import React from "react"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import MuiTableSortable from "../sortable-table/mui-table-sortable"; +import showConfirmDialog from "../showConfirmDialog"; + +const columns = [ + { columnKey: "name", header: "Name", sortable: true }, + { columnKey: "role", header: "Role", sortable: false } +]; + +const data = [ + { id: 1, name: "Alice", role: "Dev", order: 1 }, + { id: 2, name: "Bob", role: "PM", order: 2 } +]; + +const setup = (overrides = {}) => { + const props = { + columns, + data, + totalRows: 2, + perPage: 10, + currentPage: 1, + onPageChange: jest.fn(), + onPerPageChange: jest.fn(), + onSort: jest.fn(), + options: { sortCol: "name", sortDir: 1 }, + ...overrides + }; + render(); + return props; +}; + +describe("MuiTableSortable", () => { + beforeEach(() => jest.clearAllMocks()); + + test("renders column headers", () => { + setup(); + expect(screen.getByText("Name")).toBeInTheDocument(); + expect(screen.getByText("Role")).toBeInTheDocument(); + }); + + test("renders data rows", () => { + setup(); + expect(screen.getByText("Alice")).toBeInTheDocument(); + expect(screen.getByText("Bob")).toBeInTheDocument(); + }); + + test("shows no-items message when data is empty", () => { + setup({ data: [] }); + expect(screen.getByText("mui_table.no_items")).toBeInTheDocument(); + }); + + test("renders edit button when onEdit is provided", () => { + const onEdit = jest.fn(); + setup({ onEdit }); + expect(screen.getAllByRole("button").length).toBeGreaterThan(0); + }); + + test("calls onEdit when edit button is clicked", async () => { + const onEdit = jest.fn(); + setup({ onEdit }); + const buttons = screen.getAllByRole("button"); + // buttons[0] is the sort label button for the sortable "Name" column; + // buttons[1] is the first edit button (row 1) + await userEvent.click(buttons[1]); + expect(onEdit).toHaveBeenCalledWith(expect.objectContaining({ id: 1 })); + }); + + test("calls showConfirmDialog and onDelete when delete is confirmed", async () => { + const onDelete = jest.fn(); + showConfirmDialog.mockResolvedValueOnce(true); + setup({ onDelete }); + const buttons = screen.getAllByRole("button"); + // buttons[0] is the sort label button; buttons[1] is the first delete button (row 1) + await userEvent.click(buttons[1]); + await new Promise((r) => setTimeout(r, 0)); + expect(showConfirmDialog).toHaveBeenCalled(); + expect(onDelete).toHaveBeenCalledWith(1); + }); + + test("renders pagination", () => { + setup(); + expect(screen.getByTestId("pagination")).toBeInTheDocument(); + }); + + test("calls onPageChange when next page is clicked", async () => { + const onPageChange = jest.fn(); + setup({ onPageChange, currentPage: 1 }); + await userEvent.click( + within(screen.getByTestId("pagination")).getByRole("button", { + name: "next-page" + }) + ); + expect(onPageChange).toHaveBeenCalledWith(2); + }); +}); diff --git a/src/components/mui/__tests__/mui-table.test.js b/src/components/mui/__tests__/mui-table.test.js new file mode 100644 index 00000000..b8a0920a --- /dev/null +++ b/src/components/mui/__tests__/mui-table.test.js @@ -0,0 +1,192 @@ +/** + * 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. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +jest.mock("../showConfirmDialog", () => ({ + __esModule: true, + default: jest.fn() +})); + +jest.mock("@mui/material/TablePagination", () => { + const React = require("react"); + return { + __esModule: true, + default: ({ count, rowsPerPage, page, onPageChange, onRowsPerPageChange }) => ( +
    + count:{count} + page:{page} + + +
    + ) + }; +}); + +import React from "react"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import MuiTable from "../table/mui-table"; +import showConfirmDialog from "../showConfirmDialog"; + +const columns = [ + { columnKey: "name", header: "Name" }, + { columnKey: "role", header: "Role" } +]; + +const data = [ + { id: 1, name: "Alice", role: "Dev" }, + { id: 2, name: "Bob", role: "PM" } +]; + +const setup = (overrides = {}) => { + const props = { + columns, + data, + totalRows: 2, + perPage: 10, + currentPage: 1, + onPageChange: jest.fn(), + onPerPageChange: jest.fn(), + onSort: jest.fn(), + ...overrides + }; + render(); + return props; +}; + +describe("MuiTable", () => { + beforeEach(() => jest.clearAllMocks()); + + test("renders column headers", () => { + setup(); + expect(screen.getByText("Name")).toBeInTheDocument(); + expect(screen.getByText("Role")).toBeInTheDocument(); + }); + + test("renders data rows", () => { + setup(); + expect(screen.getByText("Alice")).toBeInTheDocument(); + expect(screen.getByText("Bob")).toBeInTheDocument(); + }); + + test("shows no-items message when data is empty", () => { + setup({ data: [] }); + expect(screen.getByText("mui_table.no_items")).toBeInTheDocument(); + }); + + test("renders edit button when onEdit is provided", () => { + setup({ onEdit: jest.fn() }); + const editBtns = screen.getAllByRole("button"); + expect(editBtns.length).toBeGreaterThan(0); + }); + + test("calls onEdit when edit button is clicked", async () => { + const onEdit = jest.fn(); + setup({ onEdit }); + const buttons = screen.getAllByRole("button"); + await userEvent.click(buttons[0]); + expect(onEdit).toHaveBeenCalledWith( + expect.objectContaining({ id: 1 }) + ); + }); + + test("calls showConfirmDialog and then onDelete when delete confirmed", async () => { + const onDelete = jest.fn(); + showConfirmDialog.mockResolvedValueOnce(true); + setup({ onDelete }); + const buttons = screen.getAllByRole("button"); + await userEvent.click(buttons[0]); + await new Promise((r) => setTimeout(r, 0)); + expect(showConfirmDialog).toHaveBeenCalled(); + expect(onDelete).toHaveBeenCalledWith(1); + }); + + test("does not call onDelete when delete is cancelled", async () => { + const onDelete = jest.fn(); + showConfirmDialog.mockResolvedValueOnce(false); + setup({ onDelete }); + const buttons = screen.getAllByRole("button"); + await userEvent.click(buttons[0]); + await new Promise((r) => setTimeout(r, 0)); + expect(onDelete).not.toHaveBeenCalled(); + }); + + test("renders pagination when perPage and currentPage are set", () => { + setup(); + expect(screen.getByTestId("pagination")).toBeInTheDocument(); + }); + + test("pagination shows correct count", () => { + setup({ totalRows: 50 }); + expect( + within(screen.getByTestId("pagination")).getByText("count:50") + ).toBeInTheDocument(); + }); + + test("calls onPageChange when next page button clicked", async () => { + const onPageChange = jest.fn(); + setup({ onPageChange, currentPage: 1 }); + await userEvent.click( + within(screen.getByTestId("pagination")).getByRole("button", { + name: "next-page" + }) + ); + expect(onPageChange).toHaveBeenCalledWith(2); + }); + + test("calls onPerPageChange when rows-per-page button clicked", async () => { + const onPerPageChange = jest.fn(); + setup({ onPerPageChange }); + await userEvent.click( + within(screen.getByTestId("pagination")).getByRole("button", { + name: "change-rows" + }) + ); + expect(onPerPageChange).toHaveBeenCalledWith(20); + }); + + test("renders boolean true as CheckIcon", () => { + const boolCols = [{ columnKey: "active", header: "Active" }]; + render( + + ); + // MUI CheckIcon renders an SVG; just ensure no error + expect(screen.getByRole("cell", { hidden: true })).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/notes-modal.test.js b/src/components/mui/__tests__/notes-modal.test.js new file mode 100644 index 00000000..1bdd88f6 --- /dev/null +++ b/src/components/mui/__tests__/notes-modal.test.js @@ -0,0 +1,81 @@ +/** + * 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. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import NotesModal from "../NotesModal/index"; + +const item = { form_item_id: 1, name: "Test Item" }; +const fieldName = `i-${item.form_item_id}-c-global-f-notes`; + +const renderModal = (props) => + render( + +
    + + +
    + ); + +describe("NotesModal", () => { + test("renders dialog title when open", () => { + renderModal({}); + expect( + screen.getByText("sponsor_edit_form.notes") + ).toBeInTheDocument(); + }); + + test("renders item name", () => { + renderModal({}); + expect(screen.getByText("Test Item")).toBeInTheDocument(); + }); + + test("renders a textarea for notes", () => { + renderModal({}); + expect(screen.getByRole("textbox")).toBeInTheDocument(); + }); + + test("calls onClose when close icon button is clicked", async () => { + const onClose = jest.fn(); + renderModal({ onClose }); + const closeBtn = screen.getByRole("button", { name: /close/i }); + await userEvent.click(closeBtn); + expect(onClose).toHaveBeenCalled(); + }); + + test("calls onClose when save button is clicked", async () => { + const onClose = jest.fn(); + renderModal({ onClose }); + const saveBtn = screen.getByText("general.save"); + await userEvent.click(saveBtn); + expect(onClose).toHaveBeenCalled(); + }); + + test("does not render dialog content when open is false", () => { + renderModal({ open: false }); + expect( + screen.queryByText("sponsor_edit_form.notes") + ).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/notes-row.test.js b/src/components/mui/__tests__/notes-row.test.js new file mode 100644 index 00000000..e4ad2abd --- /dev/null +++ b/src/components/mui/__tests__/notes-row.test.js @@ -0,0 +1,49 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import NotesRow from "../table/extra-rows/NotesRow"; + +const renderInTable = (props) => + render( + + + + +
    + ); + +describe("NotesRow", () => { + test("renders the note text", () => { + renderInTable({ colCount: 3, note: "This is a note" }); + expect(screen.getByText("This is a note")).toBeInTheDocument(); + }); + + test("renders a single table row", () => { + const { container } = renderInTable({ colCount: 3, note: "Note" }); + expect(container.querySelectorAll("tr")).toHaveLength(1); + }); + + test("renders a single cell spanning colCount columns", () => { + const { container } = renderInTable({ colCount: 5, note: "Note" }); + const cell = container.querySelector("td"); + expect(cell).toHaveAttribute("colspan", "5"); + }); + + test("renders empty note text", () => { + const { container } = renderInTable({ colCount: 2, note: "" }); + expect(container.querySelector("td")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/search-input.test.js b/src/components/mui/__tests__/search-input.test.js new file mode 100644 index 00000000..ad8f8533 --- /dev/null +++ b/src/components/mui/__tests__/search-input.test.js @@ -0,0 +1,61 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import SearchInput from "../search-input"; + +describe("SearchInput", () => { + test("renders with custom placeholder", () => { + render(); + expect(screen.getByPlaceholderText("Find items...")).toBeInTheDocument(); + }); + + test("renders with default placeholder", () => { + render(); + expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument(); + }); + + test("calls onSearch when Enter is pressed", async () => { + const onSearch = jest.fn(); + render(); + const input = screen.getByPlaceholderText("Search..."); + await userEvent.type(input, "hello{Enter}"); + expect(onSearch).toHaveBeenCalledWith("hello"); + }); + + test("shows clear button when term is provided", () => { + render(); + expect(screen.getByRole("button")).toBeInTheDocument(); + }); + + test("calls onSearch with empty string when clear button is clicked", async () => { + const onSearch = jest.fn(); + render(); + await userEvent.click(screen.getByRole("button")); + expect(onSearch).toHaveBeenCalledWith(""); + }); + + test("initializes with provided term value", () => { + render(); + expect(screen.getByDisplayValue("initial")).toBeInTheDocument(); + }); + + test("syncs input when term prop changes", () => { + const { rerender } = render(); + rerender(); + expect(screen.getByDisplayValue("new")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/show-confirm-dialog.test.js b/src/components/mui/__tests__/show-confirm-dialog.test.js new file mode 100644 index 00000000..d547912b --- /dev/null +++ b/src/components/mui/__tests__/show-confirm-dialog.test.js @@ -0,0 +1,59 @@ +/** + * 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. + * */ + +jest.mock("react-dom", () => ({ + render: jest.fn(), + unmountComponentAtNode: jest.fn() +})); + +jest.mock("../confirm-dialog", () => { + const React = require("react"); + return { __esModule: true, default: () =>
    }; +}); + +import showConfirmDialog from "../showConfirmDialog"; +import ReactDOM from "react-dom"; + +describe("showConfirmDialog", () => { + beforeEach(() => jest.clearAllMocks()); + + test("returns a Promise", () => { + const result = showConfirmDialog({ title: "Test", text: "Body" }); + expect(result).toBeInstanceOf(Promise); + }); + + test("calls ReactDOM.render to mount the dialog", () => { + showConfirmDialog({ title: "Test", text: "Body" }); + expect(ReactDOM.render).toHaveBeenCalledTimes(1); + }); + + test("appends a container div to the document body", () => { + const initialChildCount = document.body.children.length; + showConfirmDialog({ title: "Test", text: "Body" }); + expect(document.body.children.length).toBeGreaterThan(initialChildCount); + }); + + test("passes title and text to ConfirmDialog", () => { + showConfirmDialog({ + title: "My Title", + text: "My Text", + confirmButtonText: "Yes", + cancelButtonText: "No" + }); + const [element] = ReactDOM.render.mock.calls[0]; + expect(element.props.title).toBe("My Title"); + expect(element.props.text).toBe("My Text"); + expect(element.props.confirmButtonText).toBe("Yes"); + expect(element.props.cancelButtonText).toBe("No"); + }); +}); diff --git a/src/components/mui/__tests__/snackbar-notification-context.test.js b/src/components/mui/__tests__/snackbar-notification-context.test.js new file mode 100644 index 00000000..df62b87a --- /dev/null +++ b/src/components/mui/__tests__/snackbar-notification-context.test.js @@ -0,0 +1,59 @@ +/** + * 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 { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import SnackbarNotificationContext, { + useSnackbarMessage +} from "../SnackbarNotification/Context"; + +const TestConsumer = () => { + const ctx = useSnackbarMessage(); + return
    {ctx ? "has-context" : "no-context"}
    ; +}; + +describe("SnackbarNotificationContext", () => { + test("provides null by default (no provider)", () => { + render(); + expect(screen.getByText("no-context")).toBeInTheDocument(); + }); + + test("provides value when wrapped in provider", () => { + const value = { + successMessage: jest.fn(), + errorMessage: jest.fn() + }; + render( + + + + ); + expect(screen.getByText("has-context")).toBeInTheDocument(); + }); + + test("useSnackbarMessage returns the context value", () => { + const value = { successMessage: jest.fn(), errorMessage: jest.fn() }; + let captured = null; + const Capturer = () => { + captured = useSnackbarMessage(); + return null; + }; + render( + + + + ); + expect(captured).toBe(value); + }); +}); diff --git a/src/components/mui/__tests__/snackbar-notification.test.js b/src/components/mui/__tests__/snackbar-notification.test.js new file mode 100644 index 00000000..b14a52f5 --- /dev/null +++ b/src/components/mui/__tests__/snackbar-notification.test.js @@ -0,0 +1,73 @@ +/** + * 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. + * */ + +jest.mock("react-redux", () => ({ + connect: () => (Component) => Component +})); + +jest.mock("../../../utils/actions", () => ({ + clearSnackbarMessage: jest.fn() +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import SnackbarNotification from "../SnackbarNotification/index"; + +describe("SnackbarNotification", () => { + test("renders children", () => { + render( + +
    Child content
    +
    + ); + expect(screen.getByText("Child content")).toBeInTheDocument(); + }); + + test("shows snackbar when snackbarMessage has html content", () => { + render( + +
    App
    +
    + ); + expect(screen.getByText("Operation successful!")).toBeInTheDocument(); + }); + + test("shows error snackbar when type is warning", () => { + render( + +
    App
    +
    + ); + expect(screen.getByText("Something went wrong")).toBeInTheDocument(); + }); + + test("does not show snackbar when snackbarMessage is null", () => { + render( + +
    App
    +
    + ); + // The snackbar should not be open + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/sponsor-addon-select.test.js b/src/components/mui/__tests__/sponsor-addon-select.test.js new file mode 100644 index 00000000..45392cf1 --- /dev/null +++ b/src/components/mui/__tests__/sponsor-addon-select.test.js @@ -0,0 +1,57 @@ +/** + * 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. + * */ + +jest.mock("../../../utils/query-actions", () => ({ + querySponsorAddons: jest.fn((summitId, sponsorId, sponsorshipIds, callback) => { + callback([ + { id: 1, name: "Addon A" }, + { id: 2, name: "Addon B" } + ]); + }) +})); + +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import SponsorAddonSelect from "../sponsor-addon-select"; + +describe("SponsorAddonSelect", () => { + const defaultProps = { + value: null, + summitId: 1, + sponsor: { id: 10, sponsorships: [{ id: 100 }, { id: 101 }] }, + onChange: jest.fn(), + placeholder: "Select addon..." + }; + + beforeEach(() => jest.clearAllMocks()); + + test("renders without errors", () => { + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + }); + + test("calls querySponsorAddons on mount with correct args", async () => { + render(); + await waitFor(() => { + expect( + require("../../../utils/query-actions").querySponsorAddons + ).toHaveBeenCalledWith(1, 10, [100, 101], expect.any(Function)); + }); + }); + + test("renders a select combobox", () => { + const { container } = render(); + expect(container.querySelector("[role='combobox']")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/sponsorship-input-mui.test.js b/src/components/mui/__tests__/sponsorship-input-mui.test.js new file mode 100644 index 00000000..f0008457 --- /dev/null +++ b/src/components/mui/__tests__/sponsorship-input-mui.test.js @@ -0,0 +1,84 @@ +/** + * 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. + * */ + +jest.mock("../../../utils/query-actions", () => ({ + querySponsorships: jest.fn() +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import SponsorshipTypeInputMUI from "../formik-inputs/sponsorship-input-mui"; + +const renderWithFormik = (props, initialValues = { sponsorship: null }) => + render( + +
    + + +
    + ); + +describe("SponsorshipTypeInputMUI", () => { + beforeEach(() => jest.clearAllMocks()); + + test("renders with placeholder", () => { + renderWithFormik({}); + expect( + screen.getByPlaceholderText("Search sponsorships...") + ).toBeInTheDocument(); + }); + + test("renders with preselected single value", () => { + renderWithFormik( + {}, + { sponsorship: { id: 1, name: "Gold Sponsor" } } + ); + expect(screen.getByDisplayValue("Gold Sponsor")).toBeInTheDocument(); + }); + + test("renders with multiple preselected values", () => { + renderWithFormik( + { isMulti: true }, + { + sponsorship: [ + { id: 1, name: "Gold Sponsor" }, + { id: 2, name: "Silver Sponsor" } + ] + } + ); + expect(screen.getByText("Gold Sponsor")).toBeInTheDocument(); + expect(screen.getByText("Silver Sponsor")).toBeInTheDocument(); + }); + + test("shows error when touched and has error", () => { + render( + +
    + + +
    + ); + expect(screen.getByText("Required")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/sponsorship-summit-select-mui.test.js b/src/components/mui/__tests__/sponsorship-summit-select-mui.test.js new file mode 100644 index 00000000..37c44e72 --- /dev/null +++ b/src/components/mui/__tests__/sponsorship-summit-select-mui.test.js @@ -0,0 +1,85 @@ +/** + * 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. + * */ + +jest.mock("../../../utils/query-actions", () => ({ + querySponsorshipsBySummit: jest.fn((input, summitId, callback) => { + callback([ + { id: 1, type: { name: "Gold" } }, + { id: 2, type: { name: "Silver" } } + ]); + return Promise.resolve(); + }) +})); + +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import SponsorshipsBySummitSelectMUI from "../formik-inputs/sponsorship-summit-select-mui"; + +const renderWithFormik = (props, initialValues = { sponsorships: [] }) => + render( + +
    + + +
    + ); + +describe("SponsorshipsBySummitSelectMUI", () => { + beforeEach(() => jest.clearAllMocks()); + + test("renders without errors", () => { + const { container } = renderWithFormik({}); + expect(container.firstChild).toBeInTheDocument(); + }); + + test("shows placeholder when no value selected", () => { + renderWithFormik({}); + expect(screen.getByText("Select sponsorships...")).toBeInTheDocument(); + }); + + test("fetches sponsorships by summit on mount", async () => { + renderWithFormik({}); + await waitFor(() => { + expect( + require("../../../utils/query-actions").querySponsorshipsBySummit + ).toHaveBeenCalledWith("", 1, expect.any(Function)); + }); + }); + + test("shows error when touched and has error", () => { + render( + +
    + + +
    + ); + expect(screen.getByText("Required")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/summit-addon-select.test.js b/src/components/mui/__tests__/summit-addon-select.test.js new file mode 100644 index 00000000..fa1a415e --- /dev/null +++ b/src/components/mui/__tests__/summit-addon-select.test.js @@ -0,0 +1,59 @@ +/** + * 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. + * */ + +jest.mock("../../../utils/query-actions", () => ({ + querySummitAddons: jest.fn((summitId, callback) => { + callback(["Addon Alpha", "Addon Beta"]); + }) +})); + +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import SummitAddonSelect from "../summit-addon-select"; + +describe("SummitAddonSelect", () => { + const defaultProps = { + value: "", + summitId: 5, + onChange: jest.fn(), + placeholder: "Select addon..." + }; + + beforeEach(() => jest.clearAllMocks()); + + test("renders a select combobox", () => { + const { container } = render(); + expect(container.querySelector("[role='combobox']")).toBeInTheDocument(); + }); + + test("calls querySummitAddons on mount with summitId", async () => { + render(); + await waitFor(() => { + expect( + require("../../../utils/query-actions").querySummitAddons + ).toHaveBeenCalledWith(5, expect.any(Function)); + }); + }); + + test("renders options returned by querySummitAddons", async () => { + render(); + // Options are rendered inside the Select's listbox - click to open + // Just verify the component renders without errors after options load + await waitFor(() => { + expect( + require("../../../utils/query-actions").querySummitAddons + ).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/mui/__tests__/summits-dropdown.test.js b/src/components/mui/__tests__/summits-dropdown.test.js new file mode 100644 index 00000000..eb1d0ba0 --- /dev/null +++ b/src/components/mui/__tests__/summits-dropdown.test.js @@ -0,0 +1,88 @@ +/** + * 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. + * */ + +jest.mock("../../../utils/query-actions", () => ({ + fetchAllSummits: jest.fn(() => + Promise.resolve([ + { id: 10, name: "Summit Alpha" }, + { id: 11, name: "Summit Beta" } + ]) + ) +})); + +jest.mock("i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +import React from "react"; +import { render, screen, waitFor, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import SummitsDropdown from "../summits-dropdown"; + +describe("SummitsDropdown", () => { + beforeEach(() => jest.clearAllMocks()); + + test("renders provided summits as options", () => { + const summits = [ + { id: 1, name: "Summit One" }, + { id: 2, name: "Summit Two" } + ]; + render(); + fireEvent.mouseDown(screen.getByRole("combobox")); + expect(screen.getByText("Summit One")).toBeInTheDocument(); + expect(screen.getByText("Summit Two")).toBeInTheDocument(); + }); + + test("renders the label", () => { + render( + + ); + expect(screen.getByText("Choose Summit")).toBeInTheDocument(); + }); + + test("fetches summits when summits prop is empty", async () => { + render(); + await waitFor(() => { + expect( + require("../../../utils/query-actions").fetchAllSummits + ).toHaveBeenCalled(); + }); + }); + + test("does not fetch summits when summits prop is provided", () => { + render( + + ); + expect( + require("../../../utils/query-actions").fetchAllSummits + ).not.toHaveBeenCalled(); + }); + + test("renders a select combobox", () => { + render( + + ); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/__tests__/total-row.test.js b/src/components/mui/__tests__/total-row.test.js new file mode 100644 index 00000000..5fcc3100 --- /dev/null +++ b/src/components/mui/__tests__/total-row.test.js @@ -0,0 +1,68 @@ +/** + * 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. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import TotalRow from "../table/extra-rows/TotalRow"; + +const columns = [ + { columnKey: "name", header: "Name" }, + { columnKey: "quantity", header: "Qty" }, + { columnKey: "price", header: "Price" } +]; + +const renderInTable = (props) => + render( + + + + +
    + ); + +describe("TotalRow", () => { + test("renders 'TOTAL' label key in first column", () => { + renderInTable({ targetCol: "quantity", total: 42 }); + expect(screen.getByText("mui_table.total")).toBeInTheDocument(); + }); + + test("renders total value in the targetCol", () => { + renderInTable({ targetCol: "quantity", total: 42 }); + expect(screen.getByText("42")).toBeInTheDocument(); + }); + + test("renders correct number of cells (one per column)", () => { + const { container } = renderInTable({ targetCol: "quantity", total: 10 }); + expect(container.querySelectorAll("td")).toHaveLength(columns.length); + }); + + test("renders extra trailing cells when trailing prop is provided", () => { + const { container } = renderInTable({ + targetCol: "quantity", + total: 10, + trailing: 2 + }); + expect(container.querySelectorAll("td")).toHaveLength(columns.length + 2); + }); + + test("renders string totals", () => { + renderInTable({ targetCol: "price", total: "$1,234" }); + expect(screen.getByText("$1,234")).toBeInTheDocument(); + }); +}); From ce165f074c6c1e8d2a1aa6bac1ac62736628ba9c Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Mon, 30 Mar 2026 15:09:55 -0300 Subject: [PATCH 3/8] fix: change react version to match admin, fix queries --- package.json | 8 +- .../__test__/extra-questions.test.js | 55 +++-- .../__tests__/upload-input-v3.test.js | 229 ++++++------------ src/utils/actions.js | 1 + src/utils/constants.js | 2 + src/utils/query-actions.js | 29 +-- yarn.lock | 4 +- 7 files changed, 120 insertions(+), 208 deletions(-) diff --git a/package.json b/package.json index 74bf5a51..3dc048bd 100644 --- a/package.json +++ b/package.json @@ -70,13 +70,13 @@ "node-sass": "^7.0.1", "path": "^0.12.7", "postcss-loader": "^6.2.1", - "react": "^16.6.3", + "react": "^16.13.1", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^0.31.5", "react-datetime": "^2.16.2", "react-dnd": "^16.0.0", "react-dnd-html5-backend": "^16.0.0", - "react-dom": "^16.4.1", + "react-dom": "^16.13.1", "react-dropzone": "^4.2.9", "react-final-form": "^6.5.9", "react-google-maps": "^9.4.5", @@ -133,13 +133,13 @@ "lodash": "^4.17.14", "moment": "^2.22.2", "moment-timezone": "^0.5.21", - "react": "^16.6.3", + "react": "^16.13.1", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^0.31.5", "react-datetime": "^2.16.2", "react-dnd": "^16.0.0", "react-dnd-html5-backend": "^16.0.0", - "react-dom": "^16.4.1", + "react-dom": "^16.13.1", "react-dropzone": "^4.2.9", "react-final-form": "^6.5.9", "react-google-maps": "^9.4.5", diff --git a/src/components/extra-questions/__test__/extra-questions.test.js b/src/components/extra-questions/__test__/extra-questions.test.js index 6e7e4f54..fe62cd69 100644 --- a/src/components/extra-questions/__test__/extra-questions.test.js +++ b/src/components/extra-questions/__test__/extra-questions.test.js @@ -2,15 +2,11 @@ * @jest-environment jsdom */ import React from 'react'; +import { render, act } from '@testing-library/react'; +import '@testing-library/jest-dom'; import ExtraQuestionsForm from '..'; -import Enzyme, {mount} from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import Input from '../../inputs/text-input'; -import Dropdown from '../../inputs/dropdown'; import {toSlug} from '../../../utils/methods'; -Enzyme.configure({adapter: new Adapter()}); - // jsdom does not implement scrollIntoView Element.prototype.scrollIntoView = jest.fn(); @@ -1067,7 +1063,7 @@ const completeAnswers2 = [ ]; it('has input', () => { - const component = mount( + const { container } = render( { />, ); - expect(component.find(Input).exists()).toBeTruthy(); - expect(component.find(Dropdown).exists()).toBeTruthy(); - + // Text input (sub-question of type Text) and Dropdown (react-select) should be rendered + expect(container.querySelector('input[type="text"]')).not.toBeNull(); + expect(container.querySelector('.ddl-extra-questions-container')).not.toBeNull(); }); it('meat-type and values should show prefer', () => { - const component = mount( + const { container } = render( { />, ); - - expect(component.find('#prefer').exists()).toBeTruthy(); + expect(container.querySelector('#prefer')).not.toBeNull(); }) it('question should disabled', () => { @@ -1157,7 +1152,7 @@ it('question should disabled', () => { } ]; - const component = mount( + const { container } = render( { ); const slug = toSlug('cloud_service_provider_market_sub_segment'); - expect(component.find('#'+slug).exists()).toBeTruthy(); - const input = component.find('#'+slug+' input').at(1); - expect(input.props().disabled === true).toBeTruthy(); + const slugContainer = container.querySelector('#chl_wrapper_' + slug); + expect(slugContainer).not.toBeNull(); + const inputs = slugContainer.querySelectorAll('input[type="checkbox"]'); + expect(inputs[1].disabled).toBe(true); }) it('question should be enabled', () => { @@ -1222,7 +1218,7 @@ it('question should be enabled', () => { } ]; - const component = mount( + const { container } = render( { ); const slug = toSlug('cloud_service_provider_market_sub_segment'); - expect(component.find(`#${slug}`).exists()).toBeTruthy(); - const input = component.find(`#${slug} input`).at(1); - expect(input.props().disabled === true).toBeFalsy(); + const slugContainer = container.querySelector(`#chl_wrapper_${slug}`); + expect(slugContainer).not.toBeNull(); + const inputs = slugContainer.querySelectorAll('input[type="checkbox"]'); + expect(inputs[1].disabled).toBe(false); }) @@ -1517,7 +1514,7 @@ test('question with mandatory imcompleted subquestion should scroll', () => { const formRef = React.createRef(); - const component = mount( + const { container } = render( { ); const slug = toSlug('Organizational Role SUB-QUESTION (Other)'); - expect(component.find(`#${slug}`).exists()).toBeTruthy(); - const input = component.find(`#${slug} input`).at(0); - expect(input.props().disabled === true).toBeFalsy(); - - formRef.current.doSubmit(); + // Text-type sub-question: the itself has id=slug + const inputElement = container.querySelector(`input#${slug}`); + expect(inputElement).not.toBeNull(); + expect(inputElement.disabled).toBe(false); - const question = component.find(`#${slug}`); + act(() => { + formRef.current.doSubmit(); + }); + expect(container.querySelector(`input#${slug}`)).not.toBeNull(); }); diff --git a/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js b/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js index 9381c47c..ebf37d8d 100644 --- a/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js +++ b/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js @@ -12,13 +12,9 @@ **/ import React from 'react'; -import Enzyme, { shallow, mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import { act } from 'react-dom/test-utils'; +import { render, screen, act, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; import UploadInputV3 from '../index'; -import { Box, Typography, IconButton, Alert } from '@mui/material'; - -Enzyme.configure({ adapter: new Adapter() }); // Capture the latest dropzone props so tests can trigger callbacks let dropzoneCallbacks = {}; @@ -54,68 +50,48 @@ describe('UploadInputV3', () => { describe('Rendering', () => { test('renders without crashing', () => { - const wrapper = shallow(); - expect(wrapper.find(Box).length).toBeGreaterThan(0); + const { container } = render(); + expect(container.firstChild).not.toBeNull(); }); test('renders label when provided', () => { - const wrapper = shallow(); - const label = wrapper.find(Typography).first(); - expect(label.children().text()).toBe('Upload File'); + render(); + expect(screen.getByText('Upload File')).toBeInTheDocument(); }); test('renders helpText when provided', () => { const helpText = 'Please upload PDF, JPG or PNG files'; - const wrapper = shallow(); - const helpTypography = wrapper.findWhere(node => - node.type() === Typography && node.prop('color') === 'text.secondary' - ); - expect(helpTypography.children().text()).toBe(helpText); + render(); + expect(screen.getByText(helpText)).toBeInTheDocument(); }); test('renders error message when error prop is provided', () => { const errorMessage = 'File upload failed'; - const wrapper = shallow(); - const alert = wrapper.findWhere(node => - node.type() === Alert && node.prop('severity') === 'error' - ); - expect(alert.length).toBe(1); - expect(alert.children().text()).toBe(errorMessage); + render(); + expect(screen.getByText(errorMessage)).toBeInTheDocument(); }); test('does not render label when not provided', () => { - const wrapper = shallow(); - const labels = wrapper.findWhere(node => - node.type() === Typography && node.prop('fontWeight') === 600 - ); - expect(labels.length).toBe(0); + render(); + expect(screen.queryByText('Upload File')).not.toBeInTheDocument(); }); test('shows alert when postUrl is not provided', () => { - const wrapper = shallow(); - const alert = wrapper.findWhere(node => - node.type() === Alert && node.prop('severity') === 'error' - ); - expect(alert.length).toBe(1); - expect(alert.children().text()).toBe('No Post URL'); + render(); + expect(screen.getByText('No Post URL')).toBeInTheDocument(); }); test('shows alert when canAdd is false', () => { - const wrapper = shallow(); - const alert = wrapper.findWhere(node => - node.type() === Alert && node.prop('severity') === 'warning' - ); - expect(alert.length).toBe(1); - expect(alert.children().text()).toBe('Upload has been disabled by administrators.'); + render(); + expect(screen.getByText('Upload has been disabled by administrators.')).toBeInTheDocument(); }); }); describe('File Display', () => { test('displays uploaded file with filename', () => { const files = [{ filename: 'document.pdf', size: 102400 }]; - const wrapper = mount(); - expect(wrapper.text()).toContain('document.pdf'); - wrapper.unmount(); + render(); + expect(screen.getByText('document.pdf')).toBeInTheDocument(); }); test('displays multiple uploaded files', () => { @@ -123,155 +99,128 @@ describe('UploadInputV3', () => { { filename: 'document1.pdf', size: 102400 }, { filename: 'document2.pdf', size: 204800 }, ]; - const wrapper = mount(); - expect(wrapper.text()).toContain('document1.pdf'); - expect(wrapper.text()).toContain('document2.pdf'); - wrapper.unmount(); + render(); + expect(screen.getByText('document1.pdf')).toBeInTheDocument(); + expect(screen.getByText('document2.pdf')).toBeInTheDocument(); }); test('shows Complete status for uploaded files', () => { const files = [{ filename: 'document.pdf', size: 102400 }]; - const wrapper = mount(); - expect(wrapper.text()).toContain('Complete'); - wrapper.unmount(); + render(); + expect(screen.getByText(/Complete/)).toBeInTheDocument(); }); test('shows default size when size is not provided', () => { const files = [{ filename: 'no-size.pdf' }]; - const wrapper = mount(); - expect(wrapper.text()).toContain('0 KB'); - wrapper.unmount(); + render(); + expect(screen.getByText(/0 KB/)).toBeInTheDocument(); }); test('formats file size correctly', () => { const files = [{ filename: 'large-file.pdf', size: 2048000 }]; - const wrapper = mount(); - expect(wrapper.text()).toContain('2 MB'); - wrapper.unmount(); + render(); + expect(screen.getByText(/2 MB/)).toBeInTheDocument(); }); }); describe('Delete Functionality', () => { test('shows delete button when onRemove and canDelete are provided', () => { const files = [{ filename: 'document.pdf', size: 102400 }]; - const wrapper = shallow( - - ); - expect(wrapper.find(IconButton).length).toBe(1); + render(); + expect(screen.getAllByRole('button').length).toBe(1); }); test('calls onRemove when delete button is clicked', () => { const files = [{ filename: 'document.pdf', size: 102400 }]; const onRemoveMock = jest.fn(); - const wrapper = shallow( - - ); - wrapper.find(IconButton).first().simulate('click', { preventDefault: () => {} }); + render(); + fireEvent.click(screen.getByRole('button')); expect(onRemoveMock).toHaveBeenCalledWith(files[0]); }); test('does not show delete button when canDelete is false', () => { const files = [{ filename: 'document.pdf', size: 102400 }]; - const wrapper = shallow( - - ); - expect(wrapper.find(IconButton).length).toBe(0); + render(); + expect(screen.queryAllByRole('button')).toHaveLength(0); }); test('does not show delete button when onRemove is not provided', () => { const files = [{ filename: 'document.pdf', size: 102400 }]; - const wrapper = shallow(); - expect(wrapper.find(IconButton).length).toBe(0); + render(); + expect(screen.queryAllByRole('button')).toHaveLength(0); }); }); describe('Upload States', () => { test('shows dropzone when no file is uploading', () => { - const wrapper = mount(); - expect(wrapper.find('.dropzone-mock').length).toBe(1); - wrapper.unmount(); + const { container } = render(); + expect(container.querySelector('.dropzone-mock')).toBeInTheDocument(); }); test('hides dropzone while a file is uploading', () => { - const wrapper = mount(); + const { container } = render(); act(() => { dropzoneCallbacks.onAddedFile({ name: 'sample.png', size: 11264 }); }); - wrapper.update(); - const dropzoneBox = wrapper.findWhere(n => - n.type() === Box && n.prop('sx') && n.prop('sx').display === 'none' - ); - expect(dropzoneBox.length).toBeGreaterThan(0); - wrapper.unmount(); + expect(container.querySelector('.dropzone-mock')).not.toBeVisible(); }); test('shows Loading status and progress bar while uploading', () => { - const wrapper = mount(); + render(); act(() => { dropzoneCallbacks.onAddedFile({ name: 'sample.png', size: 11264 }); }); - wrapper.update(); - expect(wrapper.text()).toContain('sample.png'); - expect(wrapper.text()).toContain('Loading'); - wrapper.unmount(); + expect(screen.getByText('sample.png')).toBeInTheDocument(); + expect(screen.getByText(/Loading/)).toBeInTheDocument(); }); test('shows Complete status and hides progress bar after upload finishes', () => { - const wrapper = mount(); + render(); act(() => { dropzoneCallbacks.onAddedFile({ name: 'sample.png', size: 11264 }); }); - wrapper.update(); act(() => { dropzoneCallbacks.onFileCompleted({ name: 'sample.png', size: 11264 }); }); - wrapper.update(); - expect(wrapper.text()).toContain('Complete'); - expect(wrapper.text()).not.toContain('Loading'); - wrapper.unmount(); + expect(screen.getByText(/Complete/)).toBeInTheDocument(); + expect(screen.queryByText(/Loading/)).not.toBeInTheDocument(); }); test('hides dropzone when max files reached', () => { const files = [{ filename: 'document.pdf', size: 102400 }]; - const wrapper = mount(); - expect(wrapper.find('.dropzone-mock').length).toBe(0); - wrapper.unmount(); + const { container } = render(); + expect(container.querySelector('.dropzone-mock')).toBeNull(); }); test('shows dropzone when below max files', () => { const files = [{ filename: 'document.pdf', size: 102400 }]; - const wrapper = mount(); - expect(wrapper.find('.dropzone-mock').length).toBe(1); - wrapper.unmount(); + const { container } = render(); + expect(container.querySelector('.dropzone-mock')).toBeInTheDocument(); }); }); describe('Error Handling', () => { test('shows error row with filename and message when a file error occurs', () => { - const wrapper = mount(); + render(); act(() => { dropzoneCallbacks.onAddedFile({ name: 'big-file.png', size: 9999999 }); }); - wrapper.update(); act(() => { dropzoneCallbacks.onFileError( { name: 'big-file.png', size: 9999999 }, 'File is too big (9.54MiB). Max filesize: 5MiB.' ); }); - wrapper.update(); - expect(wrapper.text()).toContain('big-file.png'); - expect(wrapper.text()).toContain('File is too big (9.54MiB). Max filesize: 5MiB.'); - wrapper.unmount(); + expect(screen.getByText('big-file.png')).toBeInTheDocument(); + expect(screen.getByText('File is too big (9.54MiB). Max filesize: 5MiB.')).toBeInTheDocument(); }); test('removes the uploading row and shows error row when error occurs', () => { - const wrapper = mount(); + render(); act(() => { dropzoneCallbacks.onAddedFile({ name: 'big-file.png', size: 9999999 }); }); - wrapper.update(); - expect(wrapper.text()).toContain('Loading'); + expect(screen.getByText(/Loading/)).toBeInTheDocument(); act(() => { dropzoneCallbacks.onFileError( @@ -279,67 +228,51 @@ describe('UploadInputV3', () => { 'File is too big (9.54MiB). Max filesize: 5MiB.' ); }); - wrapper.update(); - expect(wrapper.text()).not.toContain('Loading'); - expect(wrapper.text()).toContain('File is too big'); - wrapper.unmount(); + expect(screen.queryByText(/Loading/)).not.toBeInTheDocument(); + expect(screen.getByText(/File is too big/)).toBeInTheDocument(); }); test('dismissing an error removes it from the view and restores the dropzone', () => { - const wrapper = mount(); + const { container } = render(); act(() => { dropzoneCallbacks.onFileError( { name: 'big-file.png', size: 9999999 }, 'File is too big (9.54MiB). Max filesize: 5MiB.' ); }); - wrapper.update(); - expect(wrapper.text()).toContain('File is too big'); - - const isDropzoneHidden = () => wrapper.findWhere(n => - n.type() === Box && n.prop('sx') && n.prop('sx').display === 'none' - ).length > 0; - expect(isDropzoneHidden()).toBe(true); + expect(screen.getByText(/File is too big/)).toBeInTheDocument(); + expect(container.querySelector('.dropzone-mock')).not.toBeVisible(); - const dismissButton = wrapper.findWhere(n => - n.type() === IconButton && n.prop('onClick') !== undefined - ).first(); act(() => { - dismissButton.prop('onClick')(); + fireEvent.click(screen.getByRole('button')); }); - wrapper.update(); - expect(wrapper.text()).not.toContain('File is too big'); - expect(isDropzoneHidden()).toBe(false); - wrapper.unmount(); + + expect(screen.queryByText(/File is too big/)).not.toBeInTheDocument(); + expect(container.querySelector('.dropzone-mock')).toBeVisible(); }); test('hides dropzone when an error is present', () => { - const wrapper = mount(); + const { container } = render(); act(() => { dropzoneCallbacks.onFileError( { name: 'big-file.png', size: 9999999 }, 'File is too big (9.54MiB). Max filesize: 5MiB.' ); }); - wrapper.update(); - const dropzoneHidden = wrapper.findWhere(n => - n.type() === Box && n.prop('sx') && n.prop('sx').display === 'none' - ); - expect(dropzoneHidden.length).toBeGreaterThan(0); - wrapper.unmount(); + expect(container.querySelector('.dropzone-mock')).not.toBeVisible(); }); }); describe('Configuration', () => { test('uses custom getAllowedExtensions function when provided', () => { const customGetExtensions = jest.fn(() => '.doc,.docx'); - shallow(); + render(); expect(customGetExtensions).toHaveBeenCalled(); }); test('uses custom getMaxSize function when provided', () => { const customGetMaxSize = jest.fn(() => 500); - shallow(); + render(); expect(customGetMaxSize).toHaveBeenCalled(); }); @@ -348,36 +281,32 @@ describe('UploadInputV3', () => { { filename: 'doc1.pdf', size: 102400 }, { filename: 'doc2.pdf', size: 102400 }, ]; - const wrapper = mount(); - expect(wrapper.find('.dropzone-mock').length).toBe(1); - expect(wrapper.text()).toContain('doc1.pdf'); - expect(wrapper.text()).toContain('doc2.pdf'); - wrapper.unmount(); + const { container } = render(); + expect(container.querySelector('.dropzone-mock')).toBeInTheDocument(); + expect(screen.getByText('doc1.pdf')).toBeInTheDocument(); + expect(screen.getByText('doc2.pdf')).toBeInTheDocument(); }); }); describe('Edge Cases', () => { test('handles empty value array', () => { - const wrapper = mount(); - expect(wrapper.find('.dropzone-mock').length).toBe(1); - wrapper.unmount(); + const { container } = render(); + expect(container.querySelector('.dropzone-mock')).toBeInTheDocument(); }); test('handles mediaType without type property', () => { - const wrapper = mount(); - expect(wrapper.find('.dropzone-mock').length).toBe(1); - wrapper.unmount(); + const { container } = render(); + expect(container.querySelector('.dropzone-mock')).toBeInTheDocument(); }); test('handles missing mediaType', () => { - const wrapper = mount(); - expect(wrapper.find('.dropzone-mock').length).toBe(1); - wrapper.unmount(); + const { container } = render(); + expect(container.querySelector('.dropzone-mock')).toBeInTheDocument(); }); test('handles undefined value prop', () => { - const wrapper = shallow(); - expect(wrapper.find(Box).length).toBeGreaterThan(0); + const { container } = render(); + expect(container.firstChild).not.toBeNull(); }); }); }); diff --git a/src/utils/actions.js b/src/utils/actions.js index 0f8b415d..58d1580b 100644 --- a/src/utils/actions.js +++ b/src/utils/actions.js @@ -21,6 +21,7 @@ import T from "i18n-react/dist/i18n-react"; import { isClearingSessionState, setSessionClearingState, getCurrentPathName } from './methods'; import { CLEAR_SESSION_STATE } from '../components/security/actions'; import { doLogin, initLogOut } from '../components/security/methods'; +import {CODE_200} from "./constants"; export const GENERIC_ERROR = "Yikes. Something seems to be broken. Our web team has been notified, and we apologize for the inconvenience."; export const RESET_LOADING = 'RESET_LOADING'; diff --git a/src/utils/constants.js b/src/utils/constants.js index 28bf8626..897ffd04 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -4,6 +4,8 @@ export const THREE_DECIMAL_PLACES = 3; export const ONE_CENT = 1n; export const ZERO_INT = 0; +export const CODE_200 = 200; + export const DEBOUNCE_WAIT_250 = 250; export const DEBOUNCE_WAIT = 500; diff --git a/src/utils/query-actions.js b/src/utils/query-actions.js index 388bfd7a..05158cab 100644 --- a/src/utils/query-actions.js +++ b/src/utils/query-actions.js @@ -159,7 +159,7 @@ export const fetchAllSummits = async (onlyActive) => { endpoint.addQuery('filter[]', `end_date<=${now}`); } - return _fetchPromise(endpoint, callback) + return _fetchPromise(endpoint) .then((json) => json.data); }; @@ -388,7 +388,6 @@ export const querySponsorsV2 = _.debounce(async (input, summitId, callback) => { endpoint.addQuery("filter", `company_name=@${escapedInput}`); } _fetch(endpoint) - .then(fetchResponseHandler) .then((json) => { const options = [...json.data].map((sp) => ({ id: sp.id, @@ -605,7 +604,7 @@ export const querySponsorAddons = async ( "sponsorship,sponsorship.type,sponsorship.type.type" ); endpoint.addQuery("relations", "sponsorship.none"); - return _fetch(endpoint) + return _fetchPromise(endpoint) .then(fetchResponseHandler) .then((json) => json.data) .catch((error) => { @@ -632,13 +631,7 @@ export const querySummitAddons = async ( endpoint.addQuery("page", 1); endpoint.addQuery("per_page", MAX_PER_PAGE); - return _fetch(endpoint) - .then(fetchResponseHandler) - .then((data) => callback(data)) - .catch((error) => { - fetchErrorHandler(error); - return []; - }); + _fetch(endpoint, callback); }; @@ -648,13 +641,7 @@ export const querySponsorships = _.debounce(async (input, callback) => { if (input) { endpoint.addQuery("filter", `name=@${input}`); } - _fetch(endpoint) - .then(fetchResponseHandler) - .then((json) => { - const options = [...json.data]; - callback(options); - }) - .catch(fetchErrorHandler); + _fetch(endpoint, callback); }, DEBOUNCE_WAIT); @@ -671,13 +658,7 @@ export const querySponsorshipsBySummit = _.debounce( if (input) { endpoint.addQuery("filter", `name=@${input}`); } - _fetch(endpoint) - .then(fetchResponseHandler) - .then((json) => { - const options = [...json.data]; - callback(options); - }) - .catch(fetchErrorHandler); + _fetch(endpoint, callback); }, DEBOUNCE_WAIT ); diff --git a/yarn.lock b/yarn.lock index f1efa948..61f9e5b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9270,7 +9270,7 @@ react-dnd@^16.0.0: fast-deep-equal "^3.1.3" hoist-non-react-statics "^3.3.2" -react-dom@^16.4.1: +react-dom@^16.13.1: version "16.14.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== @@ -9476,7 +9476,7 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^16.1.0, react@^16.6.3: +react@^16.1.0, react@^16.13.1: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== From 6cebe1625e0f7176aa9e38847745c43dcd4db2bd Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Mon, 30 Mar 2026 16:49:36 -0300 Subject: [PATCH 4/8] fix: query action --- src/utils/query-actions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/query-actions.js b/src/utils/query-actions.js index 05158cab..5640b61c 100644 --- a/src/utils/query-actions.js +++ b/src/utils/query-actions.js @@ -156,7 +156,7 @@ export const fetchAllSummits = async (onlyActive) => { if (onlyActive) { const now = moment().tz("UTC").unix(); - endpoint.addQuery('filter[]', `end_date<=${now}`); + endpoint.addQuery('filter[]', `end_date>=${now}`); } return _fetchPromise(endpoint) @@ -387,7 +387,7 @@ export const querySponsorsV2 = _.debounce(async (input, summitId, callback) => { if (escapedInput) { endpoint.addQuery("filter", `company_name=@${escapedInput}`); } - _fetch(endpoint) + _fetchPromise(endpoint) .then((json) => { const options = [...json.data].map((sp) => ({ id: sp.id, From 56b5398f0981f088d6e90dce09dd3565cff9a52d Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Tue, 31 Mar 2026 12:12:21 -0300 Subject: [PATCH 5/8] fix: PR review feedback --- src/components/inputs/promocode-input.js | 3 ++- .../mui/formik-inputs/mui-formik-pricefield.js | 12 +++++++++--- src/utils/query-actions.js | 2 +- src/utils/reducers.js | 14 +++++++++++--- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/inputs/promocode-input.js b/src/components/inputs/promocode-input.js index f6714331..bd2d598c 100644 --- a/src/components/inputs/promocode-input.js +++ b/src/components/inputs/promocode-input.js @@ -14,7 +14,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import AsyncSelect from 'react-select/lib/Async'; -import {DEFAULT_PER_PAGE, queryPromocodes} from '../../utils/query-actions'; +import {queryPromocodes} from '../../utils/query-actions'; +import {DEFAULT_PER_PAGE} from '../../utils/constants'; const PromocodeInput = ({summitId, error, value, onChange, id, multi, perPage, extraFilters, ...rest}) => { diff --git a/src/components/mui/formik-inputs/mui-formik-pricefield.js b/src/components/mui/formik-inputs/mui-formik-pricefield.js index 088b2d25..c9ed18d5 100644 --- a/src/components/mui/formik-inputs/mui-formik-pricefield.js +++ b/src/components/mui/formik-inputs/mui-formik-pricefield.js @@ -16,7 +16,7 @@ import PropTypes from "prop-types"; import { InputAdornment } from "@mui/material"; import { useField } from "formik"; import MuiFormikTextField from "./mui-formik-textfield"; -import { ONE_HUNDRED } from "../../../utils/constants"; +import { ONE_HUNDRED, DECIMAL_DIGITS } from "../../../utils/constants"; const BLOCKED_KEYS = ["e", "E", "+", "-"]; @@ -38,7 +38,11 @@ const MuiFormikPriceField = ({ if (field.value == null || field.value === 0) { return field.value === 0 ? 0 : ""; } - return inCents ? field.value / ONE_HUNDRED : field.value; + const raw = inCents ? field.value / ONE_HUNDRED : field.value; + const str = String(Number(raw.toFixed(DECIMAL_DIGITS))); + const dotIdx = str.indexOf("."); + if (dotIdx !== -1 && str.length - dotIdx - 1 === 1) return `${str}0`; + return str; }; const handleChange = (e) => { @@ -52,7 +56,9 @@ const MuiFormikPriceField = ({ setCleared(false); const numericValue = Number(newVal); - const newPrice = inCents ? numericValue * ONE_HUNDRED : numericValue; + const newPrice = inCents + ? Math.round(numericValue * ONE_HUNDRED) + : numericValue; helpers.setValue(newPrice); }; diff --git a/src/utils/query-actions.js b/src/utils/query-actions.js index 5640b61c..5c5a0ced 100644 --- a/src/utils/query-actions.js +++ b/src/utils/query-actions.js @@ -44,7 +44,7 @@ const _fetchPromise = async (endpoint, options = {}) => { try { accessToken = await getAccessToken(); } catch (e) { - return Promise.reject(); + return Promise.reject(e); } endpoint.addQuery('access_token', accessToken); diff --git a/src/utils/reducers.js b/src/utils/reducers.js index 178838a5..0cb7969f 100644 --- a/src/utils/reducers.js +++ b/src/utils/reducers.js @@ -24,15 +24,23 @@ const DEFAULT_STATE = { msg_type: null, params: {}, loading: false, + snackbarMessage: { + title: "", + html: "", + type: "", + httpCode: "" + } } export const genericReducers = function ( state = DEFAULT_STATE, action = {}) { - switch(action.type) { + const { type, payload } = action; + + switch(type) { case SHOW_MESSAGE: return { ...state, - msg: action.payload.msg, - msg_type: action.payload.msg_type, + msg: payload.msg, + msg_type: payload.msg_type, }; case CLEAR_MESSAGE: From 47ce2812807c3e31dc34d67a9de767f15141bad0 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Tue, 31 Mar 2026 18:05:31 -0300 Subject: [PATCH 6/8] fix: update pricefield --- .../formik-inputs/mui-formik-pricefield.js | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/src/components/mui/formik-inputs/mui-formik-pricefield.js b/src/components/mui/formik-inputs/mui-formik-pricefield.js index c9ed18d5..e53411c8 100644 --- a/src/components/mui/formik-inputs/mui-formik-pricefield.js +++ b/src/components/mui/formik-inputs/mui-formik-pricefield.js @@ -11,42 +11,62 @@ * limitations under the License. * */ -import React, { useState } from "react"; +import React, {useState} from "react"; import PropTypes from "prop-types"; -import { InputAdornment } from "@mui/material"; -import { useField } from "formik"; +import {InputAdornment} from "@mui/material"; +import {useField} from "formik"; import MuiFormikTextField from "./mui-formik-textfield"; -import { ONE_HUNDRED, DECIMAL_DIGITS } from "../../../utils/constants"; +import {DECIMAL_DIGITS, ONE_HUNDRED} from "../../../utils/constants"; const BLOCKED_KEYS = ["e", "E", "+", "-"]; const MuiFormikPriceField = ({ - name, - label, - inCents = false, - inputProps = { step: 0.01 }, - ...props -}) => { + name, + label, + inCents = false, + inputProps = {step: 0.01}, + ...props + }) => { // eslint-disable-next-line no-unused-vars const [field, meta, helpers] = useField(name); const [cleared, setCleared] = useState(false); + const [isFocused, setIsFocused] = useState(false); + const [focusedValue, setFocusedValue] = useState(""); const emptyValue = meta.initialValue === null ? null : 0; + const getRawString = () => { + if (cleared || field.value == null) return ""; + if (field.value === 0) return "0"; + const raw = inCents ? field.value / ONE_HUNDRED : field.value; + return String(Number(raw.toFixed(DECIMAL_DIGITS))); + }; + const getDisplayValue = () => { + if (isFocused) return focusedValue; if (cleared) return ""; if (field.value == null || field.value === 0) { return field.value === 0 ? 0 : ""; } - const raw = inCents ? field.value / ONE_HUNDRED : field.value; - const str = String(Number(raw.toFixed(DECIMAL_DIGITS))); - const dotIdx = str.indexOf("."); - if (dotIdx !== -1 && str.length - dotIdx - 1 === 1) return `${str}0`; - return str; + const str = getRawString(); + const dotIdx = str.indexOf("."); + if (dotIdx !== -1 && str.length - dotIdx - 1 === 1) return `${str}0`; + return str; + }; + + const handleFocus = () => { + setIsFocused(true); + setFocusedValue(getRawString()); + }; + + const handleBlur = (e) => { + setIsFocused(false); + if (props.onBlur) props.onBlur(e); }; const handleChange = (e) => { const newVal = e.target.value; + setFocusedValue(newVal); if (newVal === "") { setCleared(true); @@ -56,9 +76,9 @@ const MuiFormikPriceField = ({ setCleared(false); const numericValue = Number(newVal); - const newPrice = inCents - ? Math.round(numericValue * ONE_HUNDRED) - : numericValue; + const newPrice = inCents + ? Math.round(numericValue * ONE_HUNDRED) + : numericValue; helpers.setValue(newPrice); }; @@ -77,6 +97,8 @@ const MuiFormikPriceField = ({ type="number" value={getDisplayValue()} onChange={handleChange} + onFocus={handleFocus} + onBlur={handleBlur} slotProps={{ input: { startAdornment: $ From 01d4ac83a1e118ba05a43eda3d3097578b7cb234 Mon Sep 17 00:00:00 2001 From: sebastian marcet Date: Wed, 1 Apr 2026 16:32:09 -0300 Subject: [PATCH 7/8] chore(deps): migrate to react 17.x (#205) * chore(deps): migrate to react 17.x * chore: fix warnings Signed-off-by: smarcet * chore(unit-tests): fix --------- Signed-off-by: smarcet --- .gitignore | 1 + package.json | 13 +- .../mui/__tests__/mui-formik-upload.test.js | 19 +- .../mui/formik-inputs/mui-formik-upload.js | 2 +- .../schedule-event-list.js | 3 +- src/utils/constants.js | 2 + yarn.lock | 458 ++---------------- 7 files changed, 55 insertions(+), 443 deletions(-) diff --git a/.gitignore b/.gitignore index 78730469..c049f0e4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules *.log .idea/ package.json.lock +.codegraph diff --git a/package.json b/package.json index 3dc048bd..6511ed31 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,6 @@ "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^4.2.2", "dropzone": "5.7.2", - "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.6", "extend": "^3.0.1", "file-loader": "^6.2.0", "final-form": "^4.20.7", @@ -70,13 +68,13 @@ "node-sass": "^7.0.1", "path": "^0.12.7", "postcss-loader": "^6.2.1", - "react": "^16.13.1", + "react": "^17.0.0", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^0.31.5", "react-datetime": "^2.16.2", "react-dnd": "^16.0.0", "react-dnd-html5-backend": "^16.0.0", - "react-dom": "^16.13.1", + "react-dom": "^17.0.0", "react-dropzone": "^4.2.9", "react-final-form": "^6.5.9", "react-google-maps": "^9.4.5", @@ -133,13 +131,13 @@ "lodash": "^4.17.14", "moment": "^2.22.2", "moment-timezone": "^0.5.21", - "react": "^16.13.1", + "react": "^17.0.0", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^0.31.5", "react-datetime": "^2.16.2", "react-dnd": "^16.0.0", "react-dnd-html5-backend": "^16.0.0", - "react-dom": "^16.13.1", + "react-dom": "^17.0.0", "react-dropzone": "^4.2.9", "react-final-form": "^6.5.9", "react-google-maps": "^9.4.5", @@ -192,5 +190,6 @@ "console": {} }, "testEnvironment": "jsdom" - } + }, + "dependencies": {} } diff --git a/src/components/mui/__tests__/mui-formik-upload.test.js b/src/components/mui/__tests__/mui-formik-upload.test.js index 54cd4fc6..72e0e19c 100644 --- a/src/components/mui/__tests__/mui-formik-upload.test.js +++ b/src/components/mui/__tests__/mui-formik-upload.test.js @@ -11,16 +11,15 @@ * limitations under the License. * */ -jest.mock("../../inputs/upload-input-v2", () => ({ - UploadInputV2: ({ value, canAdd }) => { - const React = require("react"); - return ( -
    - {value.length} file(s) -
    - ); - } -})); +jest.mock("../../inputs/upload-input-v2", () => { + const React = require("react"); + const MockUploadInputV2 = ({ value, canAdd }) => ( +
    + {value.length} file(s) +
    + ); + return { __esModule: true, default: MockUploadInputV2 }; +}); import React from "react"; import { render, screen } from "@testing-library/react"; diff --git a/src/components/mui/formik-inputs/mui-formik-upload.js b/src/components/mui/formik-inputs/mui-formik-upload.js index bec4e427..ac6e6c83 100644 --- a/src/components/mui/formik-inputs/mui-formik-upload.js +++ b/src/components/mui/formik-inputs/mui-formik-upload.js @@ -14,7 +14,7 @@ import React from "react"; import PropTypes from "prop-types"; import { FormHelperText } from "@mui/material"; -import { UploadInputV2 } from "../../inputs/upload-input-v2"; +import UploadInputV2 from "../../inputs/upload-input-v2"; import { useField } from "formik"; import { ALLOWED_INVENTORY_IMAGE_FORMATS, diff --git a/src/components/schedule-builder-view/schedule-event-list.js b/src/components/schedule-builder-view/schedule-event-list.js index 3bcd1f4e..123c4a87 100644 --- a/src/components/schedule-builder-view/schedule-event-list.js +++ b/src/components/schedule-builder-view/schedule-event-list.js @@ -16,7 +16,6 @@ import moment from 'moment-timezone' import {useDrop} from 'react-dnd' import {DraggableItemTypes} from './constants'; import ScheduleEvent from './schedule-event'; -import ReactDOM from 'react-dom'; import SummitEvent from '../../models/summit-event'; const TimeSlot = ({timeLabel, id}) => { @@ -165,7 +164,7 @@ const ScheduleEventList = (props) => { } const getBoundingBox = () => { - return ReactDOM.findDOMNode(scheduleEventContainer.current).getBoundingClientRect(); + return scheduleEventContainer.current.getBoundingClientRect(); } const calculateInitialTop = (event) => { diff --git a/src/utils/constants.js b/src/utils/constants.js index 897ffd04..e685135f 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -54,3 +54,5 @@ export const RATE_FIELDS = { STANDARD: "standard_rate", ONSITE: "onsite_rate" }; + +export const DECIMAL_DIGITS = 2; diff --git a/yarn.lock b/yarn.lock index 61f9e5b4..86082e69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2685,21 +2685,6 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -airbnb-prop-types@^2.16.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" - integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== - dependencies: - array.prototype.find "^2.1.1" - function.prototype.name "^1.1.2" - is-regex "^1.1.0" - object-is "^1.1.2" - object.assign "^4.1.0" - object.entries "^1.1.2" - prop-types "^15.7.2" - prop-types-exact "^1.2.0" - react-is "^16.13.1" - ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2901,37 +2886,6 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== -array.prototype.filter@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array.prototype.filter/-/array.prototype.filter-1.0.1.tgz#20688792acdb97a09488eaaee9eebbf3966aae21" - integrity sha512-Dk3Ty7N42Odk7PjU/Ci3zT4pLj20YvuVnneG/58ICM6bt4Ij5kZaJTVQ9TSaWaIECX2sFyz4KItkVZqHNnciqw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - es-array-method-boxes-properly "^1.0.0" - is-string "^1.0.7" - -array.prototype.find@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.2.0.tgz#153b8a28ad8965cd86d3117b07e6596af6f2880d" - integrity sha512-sn40qmUiLYAcRb/1HsIQjTTZ1kCy8II8VtZJpMn2Aoen9twULhbWXisfh3HimGqMlHGUul0/TfKCnXg42LuPpQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.4" - es-shim-unscopables "^1.0.0" - -array.prototype.flat@^1.2.3: - version "1.3.0" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b" - integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.2" - es-shim-unscopables "^1.0.0" - arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -3648,15 +3602,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0: - version "1.0.30001460" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz" - integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ== - -caniuse-lite@^1.0.30001646: - version "1.0.30001653" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz#b8af452f8f33b1c77f122780a4aecebea0caca56" - integrity sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646: + version "1.0.30001784" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz" + integrity sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw== caseless@~0.12.0: version "0.12.0" @@ -3720,7 +3669,7 @@ cheerio-select@^1.5.0: domhandler "^4.3.1" domutils "^2.8.0" -cheerio@1.0.0-rc.10, cheerio@^1.0.0-rc.3: +cheerio@1.0.0-rc.10: version "1.0.0-rc.10" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== @@ -3931,7 +3880,7 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.19.0, commander@^2.20.0: +commander@^2.11.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -4454,7 +4403,7 @@ define-lazy-prop@^3.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== -define-properties@^1.1.3, define-properties@^1.1.4: +define-properties@^1.1.3: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== @@ -4553,11 +4502,6 @@ diff-sequences@^28.0.2: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.0.2.tgz#40f8d4ffa081acbd8902ba35c798458d0ff1af41" integrity sha512-YtEoNynLDFCRznv/XDalsKGSZDoj0U5kLnXvY0JSq3nBboRrZXjD81+eSiwi+nzcZDwedMmcowcxNwwgFW23mQ== -discontinuous-range@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" - integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= - dnd-core@^16.0.1: version "16.0.1" resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19" @@ -4836,70 +4780,6 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== -enzyme-adapter-react-16@^1.15.6: - version "1.15.6" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz#fd677a658d62661ac5afd7f7f541f141f8085901" - integrity sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g== - dependencies: - enzyme-adapter-utils "^1.14.0" - enzyme-shallow-equal "^1.0.4" - has "^1.0.3" - object.assign "^4.1.2" - object.values "^1.1.2" - prop-types "^15.7.2" - react-is "^16.13.1" - react-test-renderer "^16.0.0-0" - semver "^5.7.0" - -enzyme-adapter-utils@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz#afbb0485e8033aa50c744efb5f5711e64fbf1ad0" - integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg== - dependencies: - airbnb-prop-types "^2.16.0" - function.prototype.name "^1.1.3" - has "^1.0.3" - object.assign "^4.1.2" - object.fromentries "^2.0.3" - prop-types "^15.7.2" - semver "^5.7.1" - -enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e" - integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== - dependencies: - has "^1.0.3" - object-is "^1.1.2" - -enzyme@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28" - integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw== - dependencies: - array.prototype.flat "^1.2.3" - cheerio "^1.0.0-rc.3" - enzyme-shallow-equal "^1.0.1" - function.prototype.name "^1.1.2" - has "^1.0.3" - html-element-map "^1.2.0" - is-boolean-object "^1.0.1" - is-callable "^1.1.5" - is-number-object "^1.0.4" - is-regex "^1.0.5" - is-string "^1.0.5" - is-subset "^0.1.1" - lodash.escape "^4.0.1" - lodash.isequal "^4.5.0" - object-inspect "^1.7.0" - object-is "^1.0.2" - object.assign "^4.1.0" - object.entries "^1.1.1" - object.values "^1.1.1" - raf "^3.4.1" - rst-selector-parser "^2.2.3" - string.prototype.trim "^1.2.1" - err-code@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" @@ -4919,40 +4799,6 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.4, es-abstract@^1.19.5: - version "1.20.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" - integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.1.1" - get-symbol-description "^1.0.0" - has "^1.0.3" - has-property-descriptors "^1.0.0" - has-symbols "^1.0.3" - internal-slot "^1.0.3" - is-callable "^1.2.4" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-weakref "^1.0.2" - object-inspect "^1.12.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - regexp.prototype.flags "^1.4.3" - string.prototype.trimend "^1.0.5" - string.prototype.trimstart "^1.0.5" - unbox-primitive "^1.0.2" - -es-array-method-boxes-properly@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" - integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== - es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" @@ -4990,22 +4836,6 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: dependencies: es-errors "^1.3.0" -es-shim-unscopables@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== - dependencies: - has "^1.0.3" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - es6-promise@^4.2.8: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -5573,17 +5403,7 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.2, function.prototype.name@^1.1.3, function.prototype.name@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" - integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - functions-have-names "^1.2.2" - -functions-have-names@^1.2.2, functions-have-names@^1.2.3: +functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -5634,7 +5454,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -5682,14 +5502,6 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -5830,7 +5642,7 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" -has-bigints@^1.0.1, has-bigints@^1.0.2: +has-bigints@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== @@ -6004,14 +5816,6 @@ hsl-to-rgb-for-reals@^1.1.0: resolved "https://registry.yarnpkg.com/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz#e1eb23f6b78016e3722431df68197e6dcdc016d9" integrity sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg== -html-element-map@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.3.1.tgz#44b2cbcfa7be7aa4ff59779e47e51012e1c73c08" - integrity sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg== - dependencies: - array.prototype.filter "^1.0.0" - call-bind "^1.0.2" - html-encoding-sniffer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" @@ -6283,15 +6087,6 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== - dependencies: - get-intrinsic "^1.1.0" - has "^1.0.3" - side-channel "^1.0.4" - internal-slot@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" @@ -6390,7 +6185,7 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: +is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== @@ -6403,11 +6198,6 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" - integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== - is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -6441,13 +6231,6 @@ is-data-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - is-date-object@^1.0.5: version "1.1.0" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" @@ -6564,11 +6347,6 @@ is-map@^2.0.2, is-map@^2.0.3: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== -is-negative-zero@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== - is-network-error@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.1.0.tgz#d26a760e3770226d11c169052f266a4803d9c997" @@ -6637,7 +6415,7 @@ is-primitive@^2.0.0: resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU= -is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.4: +is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -6674,12 +6452,7 @@ is-string@^1.0.5, is-string@^1.0.7: dependencies: has-tostringtag "^1.0.0" -is-subset@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" - integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= - -is-symbol@^1.0.2, is-symbol@^1.0.3: +is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== @@ -6701,13 +6474,6 @@ is-weakmap@^2.0.2: resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - is-weakset@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" @@ -7595,21 +7361,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= -lodash.escape@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" - integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= - -lodash.flattendeep@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" - integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= - -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= - lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -8030,11 +7781,6 @@ moment-timezone@^0.5.21: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3" integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw== -moo@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" - integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w== - mpd-parser@0.21.0: version "0.21.0" resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.21.0.tgz#c2036cce19522383b93c973180fdd82cd646168e" @@ -8108,16 +7854,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -nearley@^2.7.10: - version "2.20.1" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" - integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== - dependencies: - commander "^2.19.0" - moo "^0.5.0" - railroad-diagrams "^1.0.0" - randexp "0.4.6" - needle@^2.5.2: version "2.9.1" resolved "https://registry.yarnpkg.com/needle/-/needle-2.9.1.tgz#22d1dffbe3490c2b83e301f7709b6736cd8f2684" @@ -8339,11 +8075,6 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.12.0, object-inspect@^1.7.0: - version "1.12.1" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.1.tgz#28a661153bad7e470e4b01479ef1cb91ce511191" - integrity sha512-Y/jF6vnvEtOPGiKD1+q+X0CiUYRQtEHp89MLLUJ7TUivtH8Ugn2+3A7Rynqk7BRsAoqeOQWnFnjpDrKSxDgIGA== - object-inspect@^1.13.3: version "1.13.4" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" @@ -8354,14 +8085,6 @@ object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== -object-is@^1.0.2, object-is@^1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - object-is@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" @@ -8382,7 +8105,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.0, object.assign@^4.1.2: +object.assign@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -8404,24 +8127,6 @@ object.assign@^4.1.4: has-symbols "^1.1.0" object-keys "^1.1.1" -object.entries@^1.1.1, object.entries@^1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" - integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -object.fromentries@^2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" - integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -8437,15 +8142,6 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.1, object.values@^1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" - integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" @@ -9077,15 +8773,6 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types-exact@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" - integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== - dependencies: - has "^1.0.3" - object.assign "^4.1.0" - reflect.ownkeys "^0.2.0" - prop-types-extra@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" @@ -9162,26 +8849,13 @@ raf-schd@^4.0.2: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== -raf@^3.4.0, raf@^3.4.1: +raf@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== dependencies: performance-now "^2.1.0" -railroad-diagrams@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" - integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= - -randexp@0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" - integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== - dependencies: - discontinuous-range "1.0.0" - ret "~0.1.10" - randomatic@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" @@ -9270,15 +8944,14 @@ react-dnd@^16.0.0: fast-deep-equal "^3.1.3" hoist-non-react-statics "^3.3.2" -react-dom@^16.13.1: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" - integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== +react-dom@^17.0.0: + version "17.0.2" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" + scheduler "^0.20.2" react-dropzone@^4.2.9: version "4.3.0" @@ -9324,7 +8997,7 @@ react-input-autosize@^2.2.1: dependencies: prop-types "^15.5.8" -react-is@^16.13.1, react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.6: +react-is@^16.13.1, react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -9438,16 +9111,6 @@ react-star-ratings@^2.3.0: prop-types "^15.6.0" react "^16.1.0" -react-test-renderer@^16.0.0-0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae" - integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg== - dependencies: - object-assign "^4.1.1" - prop-types "^15.6.2" - react-is "^16.8.6" - scheduler "^0.19.1" - react-tooltip@^5.28.0: version "5.28.0" resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.28.0.tgz#c7b5343ab2d740a428494a3d8315515af1f26f46" @@ -9476,7 +9139,7 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^16.1.0, react@^16.13.1: +react@^16.1.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== @@ -9485,6 +9148,14 @@ react@^16.1.0, react@^16.13.1: object-assign "^4.1.1" prop-types "^15.6.2" +react@^17.0.0: + version "17.0.2" + resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -9608,11 +9279,6 @@ redux@^4.2.0: dependencies: "@babel/runtime" "^7.9.2" -reflect.ownkeys@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" - integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= - regenerate-unicode-properties@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" @@ -9667,15 +9333,6 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" - integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - functions-have-names "^1.2.2" - regexp.prototype.flags@^1.5.1: version "1.5.4" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" @@ -9852,14 +9509,6 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rst-selector-parser@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" - integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE= - dependencies: - lodash.flattendeep "^4.4.0" - nearley "^2.7.10" - run-applescript@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" @@ -9939,10 +9588,10 @@ scheduler@^0.17.0: loose-envify "^1.1.0" object-assign "^4.1.1" -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -10001,7 +9650,7 @@ selfsigned@^2.4.1: "@types/node-forge" "^1.3.0" node-forge "^1" -"semver@2 || 3 || 4 || 5", semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -10515,33 +10164,6 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.trim@^1.2.1: - version "1.2.6" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.6.tgz#824960787db37a9e24711802ed0c1d1c0254f83e" - integrity sha512-8lMR2m+U0VJTPp6JjvJTtGyc4FIGq9CdRt7O9p6T0e6K4vjU+OP+SQJpbe/SBmRcCUIvNUnjsbmY6lnMp8MhsQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.19.5" - -string.prototype.trimend@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" - integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.19.5" - -string.prototype.trimstart@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" - integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.19.5" - string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -10990,16 +10612,6 @@ un-eval@^1.2.0: resolved "https://registry.yarnpkg.com/un-eval/-/un-eval-1.2.0.tgz#22a95c650334d59d21697efae32612218ecad65f" integrity sha512-Wlj/pum6dQtGTPD/lclDtoVPkSfpjPfy1dwnnKw/sZP5DpBH9fLhBgQfsqNhe5/gS1D+vkZUuB771NRMUPA5CA== -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== - dependencies: - call-bind "^1.0.2" - has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" - uncontrollable@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-4.1.0.tgz#e0358291252e1865222d90939b19f2f49f81c1a9" From 12642db8a14b745e65fb67fe14d69f35a9be134b Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Wed, 1 Apr 2026 16:34:01 -0300 Subject: [PATCH 8/8] fix: price field null value --- src/components/mui/formik-inputs/mui-formik-pricefield.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/mui/formik-inputs/mui-formik-pricefield.js b/src/components/mui/formik-inputs/mui-formik-pricefield.js index e53411c8..658ea30c 100644 --- a/src/components/mui/formik-inputs/mui-formik-pricefield.js +++ b/src/components/mui/formik-inputs/mui-formik-pricefield.js @@ -33,7 +33,8 @@ const MuiFormikPriceField = ({ const [isFocused, setIsFocused] = useState(false); const [focusedValue, setFocusedValue] = useState(""); - const emptyValue = meta.initialValue === null ? null : 0; + // emptyValue is always 0 when editing this field, null is handled by N/A checkbox + const emptyValue = 0; const getRawString = () => { if (cleared || field.value == null) return "";