diff --git a/src/actions/form-template-item-actions.js b/src/actions/form-template-item-actions.js
index 419f96fa2..705e81930 100644
--- a/src/actions/form-template-item-actions.js
+++ b/src/actions/form-template-item-actions.js
@@ -22,7 +22,7 @@ import {
startLoading,
escapeFilterValue
} from "openstack-uicore-foundation/lib/utils/actions";
-import { amountToCents } from "openstack-uicore-foundation/lib/utils/money";
+import { rateToCents } from "../utils/rate-helpers";
import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions";
import { getAccessTokenSafely } from "../utils/methods";
import {
@@ -178,13 +178,11 @@ const normalizeEntity = (entity) => {
normalizedEntity.images = normalizedEntity.images?.filter(
(img) => img.file_path
);
- normalizedEntity.early_bird_rate = amountToCents(
+ normalizedEntity.early_bird_rate = rateToCents(
normalizedEntity.early_bird_rate
);
- normalizedEntity.standard_rate = amountToCents(
- normalizedEntity.standard_rate
- );
- normalizedEntity.onsite_rate = amountToCents(normalizedEntity.onsite_rate);
+ normalizedEntity.standard_rate = rateToCents(normalizedEntity.standard_rate);
+ normalizedEntity.onsite_rate = rateToCents(normalizedEntity.onsite_rate);
return normalizedEntity;
};
diff --git a/src/actions/inventory-item-actions.js b/src/actions/inventory-item-actions.js
index d0d2422a1..ced14ab19 100644
--- a/src/actions/inventory-item-actions.js
+++ b/src/actions/inventory-item-actions.js
@@ -25,7 +25,7 @@ import {
authErrorHandler,
escapeFilterValue
} from "openstack-uicore-foundation/lib/utils/actions";
-import { amountToCents } from "openstack-uicore-foundation/lib/utils/money";
+import { rateToCents } from "../utils/rate-helpers";
import history from "../history";
import { getAccessTokenSafely } from "../utils/methods";
import {
@@ -200,13 +200,11 @@ const normalizeEntity = (entity) => {
(img) => img.file_path
);
- normalizedEntity.early_bird_rate = amountToCents(
+ normalizedEntity.early_bird_rate = rateToCents(
normalizedEntity.early_bird_rate
);
- normalizedEntity.standard_rate = amountToCents(
- normalizedEntity.standard_rate
- );
- normalizedEntity.onsite_rate = amountToCents(normalizedEntity.onsite_rate);
+ normalizedEntity.standard_rate = rateToCents(normalizedEntity.standard_rate);
+ normalizedEntity.onsite_rate = rateToCents(normalizedEntity.onsite_rate);
return normalizedEntity;
};
diff --git a/src/actions/sponsor-forms-actions.js b/src/actions/sponsor-forms-actions.js
index 92a2a0ee0..9bb70cf6e 100644
--- a/src/actions/sponsor-forms-actions.js
+++ b/src/actions/sponsor-forms-actions.js
@@ -22,7 +22,6 @@ import {
stopLoading
} from "openstack-uicore-foundation/lib/utils/actions";
-import { amountToCents } from "openstack-uicore-foundation/lib/utils/money";
import T from "i18n-react/dist/i18n-react";
import moment from "moment-timezone";
import {
@@ -30,6 +29,7 @@ import {
getAccessTokenSafely,
normalizeSelectAllField
} from "../utils/methods";
+import { rateToCents, RATE_FIELDS } from "../utils/rate-helpers";
import {
DEFAULT_CURRENT_PAGE,
DEFAULT_ORDER_DIR,
@@ -1369,19 +1369,18 @@ const normalizeManagedItem = (entity) => {
};
const normalizeRates = (entity, normalizedEntity) => {
- const { early_bird_rate, standard_rate, onsite_rate } = entity;
-
- if (early_bird_rate === "" || early_bird_rate === undefined)
- delete normalizedEntity.early_bird_rate;
- else normalizedEntity.early_bird_rate = amountToCents(early_bird_rate);
-
- if (standard_rate === "" || standard_rate === undefined)
- delete normalizedEntity.standard_rate;
- else normalizedEntity.standard_rate = amountToCents(standard_rate);
-
- if (onsite_rate === "" || onsite_rate === undefined)
- delete normalizedEntity.onsite_rate;
- else normalizedEntity.onsite_rate = amountToCents(onsite_rate);
+ if (RATE_FIELDS.EARLY_BIRD in entity)
+ normalizedEntity[RATE_FIELDS.EARLY_BIRD] = rateToCents(
+ entity[RATE_FIELDS.EARLY_BIRD]
+ );
+ if (RATE_FIELDS.STANDARD in entity)
+ normalizedEntity[RATE_FIELDS.STANDARD] = rateToCents(
+ entity[RATE_FIELDS.STANDARD]
+ );
+ if (RATE_FIELDS.ONSITE in entity)
+ normalizedEntity[RATE_FIELDS.ONSITE] = rateToCents(
+ entity[RATE_FIELDS.ONSITE]
+ );
};
export const deleteSponsorFormManagedItem =
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 000000000..f3977a8fb
--- /dev/null
+++ b/src/components/mui/__tests__/item-price-tiers.test.js
@@ -0,0 +1,171 @@
+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 checked", () => {
+ renderComponent(ALL_ENABLED);
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(3);
+ checkboxes.forEach((cb) => expect(cb).toBeChecked());
+ });
+
+ it("should show no disabled N/A text fields", () => {
+ renderComponent(ALL_ENABLED);
+ const naFields = screen
+ .queryAllByDisplayValue("price_tiers.not_available")
+ .filter((el) => el.disabled);
+ expect(naFields).toHaveLength(0);
+ });
+ });
+
+ describe("all disabled", () => {
+ it("should render all 3 checkboxes as unchecked", () => {
+ renderComponent(ALL_DISABLED);
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes).toHaveLength(3);
+ checkboxes.forEach((cb) => expect(cb).not.toBeChecked());
+ });
+
+ it("should show 3 disabled N/A text fields", () => {
+ renderComponent(ALL_DISABLED);
+ const naFields = screen.getAllByDisplayValue("price_tiers.not_available");
+ expect(naFields).toHaveLength(3);
+ naFields.forEach((el) => expect(el).toBeDisabled());
+ });
+ });
+
+ 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(2);
+ expect(unchecked).toHaveLength(1);
+ });
+
+ it("should show 1 disabled N/A text field", () => {
+ renderComponent(MIXED);
+ const naFields = screen.getAllByDisplayValue("price_tiers.not_available");
+ expect(naFields).toHaveLength(1);
+ });
+ });
+
+ describe("toggle on", () => {
+ it("should enable a tier when its checkbox is clicked from unchecked", async () => {
+ renderComponent(ALL_DISABLED);
+ const checkboxes = screen.getAllByRole("checkbox");
+
+ await act(async () => {
+ await userEvent.click(checkboxes[0]);
+ });
+
+ expect(checkboxes[0]).toBeChecked();
+ const naFields = screen.queryAllByDisplayValue(
+ "price_tiers.not_available"
+ );
+ expect(naFields).toHaveLength(2);
+ });
+
+ it("should set the field value to 0 when toggled on", 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("toggle off", () => {
+ it("should disable a tier when its checkbox is clicked from checked", async () => {
+ renderComponent(ALL_ENABLED);
+ const checkboxes = screen.getAllByRole("checkbox");
+
+ await act(async () => {
+ await userEvent.click(checkboxes[0]);
+ });
+
+ expect(checkboxes[0]).not.toBeChecked();
+ const naFields = screen.queryAllByDisplayValue(
+ "price_tiers.not_available"
+ );
+ expect(naFields).toHaveLength(1);
+ });
+
+ it("should set the field value to null when toggled off", 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("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/editable-table/mui-table-editable.js b/src/components/mui/editable-table/mui-table-editable.js
index 47800d68e..2e3e69237 100644
--- a/src/components/mui/editable-table/mui-table-editable.js
+++ b/src/components/mui/editable-table/mui-table-editable.js
@@ -193,12 +193,15 @@ const MuiTableEditable = ({
}
};
+ const isEditable = (col, row) =>
+ typeof col.editable === "function" ? col.editable(row) : !!col.editable;
+
// Handler for starting edit mode on a cell
- const handleCellClick = (rowId, columnKey) => {
+ const handleCellClick = (row, columnKey) => {
// Check if the column is editable
const column = columns.find((col) => col.columnKey === columnKey);
- if (column && column.editable) {
- setEditingCell({ rowId, columnKey });
+ if (column && isEditable(column, row)) {
+ setEditingCell({ rowId: row.id, columnKey });
}
};
@@ -267,13 +270,13 @@ const MuiTableEditable = ({
{columns.map((col) => (
handleCellClick(row.id, col.columnKey)}
+ onClick={() => handleCellClick(row, col.columnKey)}
sx={getCellSx(row, {
- cursor: col.editable ? "pointer" : "default",
- padding: col.editable ? "8px 16px" : undefined // Ensure enough space for the edit icon
+ cursor: isEditable(col, row) ? "pointer" : "default",
+ padding: isEditable(col, row) ? "8px 16px" : undefined // Ensure enough space for the edit icon
})}
>
- {col.editable ? (
+ {isEditable(col, row) ? (
{
+ 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 ? 0 : null);
+ };
+
+ return (
+
+ {TIERS.map(({ field, label }) => {
+ const isEnabled = enabled[field];
+ return (
+
+
+ {T.translate(label)}
+ handleToggle(field, ev.target.checked)}
+ size="small"
+ disabled={readOnly}
+ />
+ }
+ label={T.translate(
+ isEnabled
+ ? "price_tiers.available"
+ : "price_tiers.not_available"
+ )}
+ />
+
+
+ {isEnabled ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+ );
+};
+
+export default ItemPriceTiers;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 38bff22d6..2f4e01bd2 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -91,7 +91,7 @@
"file_required": "File is required",
"string": "Must be a string.",
"number": "Must be a number.",
- "number_positive": "Must be a positive number.",
+ "non_negative": "Must be a non-negative number.",
"integer": "Must be a integer",
"one_option_required": "Must have one option",
"two_decimals": "Max 2 decimal places",
@@ -107,6 +107,13 @@
"wrong_format": "Wrong format.",
"add_on_required": "Select at least one add-on"
},
+ "price_tiers": {
+ "early_bird_rate": "Early Bird Rate",
+ "standard_rate": "Standard Rate",
+ "onsite_rate": "Onsite Rate",
+ "available": "Available",
+ "not_available": "N/A"
+ },
"landing": {
"os_summit_admin": "Show Admin",
"sign_out": "Sign out",
diff --git a/src/pages/sponsors-global/form-templates/add-form-template-item-popup.js b/src/pages/sponsors-global/form-templates/add-form-template-item-popup.js
index cd8c9b212..aac1d2c05 100644
--- a/src/pages/sponsors-global/form-templates/add-form-template-item-popup.js
+++ b/src/pages/sponsors-global/form-templates/add-form-template-item-popup.js
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
import T from "i18n-react/dist/i18n-react";
-import { currencyAmountFromCents } from "openstack-uicore-foundation/lib/utils/money";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import {
@@ -31,6 +30,7 @@ import {
setSelectedAll,
unSelectInventoryItem
} from "../../../actions/inventory-item-actions";
+import { formatRateFromCents } from "../../../utils/rate-helpers";
import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants";
const AddFormTemplateItemDialog = ({
@@ -129,7 +129,7 @@ const AddFormTemplateItemDialog = ({
"inventory_items_list_modal.early_bid_rate_column_label"
),
sortable: false,
- render: (row) => currencyAmountFromCents(row.early_bird_rate)
+ render: (row) => formatRateFromCents(row.early_bird_rate)
},
{
columnKey: "standard_rate",
@@ -137,7 +137,7 @@ const AddFormTemplateItemDialog = ({
"inventory_items_list_modal.standard_rate_column_label"
),
sortable: false,
- render: (row) => currencyAmountFromCents(row.standard_rate)
+ render: (row) => formatRateFromCents(row.standard_rate)
},
{
columnKey: "onsite_rate",
@@ -145,7 +145,7 @@ const AddFormTemplateItemDialog = ({
"inventory_items_list_modal.onsite_rate_column_label"
),
sortable: false,
- render: (row) => currencyAmountFromCents(row.onsite_rate)
+ render: (row) => formatRateFromCents(row.onsite_rate)
},
{
columnKey: "default_quantity",
diff --git a/src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js b/src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js
index 1ea41a94c..ffe9c6489 100644
--- a/src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js
+++ b/src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js
@@ -25,17 +25,17 @@ import {
MAX_INVENTORY_IMAGES_UPLOAD_QTY
} from "../../../utils/constants";
import MuiFormikTextField from "../../../components/mui/formik-inputs/mui-formik-textfield";
-import MuiFormikPriceField from "../../../components/mui/formik-inputs/mui-formik-pricefield";
import useScrollToError from "../../../hooks/useScrollToError";
import FormikTextEditor from "../../../components/inputs/formik-text-editor";
import {
- decimalValidation,
+ nullableDecimalValidation,
requiredStringValidation,
positiveNumberValidation,
formMetafieldsValidation,
requiredHTMLValidation
} from "../../../utils/yup";
import AdditionalInputList from "../../../components/mui/formik-inputs/additional-input/additional-input-list";
+import ItemPriceTiers from "../../../components/mui/formik-inputs/item-price-tiers";
import MuiFormikQuantityField from "../../../components/mui/formik-inputs/mui-formik-quantity-field";
import { getMediaInputValue } from "../../../utils/methods";
@@ -60,9 +60,9 @@ const SponsorItemDialog = ({
name: requiredStringValidation(),
description: requiredHTMLValidation(),
images: yup.array(),
- early_bird_rate: decimalValidation(),
- standard_rate: decimalValidation(),
- onsite_rate: decimalValidation(),
+ early_bird_rate: nullableDecimalValidation(),
+ standard_rate: nullableDecimalValidation(),
+ onsite_rate: nullableDecimalValidation(),
default_quantity: positiveNumberValidation(),
quantity_limit_per_sponsor: positiveNumberValidation(),
quantity_limit_per_show: positiveNumberValidation(),
@@ -174,36 +174,7 @@ const SponsorItemDialog = ({
-
-
- {T.translate("edit_inventory_item.early_bird_rate")} *
-
-
-
-
-
- {T.translate("edit_inventory_item.standard_rate")} *
-
-
-
-
-
- {T.translate("edit_inventory_item.onsite_rate")} *
-
-
-
+
diff --git a/src/pages/sponsors-global/page-templates/page-template-popup/index.js b/src/pages/sponsors-global/page-templates/page-template-popup/index.js
index 6b75729f4..95e01f8d0 100644
--- a/src/pages/sponsors-global/page-templates/page-template-popup/index.js
+++ b/src/pages/sponsors-global/page-templates/page-template-popup/index.js
@@ -68,7 +68,7 @@ const PageTemplatePopup = ({ pageTemplate, onClose, onSave, sponsorships, summit
is: PAGE_MODULES_MEDIA_TYPES.FILE,
then: (schema) =>
schema
- .min(BYTES_PER_MB, T.translate("validation.number_positive"))
+ .min(BYTES_PER_MB, T.translate("validation.non_negative"))
.required(T.translate("validation.required"))
.test(
"mib-aligned",
diff --git a/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-add-item-from-inventory-popup.js b/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-add-item-from-inventory-popup.js
index 42b116741..e06be44a7 100644
--- a/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-add-item-from-inventory-popup.js
+++ b/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-add-item-from-inventory-popup.js
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
-import { currencyAmountFromCents } from "openstack-uicore-foundation/lib/utils/money";
import T from "i18n-react/dist/i18n-react";
import { connect } from "react-redux";
import {
@@ -20,6 +19,7 @@ import CloseIcon from "@mui/icons-material/Close";
import Tooltip from "@mui/material/Tooltip";
import ImageIcon from "@mui/icons-material/Image";
import Box from "@mui/material/Box";
+import { formatRateFromCents } from "../../../../utils/rate-helpers";
import SearchInput from "../../../../components/mui/search-input";
import MuiTable from "../../../../components/mui/table/mui-table";
import { addInventoryItems } from "../../../../actions/sponsor-forms-actions";
@@ -123,7 +123,7 @@ const SponsorFormAddItemFromInventoryPopup = ({
"sponsor_form_item_list.add_from_inventory.early_bird_rate"
),
sortable: false,
- render: (row) => currencyAmountFromCents(row.early_bird_rate)
+ render: (row) => formatRateFromCents(row.early_bird_rate)
},
{
columnKey: "standard_rate",
@@ -131,7 +131,7 @@ const SponsorFormAddItemFromInventoryPopup = ({
"sponsor_form_item_list.add_from_inventory.standard_rate"
),
sortable: false,
- render: (row) => currencyAmountFromCents(row.standard_rate)
+ render: (row) => formatRateFromCents(row.standard_rate)
},
{
columnKey: "onsite_rate",
@@ -139,7 +139,7 @@ const SponsorFormAddItemFromInventoryPopup = ({
"sponsor_form_item_list.add_from_inventory.onsite_rate"
),
sortable: false,
- render: (row) => currencyAmountFromCents(row.onsite_rate)
+ render: (row) => formatRateFromCents(row.onsite_rate)
},
{
columnKey: "hasImage",
diff --git a/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js b/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js
index 7176b4b96..067add164 100644
--- a/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js
+++ b/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js
@@ -15,7 +15,7 @@ import * as yup from "yup";
import { FormikProvider, useFormik } from "formik";
import {
addIssAfterDateFieldValidator,
- decimalValidation,
+ nullableDecimalValidation,
formMetafieldsValidation,
positiveNumberValidation,
requiredHTMLValidation,
@@ -25,7 +25,7 @@ import MuiFormikTextField from "../../../../components/mui/formik-inputs/mui-for
import AdditionalInputList from "../../../../components/mui/formik-inputs/additional-input/additional-input-list";
import useScrollToError from "../../../../hooks/useScrollToError";
import MuiFormikUpload from "../../../../components/mui/formik-inputs/mui-formik-upload";
-import MuiFormikPriceField from "../../../../components/mui/formik-inputs/mui-formik-pricefield";
+import ItemPriceTiers from "../../../../components/mui/formik-inputs/item-price-tiers";
import FormikTextEditor from "../../../../components/inputs/formik-text-editor";
import MuiFormikQuantityField from "../../../../components/mui/formik-inputs/mui-formik-quantity-field";
@@ -40,9 +40,9 @@ const SponsorFormItemForm = ({ initialValues, onSubmit }) => {
code: requiredStringValidation(),
name: requiredStringValidation(),
description: requiredHTMLValidation(),
- early_bird_rate: decimalValidation(),
- standard_rate: decimalValidation(),
- onsite_rate: decimalValidation(),
+ early_bird_rate: nullableDecimalValidation(),
+ standard_rate: nullableDecimalValidation(),
+ onsite_rate: nullableDecimalValidation(),
default_quantity: positiveNumberValidation().required(
T.translate("validation.required")
),
@@ -92,36 +92,7 @@ const SponsorFormItemForm = ({ initialValues, onSubmit }) => {
options={{ zIndex: 9999999 }}
/>
-
-
-
-
-
-
-
-
-
+
+ row.early_bird_rate !== T.translate("price_tiers.not_available"),
validation: {
schema: rateCellValidation()
}
@@ -146,7 +147,8 @@ const SponsorFormItemListPage = ({
columnKey: "standard_rate",
header: T.translate("sponsor_form_item_list.standard_rate"),
sortable: true,
- editable: true,
+ editable: (row) =>
+ row.standard_rate !== T.translate("price_tiers.not_available"),
validation: {
schema: rateCellValidation()
}
@@ -155,7 +157,8 @@ const SponsorFormItemListPage = ({
columnKey: "onsite_rate",
header: T.translate("sponsor_form_item_list.onsite_rate"),
sortable: true,
- editable: true,
+ editable: (row) =>
+ row.onsite_rate !== T.translate("price_tiers.not_available"),
validation: {
schema: rateCellValidation()
}
diff --git a/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js b/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js
index f8b8fd61c..b60707291 100644
--- a/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js
+++ b/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js
@@ -20,7 +20,7 @@ import {
import CloseIcon from "@mui/icons-material/Close";
import ImageIcon from "@mui/icons-material/Image";
import SwapVertIcon from "@mui/icons-material/SwapVert";
-import { currencyAmountFromCents } from "openstack-uicore-foundation/lib/utils/money";
+import { formatRateFromCents } from "../../../../../utils/rate-helpers";
import SearchInput from "../../../../../components/mui/search-input";
import {
DEFAULT_CURRENT_PAGE,
@@ -116,7 +116,7 @@ const SponsorFormItemFromInventoryPopup = ({
"edit_sponsor.forms_tab.form_manage_items.early_bird_rate"
),
sortable: false,
- render: (row) => currencyAmountFromCents(row.early_bird_rate)
+ render: (row) => formatRateFromCents(row.early_bird_rate)
},
{
columnKey: "standard_rate",
@@ -124,7 +124,7 @@ const SponsorFormItemFromInventoryPopup = ({
"edit_sponsor.forms_tab.form_manage_items.standard_rate"
),
sortable: false,
- render: (row) => currencyAmountFromCents(row.standard_rate)
+ render: (row) => formatRateFromCents(row.standard_rate)
},
{
columnKey: "onsite_rate",
@@ -132,7 +132,7 @@ const SponsorFormItemFromInventoryPopup = ({
"edit_sponsor.forms_tab.form_manage_items.onsite_rate"
),
sortable: false,
- render: (row) => currencyAmountFromCents(row.onsite_rate)
+ render: (row) => formatRateFromCents(row.onsite_rate)
},
{
columnKey: "default_quantity",
diff --git a/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js b/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
index 7f2003787..54ceb0de0 100644
--- a/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
+++ b/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
@@ -212,7 +212,8 @@ const SponsorFormsManageItems = ({
"edit_sponsor.forms_tab.form_manage_items.early_bird_rate"
),
sortable: false,
- editable: true,
+ editable: (row) =>
+ row.early_bird_rate !== T.translate("price_tiers.not_available"),
validation: {
schema: rateCellValidation()
}
@@ -223,7 +224,8 @@ const SponsorFormsManageItems = ({
"edit_sponsor.forms_tab.form_manage_items.standard_rate"
),
sortable: false,
- editable: true,
+ editable: (row) =>
+ row.standard_rate !== T.translate("price_tiers.not_available"),
validation: {
schema: rateCellValidation()
}
@@ -234,7 +236,8 @@ const SponsorFormsManageItems = ({
"edit_sponsor.forms_tab.form_manage_items.onsite_rate"
),
sortable: false,
- editable: true,
+ editable: (row) =>
+ row.onsite_rate !== T.translate("price_tiers.not_available"),
validation: {
schema: rateCellValidation()
}
diff --git a/src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js b/src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js
index 562a7f486..a33b5d430 100644
--- a/src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js
+++ b/src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js
@@ -25,9 +25,9 @@ function createDefaultState() {
code: "",
name: "",
description: "",
- early_bird_rate: "",
- standard_rate: "",
- onsite_rate: "",
+ early_bird_rate: 0,
+ standard_rate: 0,
+ onsite_rate: 0,
quantity_limit_per_show: "",
quantity_limit_per_sponsor: "",
default_quantity: "",
@@ -92,9 +92,9 @@ describe("SponsorFormItemsListReducer", () => {
id: "A",
code: "A",
name: "A",
- early_bird_rate: "100",
- standard_rate: "100",
- onsite_rate: "100",
+ early_bird_rate: 100,
+ standard_rate: 100,
+ onsite_rate: 100,
default_quantity: "100",
is_archived: true,
images: []
@@ -163,9 +163,9 @@ describe("SponsorFormItemsListReducer", () => {
id: "A",
code: "A",
name: "A",
- early_bird_rate: "100",
- standard_rate: "100",
- onsite_rate: "100",
+ early_bird_rate: 100,
+ standard_rate: 100,
+ onsite_rate: 100,
default_quantity: "100",
is_archived: true,
images: [],
diff --git a/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js b/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js
index 2bb0d7c57..3d47f6452 100644
--- a/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js
+++ b/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js
@@ -12,10 +12,7 @@
* */
import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions";
-import {
- amountFromCents,
- currencyAmountFromCents
-} from "openstack-uicore-foundation/lib/utils/money";
+import { formatRateFromCents, rateFromCents } from "../../utils/rate-helpers";
import {
RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEMS,
REQUEST_SPONSOR_CUSTOMIZED_FORM_ITEMS,
@@ -92,9 +89,9 @@ const sponsorCustomizedFormItemsListReducer = (
id: a.id,
code: a.code,
name: a.name,
- early_bird_rate: currencyAmountFromCents(a.early_bird_rate),
- standard_rate: currencyAmountFromCents(a.standard_rate),
- onsite_rate: currencyAmountFromCents(a.onsite_rate),
+ early_bird_rate: formatRateFromCents(a.early_bird_rate),
+ standard_rate: formatRateFromCents(a.standard_rate),
+ onsite_rate: formatRateFromCents(a.onsite_rate),
default_quantity: a.default_quantity,
is_archived: a.is_archived,
images: a.images
@@ -113,9 +110,9 @@ const sponsorCustomizedFormItemsListReducer = (
const currentItem = {
...item,
- early_bird_rate: amountFromCents(item.early_bird_rate),
- standard_rate: amountFromCents(item.standard_rate),
- onsite_rate: amountFromCents(item.onsite_rate),
+ early_bird_rate: rateFromCents(item.early_bird_rate),
+ standard_rate: rateFromCents(item.standard_rate),
+ onsite_rate: rateFromCents(item.onsite_rate),
meta_fields: item.meta_fields.length > 0 ? item.meta_fields : []
};
return { ...state, currentItem };
@@ -152,11 +149,9 @@ const sponsorCustomizedFormItemsListReducer = (
id: updatedItem.id,
code: updatedItem.code,
name: updatedItem.name,
- early_bird_rate: currencyAmountFromCents(
- updatedItem.early_bird_rate
- ),
- standard_rate: currencyAmountFromCents(updatedItem.standard_rate),
- onsite_rate: currencyAmountFromCents(updatedItem.onsite_rate),
+ early_bird_rate: formatRateFromCents(updatedItem.early_bird_rate),
+ standard_rate: formatRateFromCents(updatedItem.standard_rate),
+ onsite_rate: formatRateFromCents(updatedItem.onsite_rate),
default_quantity: updatedItem.default_quantity,
is_archived: updatedItem.is_archived,
images: updatedItem.images
diff --git a/src/reducers/sponsors/sponsor-form-items-list-reducer.js b/src/reducers/sponsors/sponsor-form-items-list-reducer.js
index e15f1dcff..a9779cdfe 100644
--- a/src/reducers/sponsors/sponsor-form-items-list-reducer.js
+++ b/src/reducers/sponsors/sponsor-form-items-list-reducer.js
@@ -11,8 +11,8 @@
* limitations under the License.
* */
-import { amountFromCents } from "openstack-uicore-foundation/lib/utils/money";
import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions";
+import { formatRateFromCents, rateFromCents } from "../../utils/rate-helpers";
import {
RECEIVE_SPONSOR_FORM_ITEM,
RECEIVE_SPONSOR_FORM_ITEMS,
@@ -37,9 +37,9 @@ const DEFAULT_STATE = {
code: "",
name: "",
description: "",
- early_bird_rate: "",
- standard_rate: "",
- onsite_rate: "",
+ early_bird_rate: 0,
+ standard_rate: 0,
+ onsite_rate: 0,
quantity_limit_per_show: "",
quantity_limit_per_sponsor: "",
default_quantity: "",
@@ -79,9 +79,9 @@ const sponsorFormItemsListReducer = (state = DEFAULT_STATE, action) => {
id: a.id,
code: a.code,
name: a.name,
- early_bird_rate: `$${amountFromCents(a.early_bird_rate)}`,
- standard_rate: `$${amountFromCents(a.standard_rate)}`,
- onsite_rate: `$${amountFromCents(a.onsite_rate)}`,
+ early_bird_rate: formatRateFromCents(a.early_bird_rate),
+ standard_rate: formatRateFromCents(a.standard_rate),
+ onsite_rate: formatRateFromCents(a.onsite_rate),
default_quantity: a.default_quantity,
is_archived: a.is_archived,
images: a.images
@@ -100,9 +100,9 @@ const sponsorFormItemsListReducer = (state = DEFAULT_STATE, action) => {
const currentItem = {
...item,
- early_bird_rate: amountFromCents(item.early_bird_rate),
- standard_rate: amountFromCents(item.standard_rate),
- onsite_rate: amountFromCents(item.onsite_rate),
+ early_bird_rate: rateFromCents(item.early_bird_rate),
+ standard_rate: rateFromCents(item.standard_rate),
+ onsite_rate: rateFromCents(item.onsite_rate),
meta_fields: item.meta_fields.length > 0 ? item.meta_fields : []
};
diff --git a/src/reducers/sponsors_inventory/form-template-item-reducer.js b/src/reducers/sponsors_inventory/form-template-item-reducer.js
index 3c41fb11a..8b900dc94 100644
--- a/src/reducers/sponsors_inventory/form-template-item-reducer.js
+++ b/src/reducers/sponsors_inventory/form-template-item-reducer.js
@@ -12,7 +12,7 @@
* */
import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions";
-import { amountFromCents } from "openstack-uicore-foundation/lib/utils/money";
+import { RATE_FIELDS, rateFromCents } from "../../utils/rate-helpers";
import {
RECEIVE_FORM_TEMPLATE_ITEM,
RESET_FORM_TEMPLATE_ITEM_FORM,
@@ -62,16 +62,17 @@ const formTemplateItemReducer = (state = DEFAULT_STATE, action) => {
}
case RECEIVE_FORM_TEMPLATE_ITEM: {
const entity = { ...payload.response };
+ const rateFieldValues = Object.values(RATE_FIELDS);
for (const key in entity) {
- if (entity.hasOwnProperty(key)) {
+ if (entity.hasOwnProperty(key) && !rateFieldValues.includes(key)) {
entity[key] = entity[key] == null ? "" : entity[key];
}
}
- entity.early_bird_rate = amountFromCents(entity.early_bird_rate);
- entity.standard_rate = amountFromCents(entity.standard_rate);
- entity.onsite_rate = amountFromCents(entity.onsite_rate);
+ entity.early_bird_rate = rateFromCents(entity.early_bird_rate);
+ entity.standard_rate = rateFromCents(entity.standard_rate);
+ entity.onsite_rate = rateFromCents(entity.onsite_rate);
return {
...state,
@@ -84,16 +85,17 @@ const formTemplateItemReducer = (state = DEFAULT_STATE, action) => {
case FORM_TEMPLATE_ITEM_ADDED:
case FORM_TEMPLATE_ITEM_UPDATED: {
const entity = { ...payload.response };
+ const rateFieldValues = Object.values(RATE_FIELDS);
for (const key in entity) {
- if (entity.hasOwnProperty(key)) {
+ if (entity.hasOwnProperty(key) && !rateFieldValues.includes(key)) {
entity[key] = entity[key] == null ? "" : entity[key];
}
}
- entity.early_bird_rate = amountFromCents(entity.early_bird_rate);
- entity.standard_rate = amountFromCents(entity.standard_rate);
- entity.onsite_rate = amountFromCents(entity.onsite_rate);
+ entity.early_bird_rate = rateFromCents(entity.early_bird_rate);
+ entity.standard_rate = rateFromCents(entity.standard_rate);
+ entity.onsite_rate = rateFromCents(entity.onsite_rate);
return {
...state,
diff --git a/src/reducers/sponsors_inventory/inventory-item-reducer.js b/src/reducers/sponsors_inventory/inventory-item-reducer.js
index d55840300..b70bf43b7 100644
--- a/src/reducers/sponsors_inventory/inventory-item-reducer.js
+++ b/src/reducers/sponsors_inventory/inventory-item-reducer.js
@@ -12,7 +12,7 @@
* */
import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions";
-import { amountFromCents } from "openstack-uicore-foundation/lib/utils/money";
+import { RATE_FIELDS, rateFromCents } from "../../utils/rate-helpers";
import {
RECEIVE_INVENTORY_ITEM,
RESET_INVENTORY_ITEM_FORM,
@@ -61,16 +61,17 @@ const inventoryItemReducer = (state = DEFAULT_STATE, action) => {
}
case RECEIVE_INVENTORY_ITEM: {
const entity = { ...payload.response };
+ const rateFieldValues = Object.values(RATE_FIELDS);
for (const key in entity) {
- if (entity.hasOwnProperty(key)) {
+ if (entity.hasOwnProperty(key) && !rateFieldValues.includes(key)) {
entity[key] = entity[key] == null ? "" : entity[key];
}
}
- entity.early_bird_rate = amountFromCents(entity.early_bird_rate);
- entity.standard_rate = amountFromCents(entity.standard_rate);
- entity.onsite_rate = amountFromCents(entity.onsite_rate);
+ entity.early_bird_rate = rateFromCents(entity.early_bird_rate);
+ entity.standard_rate = rateFromCents(entity.standard_rate);
+ entity.onsite_rate = rateFromCents(entity.onsite_rate);
return {
...state,
@@ -83,16 +84,17 @@ const inventoryItemReducer = (state = DEFAULT_STATE, action) => {
case INVENTORY_ITEM_ADDED:
case INVENTORY_ITEM_UPDATED: {
const entity = { ...payload.response };
+ const rateFieldValues = Object.values(RATE_FIELDS);
for (const key in entity) {
- if (entity.hasOwnProperty(key)) {
+ if (entity.hasOwnProperty(key) && !rateFieldValues.includes(key)) {
entity[key] = entity[key] == null ? "" : entity[key];
}
}
- entity.early_bird_rate = amountFromCents(entity.early_bird_rate);
- entity.standard_rate = amountFromCents(entity.standard_rate);
- entity.onsite_rate = amountFromCents(entity.onsite_rate);
+ entity.early_bird_rate = rateFromCents(entity.early_bird_rate);
+ entity.standard_rate = rateFromCents(entity.standard_rate);
+ entity.onsite_rate = rateFromCents(entity.onsite_rate);
return {
...state,
diff --git a/src/utils/__tests__/rate-helpers.test.js b/src/utils/__tests__/rate-helpers.test.js
new file mode 100644
index 000000000..413c34c31
--- /dev/null
+++ b/src/utils/__tests__/rate-helpers.test.js
@@ -0,0 +1,70 @@
+import {
+ isRateEnabled,
+ rateFromCents,
+ rateToCents,
+ formatRateFromCents
+} from "../rate-helpers";
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ translate: (key) => key
+}));
+
+describe("isRateEnabled", () => {
+ it("should return false for null", () => {
+ expect(isRateEnabled(null)).toBe(false);
+ });
+
+ it("should return true for 0", () => {
+ expect(isRateEnabled(0)).toBe(true);
+ });
+
+ it("should returns true for a positive value", () => {
+ expect(isRateEnabled(10)).toBe(true);
+ });
+});
+
+describe("rateFromCents", () => {
+ it("should return null for null", () => {
+ expect(rateFromCents(null)).toBeNull();
+ });
+
+ it("should display 0 cents as 0.00", () => {
+ expect(rateFromCents(0)).toBe("0.00");
+ });
+
+ it("should display cents with decimals correctly", () => {
+ expect(rateFromCents(1050)).toBe("10.50");
+ });
+});
+
+describe("rateToCents", () => {
+ it("should return null for null", () => {
+ expect(rateToCents(null)).toBeNull();
+ });
+
+ it("should return 0 for empty string (cleared input field)", () => {
+ expect(rateToCents("")).toBe(0);
+ });
+
+ it("should converts 0 to 0 cents", () => {
+ expect(rateToCents(0)).toBe(0);
+ });
+
+ it("should converts a decimal dollar value to cents", () => {
+ expect(rateToCents(12.3)).toBe(1230);
+ });
+});
+
+describe("formatRateFromCents", () => {
+ it("should returns the N/A translation key for null", () => {
+ expect(formatRateFromCents(null)).toBe("price_tiers.not_available");
+ });
+
+ it("should format 0 cents as $0.00", () => {
+ expect(formatRateFromCents(0)).toBe("$0.00");
+ });
+
+ it("should format cents with decimals correctly", () => {
+ expect(formatRateFromCents(1234)).toBe("$12.34");
+ });
+});
diff --git a/src/utils/__tests__/yup.test.js b/src/utils/__tests__/yup.test.js
new file mode 100644
index 000000000..bdba5c50a
--- /dev/null
+++ b/src/utils/__tests__/yup.test.js
@@ -0,0 +1,38 @@
+import { nullableDecimalValidation } from "../yup";
+
+jest.mock("i18n-react", () => ({
+ translate: (key) => key
+}));
+
+describe("nullableDecimalValidation", () => {
+ const schema = nullableDecimalValidation();
+
+ it("should pass for null", async () => {
+ await expect(schema.isValid(null)).resolves.toBe(true);
+ });
+
+ it("should pass for 0", async () => {
+ await expect(schema.isValid(0)).resolves.toBe(true);
+ });
+
+ it("should pass for a positive integer", async () => {
+ await expect(schema.isValid(10)).resolves.toBe(true);
+ });
+
+ it("should pass for numbers up to 2 decimal places", async () => {
+ await expect(schema.isValid(10.5)).resolves.toBe(true);
+ await expect(schema.isValid(10.55)).resolves.toBe(true);
+ });
+
+ it("should treat empty string as 0 (cleared input field)", async () => {
+ await expect(schema.cast("")).toBe(0);
+ });
+
+ it("should fail for a negative value", async () => {
+ await expect(schema.isValid(-1)).resolves.toBe(false);
+ });
+
+ it("should fail for 3 or more decimal places", async () => {
+ await expect(schema.isValid(1.234)).resolves.toBe(false);
+ });
+});
diff --git a/src/utils/rate-helpers.js b/src/utils/rate-helpers.js
new file mode 100644
index 000000000..c841e817f
--- /dev/null
+++ b/src/utils/rate-helpers.js
@@ -0,0 +1,32 @@
+import T from "i18n-react";
+import {
+ amountFromCents,
+ amountToCents,
+ currencyAmountFromCents
+} from "openstack-uicore-foundation/lib/utils/money";
+
+export const RATE_FIELDS = {
+ EARLY_BIRD: "early_bird_rate",
+ STANDARD: "standard_rate",
+ ONSITE: "onsite_rate"
+};
+
+export const isRateEnabled = (value) =>
+ value !== null && value !== undefined && value !== "";
+
+export const rateFromCents = (cents) => {
+ if (cents === null || cents === undefined) return null;
+ return amountFromCents(cents);
+};
+
+export const rateToCents = (value) => {
+ if (value === null || value === undefined) return null;
+ if (value === "") return 0;
+ return amountToCents(value);
+};
+
+export const formatRateFromCents = (cents) => {
+ if (cents === null || cents === undefined)
+ return T.translate("price_tiers.not_available");
+ return currencyAmountFromCents(cents);
+};
diff --git a/src/utils/yup.js b/src/utils/yup.js
index 2300c7792..45291c068 100644
--- a/src/utils/yup.js
+++ b/src/utils/yup.js
@@ -47,7 +47,7 @@ export const decimalValidation = () =>
yup
.number()
.typeError(T.translate("validation.number"))
- .positive(T.translate("validation.number_positive"))
+ .min(0, T.translate("validation.non_negative"))
.required(T.translate("validation.required"))
.test("max-decimals", T.translate("validation.two_decimals"), (value) => {
if (value === undefined || value === null) return true;
@@ -60,7 +60,7 @@ export const rateCellValidation = () =>
// allow $ at the start
.transform((value, originalValue) => {
if (typeof originalValue === "string") {
- const cleaned = originalValue.replace(/^\$/, "");
+ const cleaned = originalValue.replace(/^\$/, "").replace(",", ".");
return cleaned === "" ? undefined : parseFloat(cleaned);
}
return value;
@@ -76,10 +76,10 @@ export const rateCellValidation = () =>
originalValue === ""
)
return true;
- return /^\$?-?\d+(\.\d+)?$/.test(originalValue);
+ return /^\$?-?\d+([.,]\d+)?$/.test(originalValue);
}
})
- .positive(T.translate("validation.number_positive"))
+ .min(0, T.translate("validation.non_negative"))
.test("max-decimals", T.translate("validation.two_decimals"), (value) => {
if (value === undefined || value === null) return true;
return /^\d+(\.\d{1,2})?$/.test(value.toString());
@@ -103,7 +103,7 @@ export const requiredHTMLValidation = () =>
export const positiveNumberValidation = () =>
numberValidation()
.integer(T.translate("validation.integer"))
- .min(0, T.translate("validation.number_positive"));
+ .min(0, T.translate("validation.non_negative"));
export const formMetafieldsValidation = () =>
yup.array().of(
@@ -159,3 +159,19 @@ export const opensAtValidation = () =>
yup
.date(T.translate("validation.date"))
.required(T.translate("validation.required"));
+
+export const nullableDecimalValidation = () =>
+ yup
+ .number()
+ .nullable()
+ .transform((value, originalValue) => {
+ if (typeof originalValue === "string" && originalValue.trim() === "")
+ return 0;
+ return value;
+ })
+ .typeError(T.translate("validation.number"))
+ .min(0, T.translate("validation.non_negative"))
+ .test("max-decimals", T.translate("validation.two_decimals"), (value) => {
+ if (value === undefined || value === null) return true;
+ return /^\d+(\.\d{1,2})?$/.test(value.toString());
+ });