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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.49",
"version": "5.0.50-beta.1",
"description": "ui reactjs components for openstack marketing site",
"main": "lib/openstack-uicore-foundation.js",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
37 changes: 10 additions & 27 deletions src/components/mui/AlertModal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,19 @@
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 { DialogContentText } from "@mui/material";
import CustomDialog from "../CustomDialog";

const AlertModal = ({ title, message, open, onClose }) => {
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{title}</DialogTitle>
<IconButton
aria-label="close"
onClick={onClose}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
color: theme.palette.grey[500]
})}
>
<CloseIcon />
</IconButton>
<Divider />
<DialogContent>
<DialogContentText>{message}</DialogContentText>
</DialogContent>
<Divider />
<DialogActions>
<Button onClick={onClose} variant="contained" fullWidth>
{T.translate("general.ok")}
</Button>
</DialogActions>
</Dialog>
<CustomDialog
title={title}
open={open}
onClose={onClose}
primaryAction={{ label: T.translate("general.ok"), onClick: onClose }}
>
<DialogContentText>{message}</DialogContentText>
</CustomDialog>
);
};

Expand Down
153 changes: 153 additions & 0 deletions src/components/mui/CustomDialog/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* 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, useRef, useState } from "react";
import PropTypes from "prop-types";
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
IconButton
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";

const CustomDialog = ({
Comment thread
santipalenque marked this conversation as resolved.
title,
open,
onClose,
maxWidth,
fullWidth,
primaryAction,
secondaryAction,
children
}) => {
const [isSubmitting, setIsSubmitting] = useState(false);
Comment thread
santipalenque marked this conversation as resolved.
// 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 = true;
return () => {
mountedRef.current = false;
Comment thread
santipalenque marked this conversation as resolved.
};
}, []);

const runAction = (action) => {
if (isSubmitting) return;

const result = action.onClick();

// if action is async then guard double click
if (result && typeof result.then === "function") {
setIsSubmitting(true);
Promise.resolve(result)
.catch(() => {})
.finally(() => {
if (mountedRef.current) setIsSubmitting(false);
});
}
};

const handlePrimaryClick = () => runAction(primaryAction);
const handleSecondaryClick = () => runAction(secondaryAction);

const handleClose = () => {
if (isSubmitting) return;
onClose();
};

return (
<Dialog
open={open}
onClose={handleClose}
maxWidth={maxWidth}
fullWidth={fullWidth}
disableEscapeKeyDown={isSubmitting}
>
<DialogTitle>{title}</DialogTitle>
<IconButton
aria-label="close"
onClick={handleClose}
disabled={isSubmitting}
size="small"
sx={(theme) => ({
position: "absolute",
right: 12,
top: 12,
color: theme.palette.grey[500]
})}
>
<CloseIcon fontSize="large" />
</IconButton>
<Divider />
<DialogContent>{children}</DialogContent>
Comment thread
santipalenque marked this conversation as resolved.
{(primaryAction || secondaryAction) && (
<DialogActions>
{secondaryAction && (
Comment thread
santipalenque marked this conversation as resolved.
<Button
variant="outlined"
onClick={handleSecondaryClick}
disabled={isSubmitting || secondaryAction.disabled}
>
{secondaryAction.label}
</Button>
)}
{primaryAction && (
<Button
variant="contained"
onClick={handlePrimaryClick}
disabled={isSubmitting || primaryAction.disabled}
startIcon={
isSubmitting ? (
<CircularProgress size={16} color="inherit" />
) : null
}
>
{primaryAction.label}
</Button>
)}
</DialogActions>
)}
</Dialog>
);
};

const actionPropType = PropTypes.shape({
label: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired,
disabled: PropTypes.bool
});

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,
primaryAction: actionPropType,
secondaryAction: actionPropType,
children: PropTypes.node.isRequired
};

CustomDialog.defaultProps = {
maxWidth: "sm",
fullWidth: true,
primaryAction: null,
secondaryAction: null
};

export default CustomDialog;
85 changes: 31 additions & 54 deletions src/components/mui/ItemSettingsModal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,9 @@ 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 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 }) => {
Expand All @@ -35,53 +29,36 @@ const ItemSettingsModal = ({ item, timeZone, open, onClose }) => {
};

return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{T.translate("general.settings")}</DialogTitle>
<IconButton
aria-label="close"
onClick={onClose}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
color: theme.palette.grey[500]
})}
>
<CloseIcon />
</IconButton>
<Divider />
<DialogContent>
<Typography
variant="body2"
component="div"
sx={{ marginBottom: "20px" }}
>
{item?.name}
</Typography>
<Divider
sx={{
marginBottom: "20px",
marginLeft: "-24px",
marginRight: "-24px"
}}
/>
{itemFields.map((exc) => (
<Box key={`item-field-${exc.type_id}`} sx={{ mb: 2 }}>
<ItemTableField
field={exc}
rowId={item.form_item_id}
timeZone={timeZone}
label={exc.name}
/>
</Box>
))}
</DialogContent>
<DialogActions>
<Button onClick={handleSave} variant="contained" fullWidth>
{T.translate("general.save")}
</Button>
</DialogActions>
</Dialog>
<CustomDialog
title={T.translate("general.settings")}
open={open}
onClose={onClose}
primaryAction={{
label: T.translate("general.save"),
onClick: handleSave
}}
>
<Typography variant="body2" component="div" sx={{ marginBottom: "20px" }}>
{item?.name}
</Typography>
<Divider
sx={{
marginBottom: "20px",
marginLeft: "-24px",
marginRight: "-24px"
}}
/>
{itemFields.map((exc) => (
<Box key={`item-field-${exc.type_id}`} sx={{ mb: 2 }}>
<ItemTableField
field={exc}
rowId={item.form_item_id}
timeZone={timeZone}
label={exc.name}
/>
</Box>
))}
</CustomDialog>
);
};

Expand Down
59 changes: 23 additions & 36 deletions src/components/mui/NotesModal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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`;
Expand All @@ -34,40 +34,27 @@ const NotesModal = ({ id, label, open, title, placeholder, onClose }) => {
};

return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{title || T.translate("general.notes")}</DialogTitle>
<IconButton
aria-label="close"
onClick={onClose}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
color: theme.palette.grey[500]
})}
>
<CloseIcon />
</IconButton>
<Divider />
<DialogContent>
<DialogContentText>{label}</DialogContentText>
<TextField
name={name}
onChange={(ev) => setNotes(ev.target.value)}
value={notes}
margin="normal"
multiline
fullWidth
rows={4}
placeholder={placeholder || T.translate("placeholders.notes")}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleSave} variant="contained" fullWidth>
{T.translate("general.save")}
</Button>
</DialogActions>
</Dialog>
<CustomDialog
title={title || T.translate("general.notes")}
open={open}
onClose={onClose}
primaryAction={{
label: T.translate("general.save"),
onClick: handleSave
}}
>
<DialogContentText>{label}</DialogContentText>
<TextField
name={name}
onChange={(ev) => setNotes(ev.target.value)}
value={notes}
margin="normal"
multiline
fullWidth
rows={4}
placeholder={placeholder || T.translate("placeholders.notes")}
/>
</CustomDialog>
);
};

Expand Down
Loading
Loading