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 { AddCommentFragment } from './MessageCommentFragment.generated';
26import { CreateFragment } from './MessageCreateFragment.generated';
27import { useMessageHistoryQuery } from './MessageHistory.generated';
28
29const styles = (theme: Theme) =>
30 createStyles({
31 root: {
32 margin: 0,
33 padding: theme.spacing(2),
34 },
35 closeButton: {
36 position: 'absolute',
37 right: theme.spacing(1),
38 top: theme.spacing(1),
39 },
40 });
41
42export interface DialogTitleProps extends WithStyles<typeof styles> {
43 id: string;
44 children: React.ReactNode;
45 onClose: () => void;
46}
47
48const DialogTitle = withStyles(styles)((props: DialogTitleProps) => {
49 const { children, classes, onClose, ...other } = props;
50 return (
51 <MuiDialogTitle disableTypography className={classes.root} {...other}>
52 <Typography variant="h6">{children}</Typography>
53 {onClose ? (
54 <IconButton
55 aria-label="close"
56 className={classes.closeButton}
57 onClick={onClose}
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 padding: theme.spacing(2),
112 },
113}))(MuiAccordionDetails);
114
115type Props = {
116 bugId: string;
117 commentId: string;
118 open: boolean;
119 onClose: () => void;
120};
121function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) {
122 const [expanded, setExpanded] = React.useState<string | false>('panel0');
123
124 const { loading, error, data } = useMessageHistoryQuery({
125 variables: { bugIdPrefix: bugId },
126 });
127 if (loading) {
128 return (
129 <Dialog
130 onClose={onClose}
131 aria-labelledby="customized-dialog-title"
132 open={open}
133 fullWidth
134 maxWidth="sm"
135 >
136 <DialogTitle id="customized-dialog-title" onClose={onClose}>
137 Loading...
138 </DialogTitle>
139 <DialogContent dividers>
140 <Grid container justify="center">
141 <CircularProgress />
142 </Grid>
143 </DialogContent>
144 </Dialog>
145 );
146 }
147 if (error) {
148 return (
149 <Dialog
150 onClose={onClose}
151 aria-labelledby="customized-dialog-title"
152 open={open}
153 fullWidth
154 maxWidth="sm"
155 >
156 <DialogTitle id="customized-dialog-title" onClose={onClose}>
157 Something went wrong...
158 </DialogTitle>
159 <DialogContent dividers>
160 <p>Error: {error}</p>
161 </DialogContent>
162 </Dialog>
163 );
164 }
165
166 const comments = data?.repository?.bug?.timeline.comments as (
167 | AddCommentFragment
168 | CreateFragment
169 )[];
170 // NOTE Searching for the changed comment could be dropped if GraphQL get
171 // filter by id argument for timelineitems
172 const comment = comments.find((elem) => elem.id === commentId);
173 // Sort by most recent edit. Must create a copy of constant history as
174 // reverse() modifies inplace.
175 const history = comment?.history.slice().reverse();
176
177 const handleChange = (panel: string) => (
178 event: React.ChangeEvent<{}>,
179 newExpanded: boolean
180 ) => {
181 setExpanded(newExpanded ? panel : false);
182 };
183
184 return (
185 <Dialog
186 onClose={onClose}
187 aria-labelledby="customized-dialog-title"
188 open={open}
189 fullWidth
190 maxWidth="md"
191 >
192 <DialogTitle id="customized-dialog-title" onClose={onClose}>
193 Edited {history?.length} times.
194 </DialogTitle>
195 <DialogContent dividers>
196 {history?.map((edit, index) => (
197 <Accordion
198 square
199 expanded={expanded === 'panel' + index}
200 onChange={handleChange('panel' + index)}
201 >
202 <AccordionSummary
203 expandIcon={<ExpandMoreIcon />}
204 aria-controls="panel1d-content"
205 id="panel1d-header"
206 >
207 <Typography>
208 {`#${history?.length - index} • Edited `}
209 <Tooltip title={moment(edit.date).format('LLLL')}>
210 <Moment date={edit.date} format="on ll" />
211 </Tooltip>
212 {index === 0 && ' (most recent edit)'}
213 {index === history?.length - 1 && ' (Initial description)'}
214 </Typography>
215 </AccordionSummary>
216 <AccordionDetails>{edit.message}</AccordionDetails>
217 </Accordion>
218 ))}
219 </DialogContent>
220 </Dialog>
221 );
222}
223
224export default MessageHistoryDialog;