Skip to content
Open
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openstack-uicore-foundation",
"version": "5.0.32",
"version": "5.0.33-beta.3",
"description": "ui reactjs components for openstack marketing site",
"main": "lib/openstack-uicore-foundation.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src/components/mui/InfoNote/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const InfoNote = ({ message, sx }) => (
<InfoOutlinedIcon
sx={{ fontSize: 16, color: "text.secondary", mt: "2px" }}
/>
<Typography variant="body1" color="text.secondary">
<Typography variant="body1" color="text.secondary" sx={{fontSize: "13px"}}>
{message}
</Typography>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ jest.mock("../../../../utils/money", () => ({
}));

jest.mock("../../../../utils/constants", () => ({
...jest.requireActual("../../../../utils/constants"),
SPONSOR_FORMS_METAFIELD_CLASS: { FORM: "Form", ITEM: "Item" }
}));

jest.mock("../../../../utils/methods", () => ({
formatEpoch: () => "2026-01-01"
}));

import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
Expand All @@ -33,9 +38,8 @@ const makeItem = (overrides = {}) => ({
line_id: 1,
quantity: 1,
amount: 10000,
current_rate: 5000,
canceled_by_id: null,
type: { name: "Booth" },
type: { name: "Booth", code: "BOOTH" },
meta_fields: [],
...overrides
});
Expand All @@ -44,62 +48,63 @@ const makeForm = (overrides = {}) => ({
id: 10,
code: "GOLD",
name: "Gold Sponsor",
addon_name: "Premium",
discount: null,
discount_total: null,
discount_in_cents: null,
items: [makeItem()],
...overrides
});

const defaultProps = {
lines: [makeForm()],
total: 10000
order: {
forms: [makeForm()],
total: 10000
}
};

describe("SponsorOrderGrid", () => {
test("renders column headers", () => {
render(<SponsorOrderGrid {...defaultProps} />);
expect(screen.getByText("sponsor_order_grid.code")).toBeInTheDocument();
expect(screen.getByText("sponsor_order_grid.contents")).toBeInTheDocument();
expect(screen.getByText("sponsor_order_grid.addon")).toBeInTheDocument();
expect(screen.getByText("sponsor_order_grid.type")).toBeInTheDocument();
expect(screen.getByText("sponsor_order_grid.details")).toBeInTheDocument();
expect(screen.getByText("sponsor_order_grid.rate")).toBeInTheDocument();
expect(screen.getByText("sponsor_order_grid.amount")).toBeInTheDocument();
expect(screen.getByText("sponsor_order_grid.balance")).toBeInTheDocument();
});

test("renders item code and name", () => {
test("renders item form code", () => {
render(<SponsorOrderGrid {...defaultProps} />);
expect(screen.getByText("GOLD")).toBeInTheDocument();
expect(screen.getByText("Gold Sponsor")).toBeInTheDocument();
});

test("renders formatted amount and rate", () => {
test("renders item name in details column", () => {
render(<SponsorOrderGrid {...defaultProps} />);
expect(screen.getByText(/Booth/)).toBeInTheDocument();
});

test("renders formatted charge amount", () => {
render(<SponsorOrderGrid {...defaultProps} />);
expect(screen.getByText("$100.00")).toBeInTheDocument();
expect(screen.getByText("$50.00")).toBeInTheDocument();
expect(screen.getAllByText("$100.00").length).toBeGreaterThan(0);
});

test("renders no-items message when lines is empty", () => {
render(<SponsorOrderGrid lines={[]} total={0} />);
test("renders no-items message when forms is empty", () => {
render(<SponsorOrderGrid order={{ forms: [], total: 0 }} />);
expect(screen.getByText("mui_table.no_items")).toBeInTheDocument();
});

test("renders no-items message when lines is undefined", () => {
render(<SponsorOrderGrid total={0} />);
test("renders no-items message when forms is undefined", () => {
render(<SponsorOrderGrid order={{ total: 0 }} />);
expect(screen.getByText("mui_table.no_items")).toBeInTheDocument();
});

test("filters out items with zero quantity", () => {
const lines = [makeForm({ items: [makeItem({ quantity: 0 })] })];
render(<SponsorOrderGrid lines={lines} total={0} />);
const order = { forms: [makeForm({ items: [makeItem({ quantity: 0 })] })], total: 0 };
render(<SponsorOrderGrid order={order} />);
expect(screen.queryByText("$100.00")).not.toBeInTheDocument();
});

test("does not render action column when callbacks are absent", () => {
render(<SponsorOrderGrid {...defaultProps} />);
expect(
screen.queryByText("sponsor_order_grid.action")
).not.toBeInTheDocument();
expect(screen.queryByText("sponsor_order_grid.action")).not.toBeInTheDocument();
});

test("renders action column header when both callbacks are provided", () => {
Expand All @@ -122,21 +127,17 @@ describe("SponsorOrderGrid", () => {
onUndoCancelForm={jest.fn()}
/>
);
const deleteButton = screen.getByTestId
? document.querySelector('[data-testid="DeleteIcon"]')
: null;
const button = document.querySelector("button[aria-label]") || document.querySelector("tbody button");
const button = document.querySelector("tbody button");
fireEvent.click(button);
expect(onCancelForm).toHaveBeenCalledTimes(1);
});

test("renders undo button for cancelled item and calls onUndoCancelForm on click", () => {
const onUndoCancelForm = jest.fn();
const lines = [makeForm({ items: [makeItem({ canceled_by_id: 99 })] })];
const order = { forms: [makeForm({ items: [makeItem({ canceled_by_id: 99 })] })], total: 0 };
render(
<SponsorOrderGrid
lines={lines}
total={0}
order={order}
onCancelForm={jest.fn()}
onUndoCancelForm={onUndoCancelForm}
/>
Expand All @@ -146,26 +147,26 @@ describe("SponsorOrderGrid", () => {
expect(onUndoCancelForm).toHaveBeenCalledTimes(1);
});

test("uses amountDue label when amountDue prop is provided", () => {
render(<SponsorOrderGrid lines={[]} amountDue={5000} />);
expect(
screen.getByText("sponsor_order_grid.amount_due")
).toBeInTheDocument();
test("renders amount_due label in total row", () => {
render(<SponsorOrderGrid order={{ forms: [], total: 5000 }} />);
expect(screen.getByText("sponsor_order_grid.amount_due")).toBeInTheDocument();
});

test("renders reconciliation section when withReconciliation is true", () => {
const order = {
forms: [],
total: 10000,
retained: 2000,
credited_to_payment_method: 0,
cancelled_total: 5000,
refunds_total: 3000
};
render(<SponsorOrderGrid order={order} withReconciliation />);
expect(screen.getByText("sponsor_order_grid.reconciliation")).toBeInTheDocument();
});

test("renders meta_field values in item details", () => {
const item = makeItem({
meta_fields: [
{
id: 1,
name: "Booth Size",
class_field: "Form",
current_value: "Large",
values: []
}
]
});
render(<SponsorOrderGrid lines={[makeForm({ items: [item] })]} total={0} withDescription />);
expect(screen.getByText(/Booth Size/)).toBeInTheDocument();
test("does not render reconciliation section by default", () => {
render(<SponsorOrderGrid order={{ forms: [], total: 0 }} />);
expect(screen.queryByText("sponsor_order_grid.reconciliation")).not.toBeInTheDocument();
});
});
31 changes: 31 additions & 0 deletions src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx
Original file line number Diff line number Diff line change
@@ -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 Typography from "@mui/material/Typography";
import {currencyAmountFromCents} from "../../../../utils/money";

const BalanceValue = ({value}) => {
const isNegative = value < 0;
const sign = isNegative ? "-" : "";
const color = isNegative ? "primary.dark" : "text.disabled";
const balance = `${sign}${currencyAmountFromCents(Math.abs(value))}`;

return (
<Typography variant="body1" sx={{ color }}>
{balance}
</Typography>
);
}

export default BalanceValue;
45 changes: 45 additions & 0 deletions src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx
Original file line number Diff line number Diff line change
@@ -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 T from "i18n-react/dist/i18n-react";
import Typography from "@mui/material/Typography";
import DoNotDisturbIcon from "@mui/icons-material/DoNotDisturb";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";

const CancelledItems = ({cancelledItems, sx = {}}) => {

if (cancelledItems.length === 0) return null;

return (
<Box sx={{display: "flex", flexDirection: "row", ...sx}}>
<DoNotDisturbIcon sx={{mr: 1}}/>
<Typography variant="body2" sx={{mb: 2, mr: 1}}>
{T.translate("sponsor_order_grid.cancelled_items", {count: cancelledItems.length})}
</Typography>
{cancelledItems.map((item) => (
<Link
key={`cancelled-item-${item.id}`}
variant="body1"
href={`#item-${item.id}`}
sx={{mr: 1, color: "text.disabled", textDecorationColor: "rgba(0, 0, 0, 0.38)"}}
>
{item.formCode} - {item.itemCode}
</Link>
))}
Comment thread
santipalenque marked this conversation as resolved.
</Box>
);
}

export default CancelledItems;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* 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/Typography";
import T from "i18n-react/dist/i18n-react";
import Divider from "@mui/material/Divider";
import Box from "@mui/material/Box";
import {currencyAmountFromCents} from "../../../../utils/money";

const ReconciliationBox = ({cancelledTotal, refundsTotal, retained, credited}) => {
const totalColor = retained > 0 ? "error.dark" : "success.dark";
const totalLabel = retained > 0 ? "retained" : (credited > 0 ? "credited" : "balance");
const totalValue = retained > 0 ? retained : (credited > 0 ? credited : retained);

return (
<Box sx={{maxWidth: 400, mt: 2, mb: 3}}>
<Typography variant="body2" sx={{mb: 3}}>
{T.translate("sponsor_order_grid.reconciliation")}
</Typography>
<Box sx={{display: "flex", justifyContent: "space-between"}}>
<Typography variant="body1" sx={{color: "text.secondary"}}>
{T.translate("sponsor_order_grid.cancelled")}
</Typography>
<Typography variant="body1">
{currencyAmountFromCents(cancelledTotal ?? 0)}
</Typography>
</Box>
<Box sx={{display: "flex", justifyContent: "space-between"}}>
<Typography variant="body1" sx={{color: "text.secondary"}}>
{T.translate("sponsor_order_grid.refunded")}
</Typography>
<Typography variant="body1">
{currencyAmountFromCents(refundsTotal ?? 0)}
</Typography>
</Box>
<Divider sx={{my: 1}}/>
<Box sx={{display: "flex", justifyContent: "space-between"}}>
<Typography variant="body2">
{T.translate(`sponsor_order_grid.${totalLabel}`)}
</Typography>
<Typography variant="body2" sx={{color: totalColor}}>
{currencyAmountFromCents(totalValue ?? 0)}
</Typography>
</Box>
Comment thread
santipalenque marked this conversation as resolved.

</Box>

);
}

export default ReconciliationBox;
51 changes: 51 additions & 0 deletions src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* 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 from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import T from "i18n-react/dist/i18n-react";
import {currencyAmountFromCents} from "../../../../utils/money";

const TotalFooter = ({total}) => {
const safetotal = total ?? 0;
const isNegative = safetotal < 0;
const sign = isNegative ? "-" : "";
const color = isNegative ? "primary.dark" : (safetotal === 0 ? "text.primary" : "error.main");
const totalStr = `${sign}${currencyAmountFromCents(Math.abs(safetotal))}`;

return (
<Box sx={{
bgcolor: "#F1F3F5",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
borderTop: "1px solid #EEE",
pt: 2,
mt: 2,
mx: -2,
px: "10%",
mb: -3,
pb: 2,
}}>
<Typography sx={{fontWeight: 800, textTransform: "uppercase"}}>
{T.translate("sponsor_order_grid.amount_due")}
</Typography>
<Typography sx={{color, fontWeight: 800, fontSize: "15px"}}>
{totalStr}
</Typography>
</Box>
);
}

export default TotalFooter;
Loading
Loading