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