MessageHistoryDialog.tsx

  1import moment from 'moment';
  2import * as 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 =
183    (panel: string) => (event: React.ChangeEvent<{}>, newExpanded: boolean) => {
184      setExpanded(newExpanded ? panel : false);
185    };
186
187  const getSummary = (index: number, date: Date) => {
188    const desc =
189      index === editCount ? 'Created ' : `#${editCount - index} • Edited `;
190    const mostRecent = index === 0 ? ' (most recent)' : '';
191    return (
192      <>
193        <Tooltip title={moment(date).format('LLLL')}>
194          <span>
195            {desc}
196            <Moment date={date} format="on ll" />
197            {mostRecent}
198          </span>
199        </Tooltip>
200      </>
201    );
202  };
203
204  return (
205    <Dialog
206      onClose={onClose}
207      aria-labelledby="customized-dialog-title"
208      open={open}
209      fullWidth
210      maxWidth="md"
211    >
212      <DialogTitle id="customized-dialog-title" onClose={onClose}>
213        {`Edited ${editCount} ${editCount > 1 ? 'times' : 'time'}.`}
214      </DialogTitle>
215      <DialogContent dividers>
216        {history?.map((edit, index) => (
217          <Accordion
218            square
219            key={index}
220            expanded={expanded === 'panel' + index}
221            onChange={handleChange('panel' + index)}
222          >
223            <AccordionSummary
224              expandIcon={<ExpandMoreIcon />}
225              aria-controls="panel1d-content"
226              id="panel1d-header"
227            >
228              <Typography>{getSummary(index, edit.date)}</Typography>
229            </AccordionSummary>
230            <AccordionDetails>
231              {edit.message !== '' ? (
232                <Content markdown={edit.message} />
233              ) : (
234                <Content markdown="*No description provided.*" />
235              )}
236            </AccordionDetails>
237          </Accordion>
238        ))}
239      </DialogContent>
240    </Dialog>
241  );
242}
243
244export default MessageHistoryDialog;