Uniformize styles for dialogs and popovers, add ContextMenu
Change-Id: I8687b2d171f9c15e8eb8dd5ba2a32cdaa27b70d6
diff --git a/client/src/components/Dialog.tsx b/client/src/components/Dialog.tsx
new file mode 100644
index 0000000..9a6790d
--- /dev/null
+++ b/client/src/components/Dialog.tsx
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Box, Button, List, ListItem, ListItemIcon, Stack, SvgIconProps, Typography } from '@mui/material';
+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 { ComponentType, ReactNode, useCallback, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+interface DialogHandler {
+ props: {
+ open: boolean;
+ onClose: () => void;
+ };
+ openDialog: () => void;
+}
+
+export const useDialogHandler = (): DialogHandler => {
+ const [open, setOpen] = useState(false);
+
+ const onClose = useCallback(() => {
+ setOpen(false);
+ }, []);
+
+ const openDialog = useCallback(() => {
+ setOpen(true);
+ }, []);
+
+ return useMemo(
+ () => ({
+ props: { open, onClose },
+ openDialog,
+ }),
+ [open, onClose, openDialog]
+ );
+};
+
+interface BaseDialogProps {
+ open: boolean;
+ onClose: () => void;
+ icon?: ReactNode;
+ title: string;
+ content: ReactNode;
+ actions: ReactNode;
+}
+
+export const BaseDialog = ({ open, onClose, icon, title, content, actions }: BaseDialogProps) => {
+ return (
+ <Dialog open={open} onClose={onClose}>
+ <DialogTitle>
+ <Stack direction="row" alignItems="center" spacing="16px">
+ {icon && (
+ <Box height="80px" width="80px">
+ {icon}
+ </Box>
+ )}
+ <Box>
+ <Typography variant="h2">{title}</Typography>
+ </Box>
+ </Stack>
+ </DialogTitle>
+ <DialogContent>{content}</DialogContent>
+ <DialogActions>{actions}</DialogActions>
+ </Dialog>
+ );
+};
+
+type InfosDialogProps = Omit<BaseDialogProps, 'actions'>;
+
+export const InfosDialog = (props: InfosDialogProps) => {
+ const { t } = useTranslation();
+ return (
+ <BaseDialog
+ {...props}
+ actions={
+ <Button onClick={props.onClose} autoFocus>
+ {t('dialog_close')}
+ </Button>
+ }
+ />
+ );
+};
+
+interface ConfirmationDialogProps extends Omit<BaseDialogProps, 'actions'> {
+ onConfirm: () => void;
+ confirmButtonText: string;
+}
+
+export const ConfirmationDialog = ({ onConfirm, confirmButtonText, ...props }: ConfirmationDialogProps) => {
+ const { t } = useTranslation();
+ props.title = props.title || t('dialog_confirm_title_default');
+
+ return (
+ <BaseDialog
+ {...props}
+ actions={
+ <>
+ <Button onClick={onConfirm}>{confirmButtonText}</Button>
+ <Button onClick={props.onClose}>{t('dialog_cancel')}</Button>
+ </>
+ }
+ />
+ );
+};
+
+interface DialogContentListItem {
+ Icon?: ComponentType<SvgIconProps>;
+ label?: string;
+ value: ReactNode;
+}
+
+interface DialogContentListProps {
+ title?: string;
+ items: DialogContentListItem[];
+}
+
+export const DialogContentList = ({ title, items }: DialogContentListProps) => {
+ return (
+ <List subheader={<Typography variant="h3">{title}</Typography>}>
+ {items.map(({ Icon, label, value }, index) => (
+ <ListItem key={index}>
+ {Icon && (
+ <ListItemIcon>
+ <Icon />
+ </ListItemIcon>
+ )}
+ <Stack direction="row" alignItems="center" spacing="24px">
+ {label && (
+ <Stack direction="row" width="100px" justifyContent="end">
+ <Typography variant="body2" color="#a0a0a0">
+ {label}
+ </Typography>
+ </Stack>
+ )}
+ <Box>
+ <Typography variant="body2" sx={{ fontWeight: 'bold' }}>
+ {value}
+ </Typography>
+ </Box>
+ </Stack>
+ </ListItem>
+ ))}
+ </List>
+ );
+};