MessageHistoryDialog.tsx

  1import moment from 'moment';
  2import React from 'react';
  3import Moment from 'react-moment';
  4
  5import MuiAccordion from '@material-ui/core/Accordion';
  6import MuiAccordionDetails from '@material-ui/core/AccordionDetails';
  7import MuiAccordionSummary from '@material-ui/core/AccordionSummary';
  8import CircularProgress from '@material-ui/core/CircularProgress';
  9import Dialog from '@material-ui/core/Dialog';
 10import MuiDialogContent from '@material-ui/core/DialogContent';
 11import MuiDialogTitle from '@material-ui/core/DialogTitle';
 12import Grid from '@material-ui/core/Grid';
 13import IconButton from '@material-ui/core/IconButton';
 14import Tooltip from '@material-ui/core/Tooltip/Tooltip';
 15import Typography from '@material-ui/core/Typography';
 16import {
 17  createStyles,
 18  Theme,
 19  withStyles,
 20  WithStyles,
 21} from '@material-ui/core/styles';
 22import CloseIcon from '@material-ui/icons/Close';
 23import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
 24
 25import Content from '../../components/Content';
 26
 27import { AddCommentFragment } from './MessageCommentFragment.generated';
 28import { CreateFragment } from './MessageCreateFragment.generated';
 29import { useMessageHistoryQuery } from './MessageHistory.generated';
 30
 31const styles = (theme: Theme) =>
 32  createStyles({
 33    root: {
 34      margin: 0,
 35      padding: theme.spacing(2),
 36    },
 37    closeButton: {
 38      position: 'absolute',
 39      right: theme.spacing(1),
 40      top: theme.spacing(1),
 41    },
 42  });
 43
 44export interface DialogTitleProps extends WithStyles<typeof styles> {
 45  id: string;
 46  children: React.ReactNode;
 47  onClose: () => void;
 48}
 49
 50const DialogTitle = withStyles(styles)((props: DialogTitleProps) => {
 51  const { children, classes, onClose, ...other } = props;
 52  return (
 53    <MuiDialogTitle disableTypography className={classes.root} {...other}>
 54      <Typography variant="h6">{children}</Typography>
 55      {onClose ? (
 56        <IconButton
 57          aria-label="close"
 58          className={classes.closeButton}
 59          onClick={onClose}
 60        >
 61          <CloseIcon />
 62        </IconButton>
 63      ) : null}
 64    </MuiDialogTitle>
 65  );
 66});
 67
 68const DialogContent = withStyles((theme: Theme) => ({
 69  root: {
 70    padding: theme.spacing(2),
 71  },
 72}))(MuiDialogContent);
 73
 74const Accordion = withStyles({
 75  root: {
 76    border: '1px solid rgba(0, 0, 0, .125)',
 77    boxShadow: 'none',
 78    '&:not(:last-child)': {
 79      borderBottom: 0,
 80    },
 81    '&:before': {
 82      display: 'none',
 83    },
 84    '&$expanded': {
 85      margin: 'auto',
 86    },
 87  },
 88  expanded: {},
 89})(MuiAccordion);
 90
 91const AccordionSummary = withStyles((theme) => ({
 92  root: {
 93    backgroundColor: theme.palette.primary.light,
 94    borderBottomWidth: '1px',
 95    borderBottomStyle: 'solid',
 96    borderBottomColor: theme.palette.divider,
 97    marginBottom: -1,
 98    minHeight: 56,
 99    '&$expanded': {
100      minHeight: 56,
101    },
102  },
103  content: {
104    '&$expanded': {
105      margin: '12px 0',
106    },
107  },
108  expanded: {},
109}))(MuiAccordionSummary);
110
111const AccordionDetails = withStyles((theme) => ({
112  root: {
113    display: 'block',
114    overflow: 'auto',
115    padding: theme.spacing(2),
116  },
117}))(MuiAccordionDetails);
118
119type Props = {
120  bugId: string;
121  commentId: string;
122  open: boolean;
123  onClose: () => void;
124};
125function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) {
126  const [expanded, setExpanded] = React.useState<string | false>('panel0');
127
128  const { loading, error, data } = useMessageHistoryQuery({
129    variables: { bugIdPrefix: bugId },
130  });
131  if (loading) {
132    return (
133      <Dialog
134        onClose={onClose}
135        aria-labelledby="customized-dialog-title"
136        open={open}
137        fullWidth
138        maxWidth="sm"
139      >
140        <DialogTitle id="customized-dialog-title" onClose={onClose}>
141          Loading...
142        </DialogTitle>
143        <DialogContent dividers>
144          <Grid container justify="center">
145            <CircularProgress />
146          </Grid>
147        </DialogContent>
148      </Dialog>
149    );
150  }
151  if (error) {
152    return (
153      <Dialog
154        onClose={onClose}
155        aria-labelledby="customized-dialog-title"
156        open={open}
157        fullWidth
158        maxWidth="sm"
159      >
160        <DialogTitle id="customized-dialog-title" onClose={onClose}>
161          Something went wrong...
162        </DialogTitle>
163        <DialogContent dividers>
164          <p>Error: {error}</p>
165        </DialogContent>
166      </Dialog>
167    );
168  }
169
170  const comments = data?.repository?.bug?.timeline.comments as (
171    | AddCommentFragment
172    | CreateFragment
173  )[];
174  // NOTE Searching for the changed comment could be dropped if GraphQL get
175  // filter by id argument for timelineitems
176  const comment = comments.find((elem) => elem.id === commentId);
177  // Sort by most recent edit. Must create a copy of constant history as
178  // reverse() modifies inplace.
179  const history = comment?.history.slice().reverse();
180  const editCount = history?.length === undefined ? 0 : history?.length - 1;
181
182  const handleChange = (panel: string) => (
183    event: React.ChangeEvent<{}>,
184    newExpanded: boolean
185  ) => {
186    setExpanded(newExpanded ? panel : false);
187  };
188
189  const getSummary = (index: number, date: Date) => {
190    const desc =
191      index === editCount ? 'Created ' : `#${editCount - index} • Edited `;
192    const mostRecent = index === 0 ? ' (most recent)' : '';
193    return (
194      <>
195        <Tooltip title={moment(date).format('LLLL')}>
196          <span>
197            {desc}
198            <Moment date={date} format="on ll" />
199            {mostRecent}
200          </span>
201        </Tooltip>
202      </>
203    );
204  };
205
206  return (
207    <Dialog
208      onClose={onClose}
209      aria-labelledby="customized-dialog-title"
210      open={open}
211      fullWidth
212      maxWidth="md"
213    >
214      <DialogTitle id="customized-dialog-title" onClose={onClose}>
215        {`Edited ${editCount} ${editCount > 1 ? 'times' : 'time'}.`}
216      </DialogTitle>
217      <DialogContent dividers>
218        {history?.map((edit, index) => (
219          <Accordion
220            square
221            key={index}
222            expanded={expanded === 'panel' + index}
223            onChange={handleChange('panel' + index)}
224          >
225            <AccordionSummary
226              expandIcon={<ExpandMoreIcon />}
227              aria-controls="panel1d-content"
228              id="panel1d-header"
229            >
230              <Typography>{getSummary(index, edit.date)}</Typography>
231            </AccordionSummary>
232            <AccordionDetails>
233              <Content markdown={edit.message} />
234            </AccordionDetails>
235          </Accordion>
236        ))}
237      </DialogContent>
238    </Dialog>
239  );
240}
241
242export default MessageHistoryDialog;