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    padding: theme.spacing(2),
115  },
116}))(MuiAccordionDetails);
117
118type Props = {
119  bugId: string;
120  commentId: string;
121  open: boolean;
122  onClose: () => void;
123};
124function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) {
125  const [expanded, setExpanded] = React.useState<string | false>('panel0');
126
127  const { loading, error, data } = useMessageHistoryQuery({
128    variables: { bugIdPrefix: bugId },
129  });
130  if (loading) {
131    return (
132      <Dialog
133        onClose={onClose}
134        aria-labelledby="customized-dialog-title"
135        open={open}
136        fullWidth
137        maxWidth="sm"
138      >
139        <DialogTitle id="customized-dialog-title" onClose={onClose}>
140          Loading...
141        </DialogTitle>
142        <DialogContent dividers>
143          <Grid container justify="center">
144            <CircularProgress />
145          </Grid>
146        </DialogContent>
147      </Dialog>
148    );
149  }
150  if (error) {
151    return (
152      <Dialog
153        onClose={onClose}
154        aria-labelledby="customized-dialog-title"
155        open={open}
156        fullWidth
157        maxWidth="sm"
158      >
159        <DialogTitle id="customized-dialog-title" onClose={onClose}>
160          Something went wrong...
161        </DialogTitle>
162        <DialogContent dividers>
163          <p>Error: {error}</p>
164        </DialogContent>
165      </Dialog>
166    );
167  }
168
169  const comments = data?.repository?.bug?.timeline.comments as (
170    | AddCommentFragment
171    | CreateFragment
172  )[];
173  // NOTE Searching for the changed comment could be dropped if GraphQL get
174  // filter by id argument for timelineitems
175  const comment = comments.find((elem) => elem.id === commentId);
176  // Sort by most recent edit. Must create a copy of constant history as
177  // reverse() modifies inplace.
178  const history = comment?.history.slice().reverse();
179  const editCount = history?.length === undefined ? 0 : history?.length - 1;
180
181  const handleChange = (panel: string) => (
182    event: React.ChangeEvent<{}>,
183    newExpanded: boolean
184  ) => {
185    setExpanded(newExpanded ? panel : false);
186  };
187
188  const getSummary = (index: number, date: Date) => {
189    const desc =
190      index === editCount ? 'Created ' : `#${editCount - index} • Edited `;
191    const mostRecent = index === 0 ? ' (most recent)' : '';
192    return (
193      <>
194        <Tooltip title={moment(date).format('LLLL')}>
195          <span>
196            {desc}
197            <Moment date={date} format="on ll" />
198            {mostRecent}
199          </span>
200        </Tooltip>
201      </>
202    );
203  };
204
205  return (
206    <Dialog
207      onClose={onClose}
208      aria-labelledby="customized-dialog-title"
209      open={open}
210      fullWidth
211      maxWidth="md"
212    >
213      <DialogTitle id="customized-dialog-title" onClose={onClose}>
214        {`Edited ${editCount} ${editCount > 1 ? 'times' : 'time'}.`}
215      </DialogTitle>
216      <DialogContent dividers>
217        {history?.map((edit, index) => (
218          <Accordion
219            square
220            key={index}
221            expanded={expanded === 'panel' + index}
222            onChange={handleChange('panel' + index)}
223          >
224            <AccordionSummary
225              expandIcon={<ExpandMoreIcon />}
226              aria-controls="panel1d-content"
227              id="panel1d-header"
228            >
229              <Typography>{getSummary(index, edit.date)}</Typography>
230            </AccordionSummary>
231            <AccordionDetails>
232              <Content markdown={edit.message} />
233            </AccordionDetails>
234          </Accordion>
235        ))}
236      </DialogContent>
237    </Dialog>
238  );
239}
240
241export default MessageHistoryDialog;