From 657a52386cb7ce8da2d9774026ed851463292d56 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Wed, 8 Apr 2026 18:03:39 -0300 Subject: [PATCH 01/16] feat: adding components from sponsor services --- package.json | 5 + src/components/index.js | 14 + src/components/mui/AlertButton/index.js | 31 + src/components/mui/AlertModal/index.js | 57 + src/components/mui/AuthButton/index.js | 66 + .../mui/AuthButton/styles.module.scss | 28 + src/components/mui/CartButton/index.js | 53 + .../mui/ConfirmDeleteDialog/index.js | 58 + src/components/mui/CustomAlert/index.js | 34 + src/components/mui/DashboardCard/index.js | 104 + src/components/mui/DownloadBtn/index.js | 35 + src/components/mui/LoadingOverlay/index.jsx | 27 + src/components/mui/NavBar/index.js | 44 + src/components/mui/NavBar/styles.module.scss | 12 + src/components/mui/NotesModal/index.js | 37 +- src/components/mui/OrderSummary/index.jsx | 78 + src/components/mui/StatusChip/index.js | 35 + .../__tests__/stripe-form.test.jsx | 162 + src/components/mui/StripePayment/index.jsx | 56 + .../mui/StripePayment/stripe-form.jsx | 128 + src/components/mui/UploadBtn/index.js | 34 + src/components/mui/UploadDialog/index.js | 143 + .../mui/__tests__/mui-table.test.js | 47 +- .../mui/__tests__/notes-modal.test.js | 9 +- src/components/mui/table/mui-table.js | 133 +- src/i18n/en.json | 189 +- src/utils/constants.js | 21 + src/utils/methods.js | 8 + webpack.common.js | 14 + yarn.lock | 4435 +++++++++-------- 30 files changed, 3848 insertions(+), 2249 deletions(-) create mode 100644 src/components/mui/AlertButton/index.js create mode 100644 src/components/mui/AlertModal/index.js create mode 100644 src/components/mui/AuthButton/index.js create mode 100644 src/components/mui/AuthButton/styles.module.scss create mode 100644 src/components/mui/CartButton/index.js create mode 100644 src/components/mui/ConfirmDeleteDialog/index.js create mode 100644 src/components/mui/CustomAlert/index.js create mode 100644 src/components/mui/DashboardCard/index.js create mode 100644 src/components/mui/DownloadBtn/index.js create mode 100644 src/components/mui/LoadingOverlay/index.jsx create mode 100644 src/components/mui/NavBar/index.js create mode 100644 src/components/mui/NavBar/styles.module.scss create mode 100644 src/components/mui/OrderSummary/index.jsx create mode 100644 src/components/mui/StatusChip/index.js create mode 100644 src/components/mui/StripePayment/__tests__/stripe-form.test.jsx create mode 100644 src/components/mui/StripePayment/index.jsx create mode 100644 src/components/mui/StripePayment/stripe-form.jsx create mode 100644 src/components/mui/UploadBtn/index.js create mode 100644 src/components/mui/UploadDialog/index.js diff --git a/package.json b/package.json index 653c2a42..12290653 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,10 @@ "@mui/material": "^6.4.3", "@mui/x-date-pickers": "^7.26.0", "@react-pdf/renderer": "^3.1.11", + "@sentry/react": "^8.54.0", + "@sentry/webpack-plugin": "^3.1.2", + "@stripe/react-stripe-js": "^5.4.1", + "@stripe/stripe-js": "^8.5.3", "@testing-library/jest-dom": "5.17.0", "@testing-library/react": "12.1.5", "@testing-library/user-event": "14.5.2", @@ -68,6 +72,7 @@ "node-sass": "^7.0.1", "path": "^0.12.7", "postcss-loader": "^6.2.1", + "prop-types": "^15.8.1", "react": "^17.0.0", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^0.31.5", diff --git a/src/components/index.js b/src/components/index.js index ea38031e..9c1f6d92 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -105,6 +105,20 @@ 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' +export {default as MuiAlertButton} from './mui/AlertButton' +export {default as MuiAlertModal} from './mui/AlertModal' +export {default as MuiAuthButton} from './mui/AuthButton' +export {default as MuiCartButton} from './mui/CartButton' +export {default as MuiConfirmDeleteDialog} from './mui/ConfirmDeleteDialog' +export {default as MuiDashboardCard} from './mui/DashboardCard' +export {default as MuiDownloadBtn} from './mui/DownloadBtn' +export {default as MuiLoadingOverlay} from './mui/LoadingOverlay' +export {default as MuiNavBar} from './mui/NavBar' +export {default as MuiOrderSummary} from './mui/OrderSummary' +export {default as MuiStatusChip} from './mui/StatusChip' +export {default as MuiStripePayment} from './mui/StripePayment' +export {default as MuiUploadBtn} from './mui/UploadBtn' +export {default as MuiUploadDialog} from './mui/UploadDialog' // this 5 includes 3rd party deps // export {default as ExtraQuestionsForm } from './extra-questions/index.js'; diff --git a/src/components/mui/AlertButton/index.js b/src/components/mui/AlertButton/index.js new file mode 100644 index 00000000..7b544d12 --- /dev/null +++ b/src/components/mui/AlertButton/index.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 { Button } from "@mui/material"; +import NotificationsIcon from "@mui/icons-material/Notifications"; + +const AlertButton = ({ label, onClick }) => ( + + ); + +export default AlertButton; diff --git a/src/components/mui/AlertModal/index.js b/src/components/mui/AlertModal/index.js new file mode 100644 index 00000000..0fcb0e17 --- /dev/null +++ b/src/components/mui/AlertModal/index.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 T from "i18n-react"; +import { Divider, IconButton, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; + +const AlertModal = ({ title, message, open, onClose }) => { + return ( + + {title} + ({ + position: "absolute", + right: 8, + top: 8, + color: theme.palette.grey[500] + })} + > + + + + + {message} + + + + + + + ); +}; + +AlertModal.propTypes = { + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired +}; + +export default AlertModal; diff --git a/src/components/mui/AuthButton/index.js b/src/components/mui/AuthButton/index.js new file mode 100644 index 00000000..0b521841 --- /dev/null +++ b/src/components/mui/AuthButton/index.js @@ -0,0 +1,66 @@ +/** + * 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 T from "i18n-react"; +import {Button, Box} from '@mui/material'; +import * as styles from "./styles.module.scss" + +const AuthButton = ({ isLoggedUser, doLogin, initLogOut, picture }) => { + const [showLogOut, setShowLogOut] = useState(false); + + const toggleLogOut = () => { + setShowLogOut(!showLogOut); + }; + + if (isLoggedUser) { + return ( +
+
+ {showLogOut && ( + + )} +
+ ); + } + return ( + + + + ); + +}; + +export default AuthButton; diff --git a/src/components/mui/AuthButton/styles.module.scss b/src/components/mui/AuthButton/styles.module.scss new file mode 100644 index 00000000..d3074ae3 --- /dev/null +++ b/src/components/mui/AuthButton/styles.module.scss @@ -0,0 +1,28 @@ +.userMenu { + position: absolute; + top: 12px; + right: 34px; + height: 40px; + width: 140px; + cursor: pointer; +} + +.login { + width: 100%; + text-align: right; +} + +.logout { + top: 4px; + right: 4px; +} + +.profilePic { + height: 40px; + width: 40px; + border: 1px solid #afafaf; + border-radius: 20px; + overflow: hidden; + float: right; + background-size: cover; +} \ No newline at end of file diff --git a/src/components/mui/CartButton/index.js b/src/components/mui/CartButton/index.js new file mode 100644 index 00000000..fd033ee0 --- /dev/null +++ b/src/components/mui/CartButton/index.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 { Button, Box } from "@mui/material"; +import T from "i18n-react"; + +const CartButton = ({ itemCount, onClick, sx, disabled }) => { + return ( + + ); +}; + +export default CartButton; diff --git a/src/components/mui/ConfirmDeleteDialog/index.js b/src/components/mui/ConfirmDeleteDialog/index.js new file mode 100644 index 00000000..451ba1f0 --- /dev/null +++ b/src/components/mui/ConfirmDeleteDialog/index.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 from "react"; +import PropTypes from "prop-types"; +import {Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"; +import T from "i18n-react"; + +const ConfirmDeleteDialog = ({ open, onClose, onConfirm, message }) => { + return ( + + + {T.translate("alerts.confirm_delete_title")} + + + + {message || T.translate("alerts.confirm_delete")} + + + + + + + + ); +}; + +ConfirmDeleteDialog.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + message: PropTypes.string +}; + +ConfirmDeleteDialog.defaultProps = { + message: "" +}; + +export default ConfirmDeleteDialog; diff --git a/src/components/mui/CustomAlert/index.js b/src/components/mui/CustomAlert/index.js new file mode 100644 index 00000000..a9f70481 --- /dev/null +++ b/src/components/mui/CustomAlert/index.js @@ -0,0 +1,34 @@ +/** + * 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 }) => ( + +
+ +); + +export default CustomAlert; diff --git a/src/components/mui/DashboardCard/index.js b/src/components/mui/DashboardCard/index.js new file mode 100644 index 00000000..c551a149 --- /dev/null +++ b/src/components/mui/DashboardCard/index.js @@ -0,0 +1,104 @@ +/** + * 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 { + Card, + CardContent, + Divider, + List, + ListItem, + Typography +} from "@mui/material"; + +const Heading = ({ children }) => ( + + {children} + +); + +const Value = ({ children }) => ( + + {children} + +); + +const DashboardCard = ({ title, rows, columns }) => { + const renderList = () => + rows.map((row, i) => ( + // eslint-disable-next-line react/no-array-index-key + + + {row.label} + {row.value} + + {i < rows.length - 1 && } + + )); + + const renderTable = () => { + const header = ( + + + {columns.map((col, i) => ( + // eslint-disable-next-line react/no-array-index-key + {col.label} + ))} + + + + ); + + const rest = rows.map((row, i) => ( + // eslint-disable-next-line react/no-array-index-key + + + {columns.map((col, j) => ( + // eslint-disable-next-line react/no-array-index-key + {row[col.key]} + ))} + + {i < rows.length - 1 && } + + )); + + return [header, ...rest]; + }; + + return ( + + + + {title} + + + {!columns && renderList()} + {columns && renderTable()} + + + + ); +}; + +export default DashboardCard; diff --git a/src/components/mui/DownloadBtn/index.js b/src/components/mui/DownloadBtn/index.js new file mode 100644 index 00000000..fd70762a --- /dev/null +++ b/src/components/mui/DownloadBtn/index.js @@ -0,0 +1,35 @@ +/** + * 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 ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import { Button } from "@mui/material"; + +const DownloadBtn = ({ url }) => { + return ( + + ); +}; + +export default DownloadBtn; diff --git a/src/components/mui/LoadingOverlay/index.jsx b/src/components/mui/LoadingOverlay/index.jsx new file mode 100644 index 00000000..f60eb3b3 --- /dev/null +++ b/src/components/mui/LoadingOverlay/index.jsx @@ -0,0 +1,27 @@ +/** + * 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 Backdrop from "@mui/material/Backdrop"; +import CircularProgress from "@mui/material/CircularProgress"; + +const LoadingOverlay = ({ loading }) => ( + ({ color: "#fff", zIndex: theme.zIndex.drawer + 1 })} + open={loading} + > + + + ); + +export default LoadingOverlay; diff --git a/src/components/mui/NavBar/index.js b/src/components/mui/NavBar/index.js new file mode 100644 index 00000000..fd781772 --- /dev/null +++ b/src/components/mui/NavBar/index.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 {AppBar, Box, Toolbar, Typography} from "@mui/material/AppBar"; +import AuthButton from "../AuthButton"; +import * as styles from "./styles.module.scss"; + +const NavBar = ({title, profilePic, isLoggedUser, onClickLogin, initLogOut}) => { + return ( + + + + {title} + + + + + + + ); +}; + +export default NavBar; diff --git a/src/components/mui/NavBar/styles.module.scss b/src/components/mui/NavBar/styles.module.scss new file mode 100644 index 00000000..9a440b76 --- /dev/null +++ b/src/components/mui/NavBar/styles.module.scss @@ -0,0 +1,12 @@ +.wrapper { + height: 64px; + border-bottom: 1px solid #b3b3b3; + background-color: white; + + .title { + font-size: 16px; + text-align: center; + height: 100%; + line-height: 60px; + } +} \ No newline at end of file diff --git a/src/components/mui/NotesModal/index.js b/src/components/mui/NotesModal/index.js index 150cf6dd..f11bccd3 100644 --- a/src/components/mui/NotesModal/index.js +++ b/src/components/mui/NotesModal/index.js @@ -11,28 +11,18 @@ * limitations under the License. * */ -import React, { useEffect, useState } from "react"; -import T from "i18n-react/dist/i18n-react"; +import React, { useState } from "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 T from "i18n-react"; import { useField } from "formik"; -import { Divider, IconButton, TextField } from "@mui/material"; +import { Button, Dialog, DialogActions, DialogContent, Divider, DialogContentText, DialogTitle, 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`; +const NotesModal = ({ id, label, open, title, placeholder, onClose }) => { + const name = `i-${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 [notes, setNotes] = useState(field?.value || ""); const handleSave = () => { helpers.setValue(notes); @@ -41,9 +31,7 @@ const NotesModal = ({ item, open, onClose }) => { return ( - - {T.translate("sponsor_edit_form.notes")} - + {title || T.translate("general.notes")} { - {item?.name} + {label} setNotes(ev.target.value)} @@ -67,9 +55,7 @@ const NotesModal = ({ item, open, onClose }) => { multiline fullWidth rows={4} - placeholder={T.translate( - "sponsor_edit_form.notes_placeholder" - )} + placeholder={placeholder || T.translate("placeholders.notes")} /> @@ -82,7 +68,10 @@ const NotesModal = ({ item, open, onClose }) => { }; NotesModal.propTypes = { - item: PropTypes.object, + id: PropTypes.any, + label: PropTypes.string, + title: PropTypes.string, + placeholder: PropTypes.string, open: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired }; diff --git a/src/components/mui/OrderSummary/index.jsx b/src/components/mui/OrderSummary/index.jsx new file mode 100644 index 00000000..cce9531b --- /dev/null +++ b/src/components/mui/OrderSummary/index.jsx @@ -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 { Box, CardContent, Skeleton, Typography } from "@mui/material"; +import T from "i18n-react"; +import { currencyAmountFromCents } from "../../../utils/money"; +import PropTypes from "prop-types"; + +const OrderSummary = ({ amount, dueDate, toName, fromName }) => { + const getPrice = () => { + if (amount) return currencyAmountFromCents(amount); + return ; + }; + + return ( + + + + + {getPrice()} + + {dueDate && ( + + {T.translate("general.due")} {dueDate} + + )} + + + + + + + {T.translate("general.to")} + + {toName} + + + + {T.translate("general.from")} + + {fromName} + + + + ); +}; + +OrderSummary.propTypes = { + amount: PropTypes.number.isRequired, + dueDate: PropTypes.string.isRequired, + toName: PropTypes.string.isRequired, + fromName: PropTypes.string.isRequired +}; + +export default OrderSummary; diff --git a/src/components/mui/StatusChip/index.js b/src/components/mui/StatusChip/index.js new file mode 100644 index 00000000..eee297ec --- /dev/null +++ b/src/components/mui/StatusChip/index.js @@ -0,0 +1,35 @@ +/** + * 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 PropTypes from "prop-types"; +import { FILE_UPLOAD_STATUS_COLOR } from "../../../utils/constants"; + +const StatusChip = ({ status }) => { + const color = FILE_UPLOAD_STATUS_COLOR[status] || "default"; + return ( + + ); +}; + +StatusChip.propTypes = { + status: PropTypes.string.isRequired +}; + +export default StatusChip; diff --git a/src/components/mui/StripePayment/__tests__/stripe-form.test.jsx b/src/components/mui/StripePayment/__tests__/stripe-form.test.jsx new file mode 100644 index 00000000..87b34283 --- /dev/null +++ b/src/components/mui/StripePayment/__tests__/stripe-form.test.jsx @@ -0,0 +1,162 @@ +import React from "react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import StripeForm from "../stripe-form"; +import { handleSentryException } from "../../../../utils/methods"; + +jest.mock("i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +jest.mock("../../../../utils/methods", () => ({ + handleSentryException: jest.fn() +})); + +jest.mock("../../../../utils/money", () => ({ + currencyAmountFromCents: (amount) => `$${amount}` +})); + +const mockConfirmPayment = jest.fn(); +const mockOn = jest.fn(); +const mockOff = jest.fn(); + +jest.mock("@stripe/react-stripe-js", () => ({ + PaymentElement: () =>
, + useStripe: () => ({ confirmPayment: mockConfirmPayment }), + useElements: () => ({ + getElement: () => ({ on: mockOn, off: mockOff }) + }) +})); + +const defaultProps = { + amount: 5000, + client: { + first_name: "Jane", + last_name: "Doe", + email: "jane@example.com", + address: {} + }, + redirectUrl: "/dashboard", + onSuccess: jest.fn(), + onError: jest.fn(), + onPaymentMethodChange: jest.fn() +}; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe("StripeForm payment button state", () => { + it("stays disabled after successful payment", async () => { + const onSuccess = jest.fn().mockResolvedValue(undefined); + mockConfirmPayment.mockResolvedValue({ + paymentIntent: { id: "pi_123", status: "succeeded" } + }); + + render(); + + const button = screen.getByRole("button"); + fireEvent.click(button); + + await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1)); + + expect(button).toBeDisabled(); + }); + + it("re-enables on payment error so the user can retry", async () => { + mockConfirmPayment.mockResolvedValue({ + error: { message: "Your card was declined." } + }); + + render(); + + const button = screen.getByRole("button"); + fireEvent.click(button); + + await waitFor(() => + expect(defaultProps.onError).toHaveBeenCalledWith( + "Your card was declined." + ) + ); + + expect(button).not.toBeDisabled(); + }); + + it("keeps disabled, reports to Sentry, and calls onError if onSuccess throws", async () => { + const backendError = new Error("Backend error"); + const onSuccess = jest.fn().mockRejectedValue(backendError); + mockConfirmPayment.mockResolvedValue({ + paymentIntent: { id: "pi_456", status: "succeeded" } + }); + + render(); + + const button = screen.getByRole("button"); + fireEvent.click(button); + + await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1)); + + expect(handleSentryException).toHaveBeenCalledWith(backendError); + expect(defaultProps.onError).toHaveBeenCalledWith( + "stripe_form.payment_confirmation_error" + ); + expect(button).toBeDisabled(); + }); + + it("re-enables button for retryable status 'requires_action'", async () => { + mockConfirmPayment.mockResolvedValue({ + paymentIntent: { id: "pi_999", status: "requires_action" } + }); + + render(); + + const button = screen.getByRole("button"); + fireEvent.click(button); + + await waitFor(() => expect(button).not.toBeDisabled()); + expect(defaultProps.onSuccess).not.toHaveBeenCalled(); + expect(defaultProps.onError).not.toHaveBeenCalled(); + }); + + it("keeps button disabled for async 'processing' status", async () => { + mockConfirmPayment.mockResolvedValue({ + paymentIntent: { id: "pi_999", status: "processing" } + }); + + render(); + + const button = screen.getByRole("button"); + fireEvent.click(button); + + await waitFor(() => expect(mockConfirmPayment).toHaveBeenCalled()); + expect(button).toBeDisabled(); + expect(defaultProps.onSuccess).not.toHaveBeenCalled(); + expect(defaultProps.onError).not.toHaveBeenCalled(); + }); + + it("blocks a second submit while confirmPayment is still in flight", async () => { + const onSuccess = jest.fn().mockResolvedValue(undefined); + let resolveConfirmPayment; + mockConfirmPayment.mockImplementation( + () => + new Promise((resolve) => { + resolveConfirmPayment = resolve; + }) + ); + + render(); + + const button = screen.getByRole("button"); + const form = button.closest("form"); + fireEvent.submit(form); + fireEvent.submit(form); + + expect(mockConfirmPayment).toHaveBeenCalledTimes(1); + + resolveConfirmPayment({ + paymentIntent: { id: "pi_789", status: "succeeded" } + }); + await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1)); + }); +}); diff --git a/src/components/mui/StripePayment/index.jsx b/src/components/mui/StripePayment/index.jsx new file mode 100644 index 00000000..96cd550d --- /dev/null +++ b/src/components/mui/StripePayment/index.jsx @@ -0,0 +1,56 @@ +/** + * 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, { useMemo } from "react"; +import { loadStripe } from "@stripe/stripe-js"; +import { Elements } from "@stripe/react-stripe-js"; +import StripeForm from "./stripe-form"; + +const StripePayment = ({ + paymentIntent, + paymentProfile, + client, + redirectUrl, + onPaymentSuccess, + onPaymentError, + updatePaymentIntent +}) => { + const { test_mode_enabled, test_publishable_key, live_publishable_key } = paymentProfile || {}; + const providerKey = test_mode_enabled ? test_publishable_key : live_publishable_key; + const stripePromise = useMemo( + () => (providerKey ? loadStripe(providerKey) : null), + [providerKey] + ); + + if (!paymentProfile || !paymentIntent) return null; + + const options = { + clientSecret: paymentIntent.client_secret, + appearance: { theme: "stripe" } + }; + + return ( + + + + ); +}; + +export default StripePayment; diff --git a/src/components/mui/StripePayment/stripe-form.jsx b/src/components/mui/StripePayment/stripe-form.jsx new file mode 100644 index 00000000..de74d236 --- /dev/null +++ b/src/components/mui/StripePayment/stripe-form.jsx @@ -0,0 +1,128 @@ +/** + * 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, Button, Typography} from "@mui/material"; +import T from "i18n-react"; +import {PaymentElement, useElements, useStripe} from "@stripe/react-stripe-js"; +import {currencyAmountFromCents} from "../../../utils/money"; +import {handleSentryException} from "../../../utils/methods"; + +const buildAddress = (userAddress) => { + const address = {}; + // stripe payment payload requires data that's not an empty string + if (userAddress.locality) address.city = userAddress.locality; + if (userAddress.country) address.country = userAddress.country; + if (userAddress.address1) address.line1 = userAddress.address1; + if (userAddress.address2) address.line2 = userAddress.address2; + if (userAddress.postal_code) address.postal_code = userAddress.postal_code; + if (userAddress.region) address.state = userAddress.region; + + if (Object.keys(address).length > 0) return {address}; + return {}; +}; + +const StripeForm = ({ + amount = 0, + client, + redirectUrl, + onSuccess, + onError, + onPaymentMethodChange + }) => { + const stripe = useStripe(); + const elements = useElements(); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (elements) { + const pe = elements.getElement("payment"); + const handlePaymentMethodChange = (event) => { + if (event.value.type) { + onPaymentMethodChange(event.value.type); + } + }; + + pe.on("change", handlePaymentMethodChange); + + return () => { + pe.off("change", handlePaymentMethodChange); + }; + } + }, [elements]); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!stripe || !elements || loading) return; + + setLoading(true); + + const {error, paymentIntent} = await stripe.confirmPayment({ + elements, + confirmParams: { + return_url: `${window.location.origin}${redirectUrl}`, + payment_method_data: { + billing_details: { + name: `${client.first_name} ${client.last_name}`, + email: client.email, + ...buildAddress(client.address) + } + } + }, + redirect: "if_required" + }); + + if (error) { + onError?.(error.message); + setLoading(false); + } else if (paymentIntent?.status === "succeeded") { + try { + await onSuccess?.(paymentIntent); + } catch (err) { + handleSentryException(err); + onError?.(T.translate("stripe_form.payment_confirmation_error")); + // Do not call setLoading(false) — payment already succeeded, keep form locked + } + // On success, keep loading=true so the button stays disabled until navigation + } else if (paymentIntent?.status === "processing") { + // Payment is async and still in flight — keep the form locked + } else { + // Genuinely retryable statuses (e.g. requires_action) — re-enable submission + setLoading(false); + } + }; + + return ( + + {T.translate("stripe_form.title")} + + + + + + ); +}; + +export default StripeForm; diff --git a/src/components/mui/UploadBtn/index.js b/src/components/mui/UploadBtn/index.js new file mode 100644 index 00000000..968a3a29 --- /dev/null +++ b/src/components/mui/UploadBtn/index.js @@ -0,0 +1,34 @@ +/** + * 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 ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; +import T from "i18n-react"; +import { Button } from "@mui/material"; + +const UploadBtn = ({ disabled, onClick }) => { + return ( + + ); +}; + +export default UploadBtn; diff --git a/src/components/mui/UploadDialog/index.js b/src/components/mui/UploadDialog/index.js new file mode 100644 index 00000000..65b5035f --- /dev/null +++ b/src/components/mui/UploadDialog/index.js @@ -0,0 +1,143 @@ +/** + * 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 { + Button, + Dialog, + DialogContent, + DialogTitle, + Divider, + IconButton, + Typography, + DialogActions +} from "@mui/material"; +import PropTypes from "prop-types"; +import UploadInputV3 from "../../inputs/upload-input-v3"; +import T from "i18n-react"; +import CloseIcon from "@mui/icons-material/Close"; +import { + DECIMAL_DIGITS +} from "../../../utils/constants"; + +const UploadDialog = ({ + name, + value, + open, + fileMeta, + maxFiles = 1, + onClose, + onUpload +}) => { + const [uploadedFile, setUploadedFile] = useState(null); + + const mediaType = { + id: name, + ...(fileMeta.max_file_size + ? { max_size: fileMeta.max_file_size.toFixed(DECIMAL_DIGITS) } + : {}), + max_uploads_qty: maxFiles, + type: { + allowed_extensions: fileMeta.allowed_extensions.split(",") + } + }; + + const handleClose = () => { + setUploadedFile(null); + onClose(); + }; + + const handleUpload = () => { + onUpload(uploadedFile).then(() => { + handleClose(); + }); + }; + + const handleRemove = () => { + setUploadedFile(null); + }; + + const canAddMore = () => (value?.length || 0) < maxFiles; + + const getInputValue = () => + value?.length > 0 + ? value.map((file) => ({ + ...file, + filename: + file.file_name ?? file.filename ?? file.file_path ?? file.file_url + })) + : []; + + return ( + + {T.translate("upload_input.upload_file")} + ({ + position: "absolute", + right: 8, + top: 8, + color: theme.palette.grey[500] + })} + > + + + + + + {fileMeta.name} + + + {fileMeta.description} + + + + + + + + + ); +}; + +UploadDialog.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.array.isRequired, + open: PropTypes.bool.isRequired, + fileMeta: PropTypes.object.isRequired, + maxFiles: PropTypes.number, + onClose: PropTypes.func.isRequired, + onUpload: PropTypes.func.isRequired +}; + +export default UploadDialog; diff --git a/src/components/mui/__tests__/mui-table.test.js b/src/components/mui/__tests__/mui-table.test.js index b8a0920a..02a03bc9 100644 --- a/src/components/mui/__tests__/mui-table.test.js +++ b/src/components/mui/__tests__/mui-table.test.js @@ -103,26 +103,21 @@ describe("MuiTable", () => { test("renders edit button when onEdit is provided", () => { setup({ onEdit: jest.fn() }); - const editBtns = screen.getAllByRole("button"); - expect(editBtns.length).toBeGreaterThan(0); + expect(screen.getAllByTestId("action-edit")).toHaveLength(2); }); 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 }) - ); + await userEvent.click(screen.getAllByTestId("action-edit")[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 userEvent.click(screen.getAllByTestId("action-delete")[0]); await new Promise((r) => setTimeout(r, 0)); expect(showConfirmDialog).toHaveBeenCalled(); expect(onDelete).toHaveBeenCalledWith(1); @@ -132,12 +127,42 @@ describe("MuiTable", () => { const onDelete = jest.fn(); showConfirmDialog.mockResolvedValueOnce(false); setup({ onDelete }); - const buttons = screen.getAllByRole("button"); - await userEvent.click(buttons[0]); + await userEvent.click(screen.getAllByTestId("action-delete")[0]); await new Promise((r) => setTimeout(r, 0)); expect(onDelete).not.toHaveBeenCalled(); }); + test("calls onSort when sortable column header is clicked", async () => { + const onSort = jest.fn(); + const sortableColumns = [ + { columnKey: "name", header: "Name", sortable: true }, + { columnKey: "role", header: "Role" } + ]; + setup({ columns: sortableColumns, onSort, options: { sortCol: "", sortDir: 1 } }); + await userEvent.click(screen.getByText("Name")); + expect(onSort).toHaveBeenCalledWith("name", -1); + }); + + test("calls onSelect when select button is clicked", async () => { + const onSelect = jest.fn(); + setup({ onSelect }); + await userEvent.click(screen.getAllByTestId("action-select")[0]); + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 1 })); + }); + + test("calls onArchive when archive button is clicked", async () => { + const onArchive = jest.fn(); + setup({ onArchive }); + await userEvent.click(screen.getAllByTestId("action-archive")[0]); + expect(onArchive).toHaveBeenCalledWith(expect.objectContaining({ id: 1 })); + }); + + test("hides delete button when canDelete returns false", () => { + const onDelete = jest.fn(); + setup({ onDelete, canDelete: (row) => row.id !== 1 }); + expect(screen.getAllByTestId("action-delete")).toHaveLength(1); + }); + test("renders pagination when perPage and currentPage are set", () => { setup(); expect(screen.getByTestId("pagination")).toBeInTheDocument(); diff --git a/src/components/mui/__tests__/notes-modal.test.js b/src/components/mui/__tests__/notes-modal.test.js index 1bdd88f6..f1ee0e97 100644 --- a/src/components/mui/__tests__/notes-modal.test.js +++ b/src/components/mui/__tests__/notes-modal.test.js @@ -33,7 +33,14 @@ const renderModal = (props) => onSubmit={jest.fn()} >
- + ); diff --git a/src/components/mui/table/mui-table.js b/src/components/mui/table/mui-table.js index ff3831a1..c8cac101 100644 --- a/src/components/mui/table/mui-table.js +++ b/src/components/mui/table/mui-table.js @@ -13,7 +13,7 @@ import * as React from "react"; import T from "i18n-react/dist/i18n-react"; -import { isBoolean } from "lodash"; +import {isBoolean} from "lodash"; import { Box, Button, @@ -32,14 +32,12 @@ 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 {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"; +import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; +import PropTypes from "prop-types"; const ARCHIVED_CELL_SX = { backgroundColor: "background.light", @@ -47,26 +45,30 @@ const ARCHIVED_CELL_SX = { }; 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 -}) => { + 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, + onSelect, + canDelete = () => true, + deleteDialogTitle = null, + deleteDialogBody = null, + deleteDialogConfirmText = null, + confirmButtonColor = null + }) => { + const totalColumnsCount = + columns.length + (onEdit ? 1 : 0) + (onDelete ? 1 : 0) + (onArchive ? 1 : 0) + (onSelect ? 1 : 0); + const handleChangePage = (_, newPage) => { onPageChange(newPage + 1); }; @@ -92,7 +94,7 @@ const MuiTable = ({ customPerPageOptions = [initialPerPage.current]; } - const { sortCol, sortDir } = options; + const {sortCol, sortDir} = options; const getArchivedCellSx = (row) => options.disableProp && row[options.disableProp] ? ARCHIVED_CELL_SX : null; @@ -109,7 +111,7 @@ const MuiTable = ({ typeof deleteDialogBody === "function" ? deleteDialogBody(getName(item)) : deleteDialogBody || - `${T.translate("general.row_remove_warning")} ${getName(item)}`, + `${T.translate("general.row_remove_warning")} ${getName(item)}`, type: "warning", showCancelButton: true, confirmButtonColor: confirmButtonColor || "#DD6B55", @@ -129,9 +131,9 @@ const MuiTable = ({ if (isBoolean(row[col.columnKey])) { return row[col.columnKey] ? ( - + ) : ( - + ); } @@ -139,15 +141,15 @@ const MuiTable = ({ }; return ( - - + + {/* TABLE HEADER */} - + {columns.map((col) => ( ))} - {onEdit && } - {onArchive && } - {onDelete && } + {onEdit && } + {onArchive && } + {onDelete && } + {onSelect && } @@ -211,10 +214,10 @@ const MuiTable = ({ - onEdit(row)}> - + onEdit(row)} data-testid="action-edit"> + )} @@ -222,7 +225,7 @@ const MuiTable = ({ {onArchive && ( diff --git a/src/i18n/i18n.js b/src/i18n/i18n.js index b240a410..81f0903c 100644 --- a/src/i18n/i18n.js +++ b/src/i18n/i18n.js @@ -1,5 +1,6 @@ import {getCurrentUserLanguage} from "../utils/methods"; import T from "i18n-react"; +import merge from "lodash/merge"; import en from './en.json'; import zh from './zh.json'; import es from './es.json'; @@ -25,4 +26,17 @@ try { T.setTexts(resources[language]); } catch (e) { T.setTexts(resources['en']); -} \ No newline at end of file +} + +/** + * Call this instead of T.setTexts() in consumer apps. + * Deep-merges the lib's base translations with your custom translations, + * so new keys added to the lib are always available even if your + * translation file doesn't include them yet. Consumer keys take precedence. + * + * @param {object} customTexts - your app's translation object + */ +export const setAppTexts = (customTexts = {}) => { + const libTexts = resources[language] || resources['en']; + T.setTexts(merge({}, libTexts, customTexts)); +}; \ No newline at end of file From 9d13f2549e9fcc9a5abafe0207ae3a78be2c2ca1 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 9 Apr 2026 18:23:11 -0300 Subject: [PATCH 09/16] fix: typo --- src/components/mui/DownloadBtn/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/mui/DownloadBtn/index.js b/src/components/mui/DownloadBtn/index.js index fd70762a..f762bfb9 100644 --- a/src/components/mui/DownloadBtn/index.js +++ b/src/components/mui/DownloadBtn/index.js @@ -25,7 +25,7 @@ const DownloadBtn = ({ url }) => { rel="noopener noreferrer" variant="contained" size="medium" - sx={{ width: 180, heigh: 36 }} + sx={{ width: 180, height: 36 }} > {T.translate("buttons.download")} From ca4240936ca5b4849f882802a43948275fc8101d Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 9 Apr 2026 18:28:05 -0300 Subject: [PATCH 10/16] v5.0.6-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e0229c00..cc08936c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.6", + "version": "5.0.6-beta.3", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From b9ab7640537d5118c754343623a621b3d47d9eab Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 9 Apr 2026 18:29:03 -0300 Subject: [PATCH 11/16] v5.0.7-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc08936c..93cba7c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.6-beta.3", + "version": "5.0.7-beta.1", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From 0461296482e440d36916355ef519179fed8b6658 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 10 Apr 2026 12:32:51 -0300 Subject: [PATCH 12/16] fix: tests --- .../mui/__tests__/show-confirm-dialog.test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/mui/__tests__/show-confirm-dialog.test.js b/src/components/mui/__tests__/show-confirm-dialog.test.js index d547912b..1e71daaa 100644 --- a/src/components/mui/__tests__/show-confirm-dialog.test.js +++ b/src/components/mui/__tests__/show-confirm-dialog.test.js @@ -16,6 +16,7 @@ jest.mock("react-dom", () => ({ unmountComponentAtNode: jest.fn() })); + jest.mock("../confirm-dialog", () => { const React = require("react"); return { __esModule: true, default: () =>
}; @@ -32,24 +33,27 @@ describe("showConfirmDialog", () => { expect(result).toBeInstanceOf(Promise); }); - test("calls ReactDOM.render to mount the dialog", () => { + test("calls ReactDOM.render to mount the dialog", async () => { showConfirmDialog({ title: "Test", text: "Body" }); + await Promise.resolve(); expect(ReactDOM.render).toHaveBeenCalledTimes(1); }); - test("appends a container div to the document body", () => { + test("appends a container div to the document body", async () => { const initialChildCount = document.body.children.length; showConfirmDialog({ title: "Test", text: "Body" }); + await Promise.resolve(); expect(document.body.children.length).toBeGreaterThan(initialChildCount); }); - test("passes title and text to ConfirmDialog", () => { + test("passes title and text to ConfirmDialog", async () => { showConfirmDialog({ title: "My Title", text: "My Text", confirmButtonText: "Yes", cancelButtonText: "No" }); + await Promise.resolve(); const [element] = ReactDOM.render.mock.calls[0]; expect(element.props.title).toBe("My Title"); expect(element.props.text).toBe("My Text"); From 203ebed5a967cbd91f4aa597dc2c8215d19be932 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 10 Apr 2026 13:41:21 -0300 Subject: [PATCH 13/16] fix: remove 3rd party libs from barrel components --- src/components/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/index.js b/src/components/index.js index a1c6079c..9bcf931b 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -64,7 +64,6 @@ 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' @@ -78,11 +77,8 @@ 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 {TotalRow as MuiTotalRow, NotesRow as MuiNotesRow} from './mui/table/extra-rows' -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' @@ -117,16 +113,20 @@ export {default as MuiLoadingOverlay} from './mui/LoadingOverlay' export {default as MuiNavBar} from './mui/NavBar' export {default as MuiOrderSummary} from './mui/OrderSummary' export {default as MuiStatusChip} from './mui/StatusChip' -export {default as MuiStripePayment} from './mui/StripePayment' export {default as MuiUploadBtn} from './mui/UploadBtn' export {default as MuiUploadDialog} from './mui/UploadDialog' -// this 5 includes 3rd party deps +// these include 3rd party deps // export {default as ExtraQuestionsForm } from './extra-questions/index.js'; // export {default as GMap} from './google-map'; // export {default as TextEditorV2} from './inputs/editor-input-v2' // export {default as TextEditorV3} from './inputs/editor-input-v3' // export {default as CompanyInputV2} from './inputs/company-input-v2.js' +// export {default as MuiDndList} from './mui/dnd-list' // react-beautiful-dnd +// export {default as MuiSortableTable} from './mui/sortable-table/mui-table-sortable' // react-beautiful-dnd +// export {default as MuiStripePayment} from './mui/StripePayment' // @stripe/react-stripe-js, @stripe/stripe-js +// export {default as MuiAdditionalInput} from './mui/formik-inputs/additional-input/additional-input' // react-beautiful-dnd (via dnd-list) +// export {default as MuiAdditionalInputList} from './mui/formik-inputs/additional-input/additional-input-list' // react-beautiful-dnd (via dnd-list) let language = getCurrentUserLanguage(); From 50f7a0d51dad7546dc836c683de83efbfb1f0998 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 10 Apr 2026 13:42:42 -0300 Subject: [PATCH 14/16] v5.0.7-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 93cba7c6..2ce5e2d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.7-beta.1", + "version": "5.0.7-beta.2", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From a948832bae3a4e8ba4971960afc2b702fa52b113 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 10 Apr 2026 13:54:41 -0300 Subject: [PATCH 15/16] fix: pr review --- .../__tests__/stripe-form.test.jsx | 4 ++-- src/components/mui/StripePayment/helpers.js | 20 +++++++++++++++++++ .../mui/StripePayment/stripe-form.jsx | 2 +- src/components/mui/table/mui-table.js | 2 +- src/utils/methods.js | 8 +------- 5 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 src/components/mui/StripePayment/helpers.js diff --git a/src/components/mui/StripePayment/__tests__/stripe-form.test.jsx b/src/components/mui/StripePayment/__tests__/stripe-form.test.jsx index 87b34283..d3419131 100644 --- a/src/components/mui/StripePayment/__tests__/stripe-form.test.jsx +++ b/src/components/mui/StripePayment/__tests__/stripe-form.test.jsx @@ -2,14 +2,14 @@ import React from "react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import StripeForm from "../stripe-form"; -import { handleSentryException } from "../../../../utils/methods"; +import { handleSentryException } from "../helpers"; jest.mock("i18n-react", () => ({ __esModule: true, default: { translate: (key) => key } })); -jest.mock("../../../../utils/methods", () => ({ +jest.mock("../helpers", () => ({ handleSentryException: jest.fn() })); diff --git a/src/components/mui/StripePayment/helpers.js b/src/components/mui/StripePayment/helpers.js new file mode 100644 index 00000000..a4d8983d --- /dev/null +++ b/src/components/mui/StripePayment/helpers.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 * as Sentry from "@sentry/react"; +import {isSentryInitialized} from "../../../utils/methods"; + +export const handleSentryException = (err) => + isSentryInitialized() + ? Sentry.captureException(err) + : console.log("Error on registration: ", err); \ No newline at end of file diff --git a/src/components/mui/StripePayment/stripe-form.jsx b/src/components/mui/StripePayment/stripe-form.jsx index b595ee0b..63bc06ed 100644 --- a/src/components/mui/StripePayment/stripe-form.jsx +++ b/src/components/mui/StripePayment/stripe-form.jsx @@ -16,7 +16,7 @@ import {Box, Button, Typography} from "@mui/material"; import T from "i18n-react"; import {PaymentElement, useElements, useStripe} from "@stripe/react-stripe-js"; import {currencyAmountFromCents} from "../../../utils/money"; -import {handleSentryException} from "../../../utils/methods"; +import {handleSentryException} from "./helpers"; const buildAddress = (userAddress = {}) => { const address = {}; diff --git a/src/components/mui/table/mui-table.js b/src/components/mui/table/mui-table.js index c8cac101..97d253e3 100644 --- a/src/components/mui/table/mui-table.js +++ b/src/components/mui/table/mui-table.js @@ -343,7 +343,7 @@ MuiTable.propTypes = { onSelect: PropTypes.func, canDelete: PropTypes.func, deleteDialogTitle: PropTypes.string, - deleteDialogBody: PropTypes.string, + deleteDialogBody: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), deleteDialogConfirmText: PropTypes.string, confirmButtonColor: PropTypes.string }; diff --git a/src/utils/methods.js b/src/utils/methods.js index ceacca6a..e318325e 100644 --- a/src/utils/methods.js +++ b/src/utils/methods.js @@ -11,7 +11,6 @@ * limitations under the License. **/ -import * as Sentry from "@sentry/react"; import moment from 'moment-timezone'; import URI from "urijs"; @@ -311,9 +310,4 @@ export const empty = (value) => { return false; }; -const isSentryInitialized = () => typeof window !== "undefined" && !!window.SENTRY_DSN; - -export const handleSentryException = (err) => - isSentryInitialized() - ? Sentry.captureException(err) - : console.log("Error on registration: ", err); +export const isSentryInitialized = () => typeof window !== "undefined" && !!window.SENTRY_DSN; From 746276b9afc0be422b533a1731397e71f1d97bfb Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 10 Apr 2026 13:55:49 -0300 Subject: [PATCH 16/16] v5.0.7-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ce5e2d1..839b3ea5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.7-beta.2", + "version": "5.0.7-beta.3", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": {