From bf2a5ac12189242424402c899c5697cabc0bcc05 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Tue, 18 Aug 2026 16:21:49 -0300 Subject: [PATCH 1/7] v5.0.50-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 105c5050..a68305f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.49", + "version": "5.0.50-beta.0", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From 16b2f86dc9b72345352994bfce6737bfcdfbaad1 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Tue, 18 Aug 2026 16:35:28 -0300 Subject: [PATCH 2/7] chore: create custom dialog shell and reuse it in all dialogs --- src/components/index.js | 1 + src/components/mui/AlertModal/index.js | 22 ++------ src/components/mui/CustomDialog/index.js | 54 +++++++++++++++++++ src/components/mui/ItemSettingsModal/index.js | 23 ++------ src/components/mui/NotesModal/index.js | 26 +++------ src/components/mui/UploadDialog/index.js | 27 +++------- webpack.common.js | 1 + 7 files changed, 78 insertions(+), 76 deletions(-) create mode 100644 src/components/mui/CustomDialog/index.js diff --git a/src/components/index.js b/src/components/index.js index f1dfabb5..7c377713 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -112,6 +112,7 @@ 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 MuiCustomDialog} from './mui/CustomDialog' export {default as MuiInlineCard} from './mui/cards/InlineCard' export {default as MuiListCard} from './mui/cards/ListCard' export {default as MuiTableCard} from './mui/cards/TableCard' diff --git a/src/components/mui/AlertModal/index.js b/src/components/mui/AlertModal/index.js index 0fcb0e17..2b7dcc9a 100644 --- a/src/components/mui/AlertModal/index.js +++ b/src/components/mui/AlertModal/index.js @@ -14,26 +14,12 @@ 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"; +import { Divider, Button, DialogActions, DialogContent, DialogContentText } from "@mui/material"; +import CustomDialog from "../CustomDialog"; const AlertModal = ({ title, message, open, onClose }) => { return ( - - {title} - ({ - position: "absolute", - right: 8, - top: 8, - color: theme.palette.grey[500] - })} - > - - - + {message} @@ -43,7 +29,7 @@ const AlertModal = ({ title, message, open, onClose }) => { {T.translate("general.ok")} - + ); }; diff --git a/src/components/mui/CustomDialog/index.js b/src/components/mui/CustomDialog/index.js new file mode 100644 index 00000000..3002835e --- /dev/null +++ b/src/components/mui/CustomDialog/index.js @@ -0,0 +1,54 @@ +/** + * 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 { Dialog, DialogTitle, Divider, IconButton } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; + +const CustomDialog = ({ title, open, onClose, maxWidth, fullWidth, children }) => ( + + {title} + ({ + position: "absolute", + right: 12, + top: 12, + color: theme.palette.grey[500] + })} + > + + + + {children} + +); + +CustomDialog.propTypes = { + title: PropTypes.node.isRequired, + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + maxWidth: PropTypes.oneOf(["xs", "sm", "md", "lg", "xl", false]), + fullWidth: PropTypes.bool, + children: PropTypes.node.isRequired +}; + +CustomDialog.defaultProps = { + maxWidth: "sm", + fullWidth: true +}; + +export default CustomDialog; diff --git a/src/components/mui/ItemSettingsModal/index.js b/src/components/mui/ItemSettingsModal/index.js index 77ab82d5..0817f305 100644 --- a/src/components/mui/ItemSettingsModal/index.js +++ b/src/components/mui/ItemSettingsModal/index.js @@ -16,14 +16,11 @@ import PropTypes from "prop-types"; import T from "i18n-react/dist/i18n-react"; import Box from "@mui/material/Box"; 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 DialogTitle from "@mui/material/DialogTitle"; import Divider from "@mui/material/Divider"; -import IconButton from "@mui/material/IconButton"; import Typography from "@mui/material/Typography"; -import CloseIcon from "@mui/icons-material/Close"; +import CustomDialog from "../CustomDialog"; import ItemTableField from "../FormItemTable/components/ItemTableField"; const ItemSettingsModal = ({ item, timeZone, open, onClose }) => { @@ -35,21 +32,7 @@ const ItemSettingsModal = ({ item, timeZone, open, onClose }) => { }; return ( - - {T.translate("general.settings")} - ({ - position: "absolute", - right: 8, - top: 8, - color: theme.palette.grey[500] - })} - > - - - + { {T.translate("general.save")} - + ); }; diff --git a/src/components/mui/NotesModal/index.js b/src/components/mui/NotesModal/index.js index 64ddfbcb..990c3108 100644 --- a/src/components/mui/NotesModal/index.js +++ b/src/components/mui/NotesModal/index.js @@ -15,8 +15,8 @@ import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; import T from "i18n-react"; import { useField } from "formik"; -import { Button, Dialog, DialogActions, DialogContent, Divider, DialogContentText, DialogTitle, IconButton, TextField } from "@mui/material"; -import CloseIcon from "@mui/icons-material/Close"; +import { Button, DialogActions, DialogContent, DialogContentText, TextField } from "@mui/material"; +import CustomDialog from "../CustomDialog"; const NotesModal = ({ id, label, open, title, placeholder, onClose }) => { const name = `i-${id}-c-global-f-notes`; @@ -34,21 +34,11 @@ const NotesModal = ({ id, label, open, title, placeholder, onClose }) => { }; return ( - - {title || T.translate("general.notes")} - ({ - position: "absolute", - right: 8, - top: 8, - color: theme.palette.grey[500] - })} - > - - - + {label} { {T.translate("general.save")} - + ); }; diff --git a/src/components/mui/UploadDialog/index.js b/src/components/mui/UploadDialog/index.js index 65b5035f..95630ab8 100644 --- a/src/components/mui/UploadDialog/index.js +++ b/src/components/mui/UploadDialog/index.js @@ -14,18 +14,15 @@ 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 CustomDialog from "../CustomDialog"; import { DECIMAL_DIGITS } from "../../../utils/constants"; @@ -79,21 +76,11 @@ const UploadDialog = ({ : []; return ( - - {T.translate("upload_input.upload_file")} - ({ - position: "absolute", - right: 8, - top: 8, - color: theme.palette.grey[500] - })} - > - - - + {fileMeta.name} @@ -126,7 +113,7 @@ const UploadDialog = ({ {T.translate("upload_input.upload_file")} - + ); }; diff --git a/webpack.common.js b/webpack.common.js index 5719e74b..043bd518 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -145,6 +145,7 @@ module.exports = { 'components/mui/auth-button': './src/components/mui/AuthButton/index.js', 'components/mui/cart-button': './src/components/mui/CartButton/index.js', 'components/mui/confirm-delete-dialog': './src/components/mui/ConfirmDeleteDialog/index.js', + 'components/mui/custom-dialog': './src/components/mui/CustomDialog/index.js', 'components/mui/cards': './src/components/mui/cards/index.js', 'components/mui/cards/inline-card': './src/components/mui/cards/InlineCard/index.js', 'components/mui/cards/list-card': './src/components/mui/cards/ListCard/index.js', From f5cfcd67a036025c2670554dd7d5ff97856734ec Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 20 Aug 2026 15:40:10 -0300 Subject: [PATCH 3/7] chore: move actions to custom dialog --- src/components/mui/CustomDialog/index.js | 123 +++++++++++++++++++---- 1 file changed, 101 insertions(+), 22 deletions(-) diff --git a/src/components/mui/CustomDialog/index.js b/src/components/mui/CustomDialog/index.js index 3002835e..61fb543a 100644 --- a/src/components/mui/CustomDialog/index.js +++ b/src/components/mui/CustomDialog/index.js @@ -11,31 +11,106 @@ * limitations under the License. * */ -import React from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; -import { Dialog, DialogTitle, Divider, IconButton } from "@mui/material"; +import { + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + IconButton +} from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; -const CustomDialog = ({ title, open, onClose, maxWidth, fullWidth, children }) => ( - - {title} - ({ - position: "absolute", - right: 12, - top: 12, - color: theme.palette.grey[500] - })} +const CustomDialog = ({ + title, + open, + onClose, + maxWidth, + fullWidth, + primaryAction, + secondaryAction, + children +}) => { + const [isSubmitting, setIsSubmitting] = useState(false); + + const handlePrimaryClick = () => { + const result = primaryAction.onClick(); + if (result && typeof result.then === "function") { + setIsSubmitting(true); + result.finally(() => setIsSubmitting(false)); + } + }; + + const handleClose = () => { + if (isSubmitting) return; + onClose(); + }; + + return ( + - - - - {children} - -); + {title} + ({ + position: "absolute", + right: 12, + top: 12, + color: theme.palette.grey[500] + })} + > + + + + {children} + {(primaryAction || secondaryAction) && ( + + {secondaryAction && ( + + )} + {primaryAction && ( + + )} + + )} + + ); +}; + +const actionPropType = PropTypes.shape({ + label: PropTypes.node.isRequired, + onClick: PropTypes.func.isRequired, + disabled: PropTypes.bool +}); CustomDialog.propTypes = { title: PropTypes.node.isRequired, @@ -43,12 +118,16 @@ CustomDialog.propTypes = { onClose: PropTypes.func.isRequired, maxWidth: PropTypes.oneOf(["xs", "sm", "md", "lg", "xl", false]), fullWidth: PropTypes.bool, + primaryAction: actionPropType, + secondaryAction: actionPropType, children: PropTypes.node.isRequired }; CustomDialog.defaultProps = { maxWidth: "sm", - fullWidth: true + fullWidth: true, + primaryAction: null, + secondaryAction: null }; export default CustomDialog; From 7f1e25827073fc44d78d32500b924c44b9bd3f04 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 20 Aug 2026 15:41:11 -0300 Subject: [PATCH 4/7] v5.0.50-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a68305f2..45be0341 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.50-beta.0", + "version": "5.0.50-beta.1", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From af3ee7fe659c16e549250b63a40274494a64aa6a Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 20 Aug 2026 17:31:48 -0300 Subject: [PATCH 5/7] chore: update AlertModal, NotesModal, ItemSettingsModal, UploadDialog for CustomDialog's content/actions contract --- src/components/mui/AlertModal/index.js | 19 +++--- src/components/mui/ItemSettingsModal/index.js | 64 ++++++++--------- src/components/mui/NotesModal/index.js | 35 +++++----- src/components/mui/UploadDialog/index.js | 68 ++++++++----------- 4 files changed, 80 insertions(+), 106 deletions(-) diff --git a/src/components/mui/AlertModal/index.js b/src/components/mui/AlertModal/index.js index 2b7dcc9a..a22d99f3 100644 --- a/src/components/mui/AlertModal/index.js +++ b/src/components/mui/AlertModal/index.js @@ -14,21 +14,18 @@ import React from "react"; import PropTypes from "prop-types"; import T from "i18n-react"; -import { Divider, Button, DialogActions, DialogContent, DialogContentText } from "@mui/material"; +import { DialogContentText } from "@mui/material"; import CustomDialog from "../CustomDialog"; const AlertModal = ({ title, message, open, onClose }) => { return ( - - - {message} - - - - - + + {message} ); }; diff --git a/src/components/mui/ItemSettingsModal/index.js b/src/components/mui/ItemSettingsModal/index.js index 0817f305..917afb75 100644 --- a/src/components/mui/ItemSettingsModal/index.js +++ b/src/components/mui/ItemSettingsModal/index.js @@ -15,9 +15,6 @@ import React from "react"; import PropTypes from "prop-types"; import T from "i18n-react/dist/i18n-react"; import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; import Divider from "@mui/material/Divider"; import Typography from "@mui/material/Typography"; import CustomDialog from "../CustomDialog"; @@ -32,38 +29,35 @@ const ItemSettingsModal = ({ item, timeZone, open, onClose }) => { }; return ( - - - - {item?.name} - - - {itemFields.map((exc) => ( - - - - ))} - - - - + + + {item?.name} + + + {itemFields.map((exc) => ( + + + + ))} ); }; diff --git a/src/components/mui/NotesModal/index.js b/src/components/mui/NotesModal/index.js index 990c3108..5f347286 100644 --- a/src/components/mui/NotesModal/index.js +++ b/src/components/mui/NotesModal/index.js @@ -15,7 +15,7 @@ import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; import T from "i18n-react"; import { useField } from "formik"; -import { Button, DialogActions, DialogContent, DialogContentText, TextField } from "@mui/material"; +import { DialogContentText, TextField } from "@mui/material"; import CustomDialog from "../CustomDialog"; const NotesModal = ({ id, label, open, title, placeholder, onClose }) => { @@ -38,25 +38,22 @@ const NotesModal = ({ id, label, open, title, placeholder, onClose }) => { title={title || T.translate("general.notes")} open={open} onClose={onClose} + primaryAction={{ + label: T.translate("general.save"), + onClick: handleSave + }} > - - {label} - setNotes(ev.target.value)} - value={notes} - margin="normal" - multiline - fullWidth - rows={4} - placeholder={placeholder || T.translate("placeholders.notes")} - /> - - - - + {label} + setNotes(ev.target.value)} + value={notes} + margin="normal" + multiline + fullWidth + rows={4} + placeholder={placeholder || T.translate("placeholders.notes")} + /> ); }; diff --git a/src/components/mui/UploadDialog/index.js b/src/components/mui/UploadDialog/index.js index 95630ab8..4a233238 100644 --- a/src/components/mui/UploadDialog/index.js +++ b/src/components/mui/UploadDialog/index.js @@ -12,13 +12,7 @@ * */ import React, { useState } from "react"; -import { - Button, - DialogContent, - Divider, - Typography, - DialogActions -} from "@mui/material"; +import { Divider, Typography } from "@mui/material"; import PropTypes from "prop-types"; import UploadInputV3 from "../../inputs/upload-input-v3"; import T from "i18n-react"; @@ -54,11 +48,10 @@ const UploadDialog = ({ onClose(); }; - const handleUpload = () => { + const handleUpload = () => onUpload(uploadedFile).then(() => { handleClose(); }); - }; const handleRemove = () => { setUploadedFile(null); @@ -80,39 +73,32 @@ const UploadDialog = ({ title={T.translate("upload_input.upload_file")} open={open} onClose={handleClose} + primaryAction={{ + label: T.translate("upload_input.upload_file"), + onClick: handleUpload, + disabled: !uploadedFile + }} > - - - {fileMeta.name} - - - {fileMeta.description} - - - - - - - + + {fileMeta.name} + + + {fileMeta.description} + + + ); }; From f0a113d766b93c314572df4c415620b66fee2dec Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 20 Aug 2026 18:08:03 -0300 Subject: [PATCH 6/7] chore: pr review --- src/components/mui/CustomDialog/index.js | 17 +- .../mui/__tests__/custom-dialog.test.js | 154 ++++++++++++++++++ 2 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 src/components/mui/__tests__/custom-dialog.test.js diff --git a/src/components/mui/CustomDialog/index.js b/src/components/mui/CustomDialog/index.js index 61fb543a..575f783b 100644 --- a/src/components/mui/CustomDialog/index.js +++ b/src/components/mui/CustomDialog/index.js @@ -11,7 +11,7 @@ * limitations under the License. * */ -import React, { useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import PropTypes from "prop-types"; import { Button, @@ -36,12 +36,25 @@ const CustomDialog = ({ children }) => { const [isSubmitting, setIsSubmitting] = useState(false); + // the only purpose of this is to avoid having a console log when modal is closed and setIsSubmitting is called + const mountedRef = useRef(true); + + useEffect( + () => () => { + mountedRef.current = false; + }, + [] + ); const handlePrimaryClick = () => { const result = primaryAction.onClick(); if (result && typeof result.then === "function") { setIsSubmitting(true); - result.finally(() => setIsSubmitting(false)); + Promise.resolve(result) + .catch(() => {}) + .finally(() => { + if (mountedRef.current) setIsSubmitting(false); + }); } }; diff --git a/src/components/mui/__tests__/custom-dialog.test.js b/src/components/mui/__tests__/custom-dialog.test.js new file mode 100644 index 00000000..2d083636 --- /dev/null +++ b/src/components/mui/__tests__/custom-dialog.test.js @@ -0,0 +1,154 @@ +/** + * 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. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +import React from "react"; +import { render, screen, fireEvent, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import CustomDialog from "../CustomDialog/index"; + +const deferred = () => { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +}; + +const defaultProps = { + title: "Dialog Title", + open: true, + onClose: jest.fn() +}; + +const getPrimaryButton = () => + screen.getByRole("button", { name: "Primary" }); + +const getCloseButton = () => screen.getByRole("button", { name: /close/i }); + +beforeEach(() => jest.clearAllMocks()); + +describe("CustomDialog", () => { + test("a pending onClick promise disables the primary button and the close icon, and ignores backdrop/Escape", async () => { + const { promise, resolve } = deferred(); + const onClick = jest.fn(() => promise); + + render( + + content + + ); + + await userEvent.click(getPrimaryButton()); + + expect(getPrimaryButton()).toBeDisabled(); + expect(getCloseButton()).toBeDisabled(); + + fireEvent.keyDown(screen.getByRole("dialog"), { + key: "Escape", + code: "Escape" + }); + expect(defaultProps.onClose).not.toHaveBeenCalled(); + + fireEvent.click(document.querySelector(".MuiBackdrop-root")); + expect(defaultProps.onClose).not.toHaveBeenCalled(); + + await act(async () => { + resolve(); + await promise; + }); + }); + + test("a resolved onClick re-enables the primary button and the close icon", async () => { + const { promise, resolve } = deferred(); + const onClick = jest.fn(() => promise); + + render( + + content + + ); + + await userEvent.click(getPrimaryButton()); + expect(getPrimaryButton()).toBeDisabled(); + + await act(async () => { + resolve(); + await promise; + }); + + expect(getPrimaryButton()).toBeEnabled(); + expect(getCloseButton()).toBeEnabled(); + }); + + test("a rejected onClick re-enables the primary button and the close icon", async () => { + const { promise, reject } = deferred(); + const onClick = jest.fn(() => promise); + + render( + + content + + ); + + await userEvent.click(getPrimaryButton()); + expect(getPrimaryButton()).toBeDisabled(); + + await act(async () => { + reject(new Error("save failed")); + await promise.catch(() => {}); + }); + + expect(getPrimaryButton()).toBeEnabled(); + expect(getCloseButton()).toBeEnabled(); + }); + + test("a sync onClick never enters the locked state", async () => { + const onClick = jest.fn(); + + render( + + content + + ); + + await userEvent.click(getPrimaryButton()); + + expect(onClick).toHaveBeenCalledTimes(1); + expect(getPrimaryButton()).toBeEnabled(); + expect(getCloseButton()).toBeEnabled(); + }); + + test("renders DialogActions as a direct child of the dialog paper", () => { + render( + + content + + ); + + const paper = document.querySelector(".MuiDialog-paper"); + const actions = document.querySelector(".MuiDialogActions-root"); + expect(actions).toBeInTheDocument(); + expect(actions.parentElement).toBe(paper); + }); +}); From 44db4e6f33dc99a4f158ee6b74a3d371f99552e6 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 21 Aug 2026 14:44:44 -0300 Subject: [PATCH 7/7] chore: pr review --- src/components/mui/CustomDialog/index.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/mui/CustomDialog/index.js b/src/components/mui/CustomDialog/index.js index 575f783b..32b235a5 100644 --- a/src/components/mui/CustomDialog/index.js +++ b/src/components/mui/CustomDialog/index.js @@ -39,15 +39,19 @@ const CustomDialog = ({ // the only purpose of this is to avoid having a console log when modal is closed and setIsSubmitting is called const mountedRef = useRef(true); - useEffect( - () => () => { + useEffect(() => { + mountedRef.current = true; + return () => { mountedRef.current = false; - }, - [] - ); + }; + }, []); + + const runAction = (action) => { + if (isSubmitting) return; - const handlePrimaryClick = () => { - const result = primaryAction.onClick(); + const result = action.onClick(); + + // if action is async then guard double click if (result && typeof result.then === "function") { setIsSubmitting(true); Promise.resolve(result) @@ -58,6 +62,9 @@ const CustomDialog = ({ } }; + const handlePrimaryClick = () => runAction(primaryAction); + const handleSecondaryClick = () => runAction(secondaryAction); + const handleClose = () => { if (isSubmitting) return; onClose(); @@ -93,7 +100,7 @@ const CustomDialog = ({ {secondaryAction && (