Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/actions/form-template-item-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
};

Expand Down
10 changes: 4 additions & 6 deletions src/actions/inventory-item-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
};
Expand Down
27 changes: 13 additions & 14 deletions src/actions/sponsor-forms-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ 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 {
escapeFilterValue,
getAccessTokenSafely,
normalizeSelectAllField
} from "../utils/methods";
import { rateToCents, RATE_FIELDS } from "../utils/rate-helpers";
import {
DEFAULT_CURRENT_PAGE,
DEFAULT_ORDER_DIR,
Expand Down Expand Up @@ -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 =
Expand Down
171 changes: 171 additions & 0 deletions src/components/mui/__tests__/item-price-tiers.test.js
Original file line number Diff line number Diff line change
@@ -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(
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<ItemPriceTiers {...props} />
<button type="submit">submit</button>
</Form>
</Formik>
);

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());
});
});
});
17 changes: 10 additions & 7 deletions src/components/mui/editable-table/mui-table-editable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
};

Expand Down Expand Up @@ -267,13 +270,13 @@ const MuiTableEditable = ({
{columns.map((col) => (
<TableCell
key={`${row.id}-${col.columnKey}`}
onClick={() => 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) ? (
<EditableCell
value={row[col.columnKey]}
isEditing={
Expand Down
88 changes: 88 additions & 0 deletions src/components/mui/formik-inputs/item-price-tiers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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, isRateEnabled } from "../../../utils/rate-helpers";

const TIERS = [
{ field: RATE_FIELDS.EARLY_BIRD, label: "price_tiers.early_bird_rate" },
{ field: RATE_FIELDS.STANDARD, label: "price_tiers.standard_rate" },
{ field: RATE_FIELDS.ONSITE, label: "price_tiers.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 ? 0 : null);
};

return (
<Grid2 container spacing={2} size={12}>
{TIERS.map(({ field, label }) => {
const isEnabled = enabled[field];
return (
<Grid2 key={field} size={4}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between"
}}
>
<InputLabel htmlFor={field}>{T.translate(label)}</InputLabel>
<FormControlLabel
control={
<Checkbox
checked={isEnabled}
onChange={(ev) => handleToggle(field, ev.target.checked)}
size="small"
disabled={readOnly}
/>
}
label={T.translate(
isEnabled
? "price_tiers.available"
: "price_tiers.not_available"
)}
/>
</Box>
<Box>
{isEnabled ? (
<MuiFormikPriceField
name={field}
fullWidth
disabled={readOnly}
/>
) : (
<TextField
disabled
value={T.translate("price_tiers.not_available")}
fullWidth
variant="outlined"
margin="normal"
/>
)}
</Box>
</Grid2>
);
})}
</Grid2>
);
};

export default ItemPriceTiers;
Loading
Loading