From 6e87625f3aa6a5ece9015f7f55fedb5253818629 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Tue, 30 Jun 2026 17:00:29 -0300 Subject: [PATCH 01/14] chore: update FormItemTable with new design and tweak some inputs --- src/components/index.js | 2 +- .../components/ExpandedRowContent.js | 77 +++++ .../components/ItemTableField.js | 29 +- src/components/mui/FormItemTable/index.js | 305 ++++++++++-------- .../mui/formik-inputs/mui-formik-checkbox.js | 4 +- .../formik-inputs/mui-formik-datepicker.js | 23 +- .../mui/formik-inputs/mui-formik-select-v2.js | 6 +- .../formik-inputs/mui-formik-timepicker.js | 15 +- src/i18n/en.json | 1 + 9 files changed, 305 insertions(+), 157 deletions(-) create mode 100644 src/components/mui/FormItemTable/components/ExpandedRowContent.js diff --git a/src/components/index.js b/src/components/index.js index ea54b8e6..f074b1c0 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -70,7 +70,7 @@ 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, getCurrentApplicableRate, isItemAvailable, GlobalQuantityField, ItemTableField, UnderlyingAlertNote} from './mui/FormItemTable' +export {default as MuiFormItemTable, getCurrentApplicableRate, isItemAvailable, GlobalQuantityField, ItemTableField, UnderlyingAlertNote, ExpandedRowContent} from './mui/FormItemTable' export {default as MuiItemSettingsModal} from './mui/ItemSettingsModal' export {default as MuiNotesModal} from './mui/NotesModal' export {default as MuiSnackbarNotification} from './mui/SnackbarNotification' diff --git a/src/components/mui/FormItemTable/components/ExpandedRowContent.js b/src/components/mui/FormItemTable/components/ExpandedRowContent.js new file mode 100644 index 00000000..67225b9e --- /dev/null +++ b/src/components/mui/FormItemTable/components/ExpandedRowContent.js @@ -0,0 +1,77 @@ +/** + * 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, Grid2, TextField } from "@mui/material"; +import { useField } from "formik"; +import T from "i18n-react/dist/i18n-react"; +import { SPONSOR_FORMS_METAFIELD_CLASS } from "../../../../utils/constants"; +import ItemTableField from "./ItemTableField"; + +const InlineNotesField = ({ rowId, disabled }) => { + const name = `i-${rowId}-c-global-f-notes`; + const [field] = useField(name); + return ( + + ); +}; + +const ExpandedRowContent = ({ row, extraColumns, timeZone, disabled }) => { + const itemFields = (row.meta_fields ?? []).filter( + (f) => f.class_field === SPONSOR_FORMS_METAFIELD_CLASS.ITEM + ); + + return ( + + + {extraColumns.map((exc) => ( + + + + ))} + {itemFields.map((f) => ( + + + + ))} + + + + + + ); +}; + +export default ExpandedRowContent; diff --git a/src/components/mui/FormItemTable/components/ItemTableField.js b/src/components/mui/FormItemTable/components/ItemTableField.js index 69cd5f40..6ee1d674 100644 --- a/src/components/mui/FormItemTable/components/ItemTableField.js +++ b/src/components/mui/FormItemTable/components/ItemTableField.js @@ -28,7 +28,8 @@ const ItemTableField = ({ disabled = false }) => { const name = `i-${rowId}-c-${field.class_field}-f-${field.type_id}`; - const commonProps = { name, label, disabled }; + const required = field.is_required ?? false; + const commonProps = { name, label, disabled, required, slotProps: { inputLabel: { shrink: true } }, margin: "none" }; switch (field.type) { case "CheckBox": @@ -46,13 +47,30 @@ const ItemTableField = ({ ({ value: v.id, label: v.value }))} + options={field.values.map((v) => ({ value: String(v.id), label: v.value }))} /> ); case "DateTime": - return ; + return ( + + ); case "Time": - return ; + return ( + + ); case "Quantity": return ( 0 @@ -75,7 +94,7 @@ const ItemTableField = ({ ({ value: v.id, label: v.value }))} + options={field.values.map((v) => ({ value: String(v.id), label: v.value }))} /> ); case "Text": diff --git a/src/components/mui/FormItemTable/index.js b/src/components/mui/FormItemTable/index.js index a1d46e7f..70608340 100644 --- a/src/components/mui/FormItemTable/index.js +++ b/src/components/mui/FormItemTable/index.js @@ -11,8 +11,10 @@ * limitations under the License. * */ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { + Box, + Collapse, IconButton, MenuItem, Paper, @@ -23,31 +25,93 @@ import { TableHead, TableRow } from "@mui/material"; -import EditIcon from "@mui/icons-material/Edit"; -import SettingsIcon from "@mui/icons-material/Settings"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import T from "i18n-react/dist/i18n-react"; import { currencyAmountFromCents } from "../../../utils/money"; -import { DISCOUNT_TYPES, ONE_HUNDRED } from "../../../utils/constants"; +import { + DISCOUNT_TYPES, + ONE_HUNDRED, + SPONSOR_FORMS_METAFIELD_CLASS +} 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"; +import ExpandedRowContent from "./components/ExpandedRowContent"; +import { isItemAvailable } from "./helpers"; const FormItemTable = ({ data, currentApplicableRate, timeZone, values, - onNotesClick, - onSettingsClick + touched, + errors }) => { const valuesStr = JSON.stringify(values); + const [openRows, setOpenRows] = useState({}); + const extraColumns = - data[0]?.meta_fields?.filter((mf) => mf.class_field === "Form") || []; - const fixedColumns = 10; - const totalColumns = extraColumns.length + fixedColumns; + data[0]?.meta_fields?.filter( + (mf) => mf.class_field === SPONSOR_FORMS_METAFIELD_CLASS.FORM + ) || []; + + // toggle, code, name, custom_rate, early_bird, standard, onsite, qty, total, details + const totalColumns = 10; + + useEffect(() => { + if (!errors || Object.keys(errors).length === 0) return; + setOpenRows((prev) => { + const updates = {}; + data.forEach((row) => { + const itemFields = (row.meta_fields ?? []).filter( + (f) => f.class_field === SPONSOR_FORMS_METAFIELD_CLASS.ITEM + ); + const expandedKeys = new Set([ + ...extraColumns.map( + (exc) => + `i-${row.form_item_id}-c-${exc.class_field}-f-${exc.type_id}` + ), + ...itemFields.map( + (f) => `i-${row.form_item_id}-c-${f.class_field}-f-${f.type_id}` + ), + `i-${row.form_item_id}-c-global-f-notes` + ]); + const hasVisibleError = Object.keys(errors).some( + (key) => expandedKeys.has(key) && touched[key] + ); + if (hasVisibleError) updates[row.form_item_id] = true; + }); + return { ...prev, ...updates }; + }); + }, [errors, touched]); + + const toggleRow = (rowId) => { + setOpenRows((prev) => ({ ...prev, [rowId]: !prev[rowId] })); + }; + + const getDetailsIconColor = (row) => { + const hasIncomplete = (row.meta_fields ?? []) + .filter((mf) => mf.is_required) + .some((mf) => { + const val = + values[ + `i-${row.form_item_id}-c-${mf.class_field}-f-${mf.type_id}` + ]; + if (mf.type === "CheckBoxList") return !Array.isArray(val) || val.length === 0; + if (mf.type === "CheckBox") return val !== true; + return val === undefined || val === null || val === ""; + }); + if (hasIncomplete) return "error"; + + const prefix = `i-${row.form_item_id}-`; + const isTouched = Object.keys(touched ?? {}).some( + (key) => key.startsWith(prefix) && touched[key] + ); + return isTouched ? "success" : "warning"; + }; const calculateQuantity = useCallback( (row) => { @@ -83,23 +147,6 @@ const FormItemTable = ({ 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}`]; - if (mf.type === "CheckBoxList") return !Array.isArray(value) || value.length === 0; - if (mf.type === "CheckBox") return value !== true; - 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); @@ -110,24 +157,17 @@ const FormItemTable = ({ const discount = values.discount_type === DISCOUNT_TYPES.AMOUNT ? values.discount_amount - : subtotal * (values.discount_amount / ONE_HUNDRED / ONE_HUNDRED); // bps to fraction + : subtotal * (values.discount_amount / ONE_HUNDRED / ONE_HUNDRED); 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")} @@ -146,121 +186,122 @@ const FormItemTable = ({ {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")} + + {T.translate("sponsor_edit_form.details")} - {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)} + {data.map((row) => { + const disabled = !isItemAvailable(row, currentApplicableRate); + const isOpen = !!openRows[row.form_item_id]; + + return ( + + + + toggleRow(row.form_item_id)} + > + {isOpen ? ( + + ) : ( + + )} + + + {row.code} + {row.name} + + + + + {formatRate(row.rates.early_bird)} + + + {formatRate(row.rates.standard)} + + - - - )} - - - {currencyAmountFromCents(calculateRowTotal(row))} - - - handleEdit(row)} - > - - - -
- ))} + {formatRate(row.rates.onsite)} + + + + + + {currencyAmountFromCents(calculateRowTotal(row))} + + + toggleRow(row.form_item_id)} + > + + + + + + + + + + + + + ); + })} {T.translate("sponsor_edit_form.discount")} {/* eslint-disable-next-line */} - {new Array(totalColumns - 5).fill(0).map((_, i) => ( + {new Array(totalColumns - 4).fill(0).map((_, i) => ( ))} - + {Object.values(DISCOUNT_TYPES).map((p) => ( {p} @@ -268,16 +309,15 @@ const FormItemTable = ({ ))} - @@ -309,3 +349,4 @@ export { getCurrentApplicableRate, isItemAvailable } from "./helpers"; export { default as GlobalQuantityField } from "./components/GlobalQuantityField"; export { default as ItemTableField } from "./components/ItemTableField"; export { default as UnderlyingAlertNote } from "./components/UnderlyingAlertNote"; +export { default as ExpandedRowContent } from "./components/ExpandedRowContent"; diff --git a/src/components/mui/formik-inputs/mui-formik-checkbox.js b/src/components/mui/formik-inputs/mui-formik-checkbox.js index 667e493a..f5181538 100644 --- a/src/components/mui/formik-inputs/mui-formik-checkbox.js +++ b/src/components/mui/formik-inputs/mui-formik-checkbox.js @@ -21,13 +21,13 @@ import { } from "@mui/material"; import { useField } from "formik"; -const MuiFormikCheckbox = ({ name, label, ...props }) => { +const MuiFormikCheckbox = ({ name, label, margin = "normal", ...props }) => { const [field, meta] = useField({ name, type: "checkbox" }); return ( { const [field, meta, helpers] = useField(name); - const requiredLabel = `${label} *`; + const displayLabel = required ? `${label} *` : label; return ( @@ -34,15 +35,6 @@ const MuiFormikDatepicker = ({ value={field.value} onChange={helpers.setValue} slotProps={{ - textField: { - name, - label: required ? requiredLabel : label, - error: meta.touched && Boolean(meta.error), - helperText: meta.touched && meta.error, - fullWidth: true, - disabled, - size: "small" - }, day: { sx: { fontSize: "1.2rem", @@ -55,6 +47,17 @@ const MuiFormikDatepicker = ({ fontSize: "1rem" } } + }, + ...externalSlotProps, + textField: { + name, + label: displayLabel, + error: meta.touched && Boolean(meta.error), + helperText: meta.touched && meta.error, + fullWidth: true, + disabled, + size: "small", + ...(externalSlotProps?.textField || {}) } }} margin="normal" diff --git a/src/components/mui/formik-inputs/mui-formik-select-v2.js b/src/components/mui/formik-inputs/mui-formik-select-v2.js index 27b3d6a0..ddcd85a4 100644 --- a/src/components/mui/formik-inputs/mui-formik-select-v2.js +++ b/src/components/mui/formik-inputs/mui-formik-select-v2.js @@ -10,14 +10,14 @@ import { } from "@mui/material"; import { useField } from "formik"; -const MuiFormikSelectV2 = ({ name, label, placeholder, options, ...rest }) => { +const MuiFormikSelectV2 = ({ name, label, placeholder, options, required, ...rest }) => { const [field, meta] = useField(name); const finalPlaceholder = placeholder || T.translate("placeholders.select"); return ( - - {label && {label}} + + {label && {label}}