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;