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';
20import Moment from 'react-moment';
21
22import Content from '../../components/Content';
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}</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;