Use dialog with accordion for message history menu

Sascha created

Change summary

webui/src/pages/bug/EditHistoryMenu.tsx      |  67 ------
webui/src/pages/bug/Message.tsx              |  36 +-
webui/src/pages/bug/MessageHistoryDialog.tsx | 215 ++++++++++++++++++++++
3 files changed, 233 insertions(+), 85 deletions(-)

Detailed changes

webui/src/pages/bug/EditHistoryMenu.tsx 🔗

@@ -1,67 +0,0 @@
-import React from 'react';
-
-import CircularProgress from '@material-ui/core/CircularProgress';
-import Menu from '@material-ui/core/Menu';
-import MenuItem from '@material-ui/core/MenuItem';
-
-import Date from 'src/components/Date';
-
-import { AddCommentFragment } from './MessageCommentFragment.generated';
-import { CreateFragment } from './MessageCreateFragment.generated';
-import { useMessageEditHistoryQuery } from './MessageEditHistory.generated';
-
-const ITEM_HEIGHT = 48;
-
-type Props = {
-  anchor: null | HTMLElement;
-  bugId: string;
-  commentId: string;
-  onClose: () => void;
-};
-function EditHistoryMenu({ anchor, bugId, commentId, onClose }: Props) {
-  const open = Boolean(anchor);
-
-  const { loading, error, data } = useMessageEditHistoryQuery({
-    variables: { bugIdPrefix: bugId },
-  });
-  if (loading) return <CircularProgress />;
-  if (error) return <p>Error: {error}</p>;
-
-  const comments = data?.repository?.bug?.timeline.comments as (
-    | AddCommentFragment
-    | CreateFragment
-  )[];
-  // NOTE Searching for the changed comment could be dropped if GraphQL get
-  // filter by id argument for timelineitems
-  const comment = comments.find((elem) => elem.id === commentId);
-  const history = comment?.history;
-
-  return (
-    <div>
-      <Menu
-        id="long-menu"
-        anchorEl={anchor}
-        keepMounted
-        open={open}
-        onClose={onClose}
-        PaperProps={{
-          style: {
-            maxHeight: ITEM_HEIGHT * 4.5,
-            width: '20ch',
-          },
-        }}
-      >
-        <MenuItem key={0} disabled>
-          Edited {history?.length} times.
-        </MenuItem>
-        {history?.map((edit, index) => (
-          <MenuItem key={index} onClick={onClose}>
-            <Date date={edit.date} />
-          </MenuItem>
-        ))}
-      </Menu>
-    </div>
-  );
-}
-
-export default EditHistoryMenu;

webui/src/pages/bug/Message.tsx 🔗

@@ -14,9 +14,9 @@ import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn';
 
 import { BugFragment } from './Bug.generated';
 import EditCommentForm from './EditCommentForm';
-import EditHistoryMenu from './EditHistoryMenu';
 import { AddCommentFragment } from './MessageCommentFragment.generated';
 import { CreateFragment } from './MessageCreateFragment.generated';
+import MessageHistoryDialog from './MessageHistoryDialog';
 
 const useStyles = makeStyles((theme) => ({
   author: {
@@ -70,10 +70,6 @@ const useStyles = makeStyles((theme) => ({
   },
 }));
 
-//TODO move button out of this component and let only menu as component with
-//query. Then the query won't execute unless button click renders menu with
-//query.
-//TODO Fix display of load button spinner.
 //TODO Move this button and menu in separate component directory
 //TODO fix failing pipeline due to eslint error
 type HistBtnProps = {
@@ -82,14 +78,14 @@ type HistBtnProps = {
 };
 function HistoryMenuToggleButton({ bugId, commentId }: HistBtnProps) {
   const classes = useStyles();
-  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
+  const [open, setOpen] = React.useState(false);
 
-  const handleClick = (event: React.MouseEvent<HTMLElement>) => {
-    setAnchorEl(event.currentTarget);
+  const handleClickOpen = () => {
+    setOpen(true);
   };
 
   const handleClose = () => {
-    setAnchorEl(null);
+    setOpen(false);
   };
 
   return (
@@ -98,19 +94,23 @@ function HistoryMenuToggleButton({ bugId, commentId }: HistBtnProps) {
         aria-label="more"
         aria-controls="long-menu"
         aria-haspopup="true"
-        onClick={handleClick}
+        onClick={handleClickOpen}
         className={classes.headerActions}
       >
         <HistoryIcon />
       </IconButton>
-      {anchorEl && (
-        <EditHistoryMenu
-          bugId={bugId}
-          commentId={commentId}
-          anchor={anchorEl}
-          onClose={handleClose}
-        />
-      )}
+      {
+        // Render CustomizedDialogs on open to prevent fetching the history
+        // before opening the history menu.
+        open && (
+          <MessageHistoryDialog
+            bugId={bugId}
+            commentId={commentId}
+            open={open}
+            onClose={handleClose}
+          />
+        )
+      }
     </div>
   );
 }

webui/src/pages/bug/MessageHistoryDialog.tsx 🔗

@@ -0,0 +1,215 @@
+import moment from 'moment';
+import React from 'react';
+import Moment from 'react-moment';
+
+import MuiAccordion from '@material-ui/core/Accordion';
+import MuiAccordionDetails from '@material-ui/core/AccordionDetails';
+import MuiAccordionSummary from '@material-ui/core/AccordionSummary';
+import CircularProgress from '@material-ui/core/CircularProgress';
+import Dialog from '@material-ui/core/Dialog';
+import MuiDialogContent from '@material-ui/core/DialogContent';
+import MuiDialogTitle from '@material-ui/core/DialogTitle';
+import Grid from '@material-ui/core/Grid';
+import IconButton from '@material-ui/core/IconButton';
+import Tooltip from '@material-ui/core/Tooltip/Tooltip';
+import Typography from '@material-ui/core/Typography';
+import {
+  createStyles,
+  Theme,
+  withStyles,
+  WithStyles,
+} from '@material-ui/core/styles';
+import CloseIcon from '@material-ui/icons/Close';
+
+import { AddCommentFragment } from './MessageCommentFragment.generated';
+import { CreateFragment } from './MessageCreateFragment.generated';
+import { useMessageEditHistoryQuery } from './MessageEditHistory.generated';
+
+const styles = (theme: Theme) =>
+  createStyles({
+    root: {
+      margin: 0,
+      padding: theme.spacing(2),
+    },
+    closeButton: {
+      position: 'absolute',
+      right: theme.spacing(1),
+      top: theme.spacing(1),
+    },
+  });
+
+export interface DialogTitleProps extends WithStyles<typeof styles> {
+  id: string;
+  children: React.ReactNode;
+  onClose: () => void;
+}
+
+const DialogTitle = withStyles(styles)((props: DialogTitleProps) => {
+  const { children, classes, onClose, ...other } = props;
+  return (
+    <MuiDialogTitle disableTypography className={classes.root} {...other}>
+      <Typography variant="h6">{children}</Typography>
+      {onClose ? (
+        <IconButton
+          aria-label="close"
+          className={classes.closeButton}
+          onClick={onClose}
+        >
+          <CloseIcon />
+        </IconButton>
+      ) : null}
+    </MuiDialogTitle>
+  );
+});
+
+const DialogContent = withStyles((theme: Theme) => ({
+  root: {
+    padding: theme.spacing(2),
+  },
+}))(MuiDialogContent);
+
+const Accordion = withStyles({
+  root: {
+    border: '1px solid rgba(0, 0, 0, .125)',
+    boxShadow: 'none',
+    '&:not(:last-child)': {
+      borderBottom: 0,
+    },
+    '&:before': {
+      display: 'none',
+    },
+    '&$expanded': {
+      margin: 'auto',
+    },
+  },
+  expanded: {},
+})(MuiAccordion);
+
+const AccordionSummary = withStyles((theme) => ({
+  root: {
+    backgroundColor: theme.palette.primary.light,
+    borderBottomWidth: '1px',
+    borderBottomStyle: 'solid',
+    borderBottomColor: theme.palette.divider,
+    marginBottom: -1,
+    minHeight: 56,
+    '&$expanded': {
+      minHeight: 56,
+    },
+  },
+  content: {
+    '&$expanded': {
+      margin: '12px 0',
+    },
+  },
+  expanded: {},
+}))(MuiAccordionSummary);
+
+const AccordionDetails = withStyles((theme) => ({
+  root: {
+    padding: theme.spacing(2),
+  },
+}))(MuiAccordionDetails);
+
+type Props = {
+  bugId: string;
+  commentId: string;
+  open: boolean;
+  onClose: () => void;
+};
+function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) {
+  const [expanded, setExpanded] = React.useState<string | false>('panel0');
+
+  const { loading, error, data } = useMessageEditHistoryQuery({
+    variables: { bugIdPrefix: bugId },
+  });
+  if (loading) {
+    return (
+      <Dialog
+        onClose={onClose}
+        aria-labelledby="customized-dialog-title"
+        open={open}
+        fullWidth
+        maxWidth="sm"
+      >
+        <DialogTitle id="customized-dialog-title" onClose={onClose}>
+          Loading...
+        </DialogTitle>
+        <DialogContent dividers>
+          <Grid container justify="center">
+            <CircularProgress />
+          </Grid>
+        </DialogContent>
+      </Dialog>
+    );
+  }
+  if (error) {
+    return (
+      <Dialog
+        onClose={onClose}
+        aria-labelledby="customized-dialog-title"
+        open={open}
+        fullWidth
+        maxWidth="sm"
+      >
+        <DialogTitle id="customized-dialog-title" onClose={onClose}>
+          Something went wrong...
+        </DialogTitle>
+        <DialogContent dividers>
+          <p>Error: {error}</p>
+        </DialogContent>
+      </Dialog>
+    );
+  }
+
+  const comments = data?.repository?.bug?.timeline.comments as (
+    | AddCommentFragment
+    | CreateFragment
+  )[];
+  // NOTE Searching for the changed comment could be dropped if GraphQL get
+  // filter by id argument for timelineitems
+  const comment = comments.find((elem) => elem.id === commentId);
+  const history = comment?.history;
+
+  const handleChange = (panel: string) => (
+    event: React.ChangeEvent<{}>,
+    newExpanded: boolean
+  ) => {
+    setExpanded(newExpanded ? panel : false);
+  };
+
+  return (
+    <Dialog
+      onClose={onClose}
+      aria-labelledby="customized-dialog-title"
+      open={open}
+      fullWidth
+      maxWidth="md"
+    >
+      <DialogTitle id="customized-dialog-title" onClose={onClose}>
+        Edited {history?.length} times.
+      </DialogTitle>
+      <DialogContent dividers>
+        {history?.map((edit, index) => (
+          <Accordion
+            square
+            expanded={expanded === 'panel' + index}
+            onChange={handleChange('panel' + index)}
+          >
+            <AccordionSummary
+              aria-controls="panel1d-content"
+              id="panel1d-header"
+            >
+              <Tooltip title={moment(edit.date).format('LLLL')}>
+                <Moment date={edit.date} format="on ll" />
+              </Tooltip>
+            </AccordionSummary>
+            <AccordionDetails>{edit.message}</AccordionDetails>
+          </Accordion>
+        ))}
+      </DialogContent>
+    </Dialog>
+  );
+}
+
+export default MessageHistoryDialog;