Skip to content

Commit 26336bd

Browse files
authored
Merge pull request #21 from apdavison/discussions
Add "Discussions" feature
2 parents 14df6c2 + 87d5778 commit 26336bd

File tree

5 files changed

+412
-21
lines changed

5 files changed

+412
-21
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import React from "react";
2+
import axios from "axios";
3+
import {
4+
Box,
5+
Button,
6+
CircularProgress,
7+
Grid,
8+
Typography,
9+
TextField,
10+
IconButton,
11+
} from "@material-ui/core";
12+
import DeleteIcon from "@material-ui/icons/Delete";
13+
import EditIcon from "@material-ui/icons/Edit";
14+
15+
import { datastore } from "./datastore";
16+
import Markdown from "./Markdown";
17+
import Theme from "./theme";
18+
import {
19+
formatTimeStampAsDate,
20+
formatTimeStampToLongString,
21+
formatTimeStampToCompact,
22+
} from "./utils";
23+
24+
function CommentEditor(props) {
25+
return (
26+
<React.Fragment>
27+
<TextField
28+
id={props.id}
29+
placeholder="Use Markdown to format your comment"
30+
value={props.content}
31+
onChange={props.onChange}
32+
multiline
33+
minRows={4}
34+
variant="outlined"
35+
style={{ width: "100%" }}
36+
/>
37+
<Button onClick={props.handleCancel} color="default">
38+
Cancel
39+
</Button>
40+
<Button onClick={() => props.handleSave({submit: false})} color="primary">
41+
Save draft
42+
</Button>
43+
<Button onClick={() => props.handleSave({submit: true})} color="primary">
44+
Submit comment
45+
</Button>
46+
</React.Fragment>
47+
);
48+
}
49+
50+
function CommentToolbar(props) {
51+
52+
const editButton = (hide) => {
53+
if (hide) {
54+
return "";
55+
} else {
56+
return (<IconButton
57+
aria-label="edit"
58+
size="small"
59+
onClick={() => props.handleSetEditing(true)}
60+
>
61+
<EditIcon />
62+
</IconButton>)
63+
}
64+
}
65+
66+
return (
67+
<Grid
68+
container
69+
style={{
70+
display: "flex",
71+
alignItems: "center",
72+
backgroundColor: Theme.mediumBackground,
73+
}}
74+
>
75+
<Grid item xs={6}>
76+
<Box px={2} py={1} display="flex" flexDirection="row">
77+
<Typography variant="subtitle2">
78+
<b>
79+
{props.comment.commenter.given_name}&nbsp;
80+
{props.comment.commenter.family_name}
81+
</b>
82+
&nbsp;
83+
</Typography>
84+
<Typography variant="caption">
85+
{formatTimeStampToLongString(props.comment.timestamp)}
86+
&nbsp;
87+
</Typography>
88+
<Typography variant="caption">({props.comment.status})</Typography>
89+
</Box>
90+
</Grid>
91+
<Grid container item justifyContent="flex-end" xs={6}>
92+
<Box
93+
px={2}
94+
style={{
95+
display: "flex",
96+
alignItems: "center",
97+
justifyContent: "center",
98+
}}
99+
>
100+
<Typography variant="body2" color="textSecondary">
101+
ID: {props.comment.id}
102+
</Typography>
103+
{props.comment.status === "draft" ? editButton(props.editing) : ""}
104+
{props.canDelete ? (
105+
<IconButton
106+
aria-label="delete"
107+
size="small"
108+
onClick={() => props.handleDelete(props.comment.id)}
109+
>
110+
<DeleteIcon />
111+
</IconButton>
112+
) : (
113+
""
114+
)}
115+
</Box>
116+
</Grid>
117+
</Grid>
118+
);
119+
}
120+
121+
function CommentBox(props) {
122+
const [content, setContent] = React.useState("");
123+
const [editing, setEditing] = React.useState(props.openEditor);
124+
125+
React.useEffect(() => {
126+
if (content === "") {
127+
setContent(props.comment.content);
128+
}
129+
});
130+
131+
const onChange = (event) => {
132+
setContent(event.target.value);
133+
};
134+
135+
const handleCancel = () => {
136+
console.log(props.comment);
137+
setContent(props.comment.content || "");
138+
setEditing(props.openEditor);
139+
};
140+
141+
const handleSave = ({submit = false}) => {
142+
setEditing(props.openEditor);
143+
props.handleSave(content, submit);
144+
if (props.openEditor) {
145+
setContent("");
146+
}
147+
};
148+
149+
return (
150+
<Grid item xs={12} key={props.comment.id}>
151+
{/* top bar */}
152+
{props.comment.id ? (
153+
<CommentToolbar
154+
comment={props.comment}
155+
canDelete={props.canDelete}
156+
handleSetEditing={setEditing}
157+
handleDelete={props.handleDelete}
158+
editing={editing}
159+
/>
160+
) : (
161+
<Typography variant="h6">Add a comment</Typography>
162+
)}
163+
{/* main content */}
164+
<Box
165+
px={2}
166+
py={1}
167+
style={{
168+
backgroundColor: Theme.lightBackground,
169+
}}
170+
>
171+
{editing ? (
172+
<CommentEditor
173+
id={props.comment.id}
174+
content={content}
175+
onChange={onChange}
176+
handleCancel={handleCancel}
177+
handleSave={handleSave}
178+
/>
179+
) : (
180+
<Markdown source={content} />
181+
)}
182+
</Box>
183+
</Grid>
184+
);
185+
}
186+
187+
function DiscussionPanel(props) {
188+
const signal = axios.CancelToken.source();
189+
190+
const [comments, setComments] = React.useState([]);
191+
const [error, setError] = React.useState(null);
192+
const [newComment, setNewComment] = React.useState({});
193+
const [waiting, setWaiting] = React.useState(false);
194+
195+
const getComments = (objId) => {
196+
return datastore
197+
.getComments(objId, signal)
198+
.then((comments) => {
199+
setComments(comments);
200+
})
201+
.catch((err) => {
202+
if (axios.isCancel(err)) {
203+
console.log("Error: ", err.message);
204+
} else {
205+
// Something went wrong. Save the error in state and re-render.
206+
this.setError(err);
207+
}
208+
});
209+
};
210+
211+
React.useEffect(() => {
212+
getComments(props.id);
213+
});
214+
215+
const canDelete = (comment) => {
216+
let deletable = comment.status === "draft";
217+
return deletable;
218+
};
219+
220+
const handleDelete = (commentId) => {
221+
datastore.deleteComment(props.id, commentId).then(() => {
222+
getComments(props.id);
223+
});
224+
};
225+
226+
const saveComment = (commentId, content, submit) => {
227+
if (commentId === "new") {
228+
console.log("Saving new comment");
229+
setWaiting(true);
230+
datastore.createComment(props.id, content, signal).then((res) => {
231+
if (submit) {
232+
datastore
233+
.updateComment(props.id, res.data.id, null, submit, signal)
234+
.then((res) => {
235+
console.log(res.data);
236+
setNewComment({});
237+
getComments(props.id);
238+
setWaiting(false);
239+
});
240+
} else {
241+
console.log(res.data);
242+
setNewComment({});
243+
getComments(props.id);
244+
setWaiting(false);
245+
}
246+
});
247+
} else {
248+
console.log("Modifying comment");
249+
datastore
250+
.updateComment(props.id, commentId, content, submit, signal)
251+
.then((res) => {
252+
console.log(res.data);
253+
getComments(props.id);
254+
});
255+
}
256+
};
257+
258+
let message = "";
259+
if (comments.length === 0) {
260+
message = (
261+
<Grid item xs={12}>
262+
<p>
263+
No-one has commented on this validation test yet. Do you have any
264+
thoughts about the reference data or about the test implementation? If
265+
so, please comment!
266+
</p>
267+
</Grid>
268+
);
269+
}
270+
return (
271+
<Grid container direction="column" spacing={1}>
272+
{message}
273+
{comments.map((comment) => (
274+
<CommentBox
275+
key={comment.id}
276+
openEditor={false}
277+
comment={comment}
278+
canDelete={canDelete(comment)}
279+
handleDelete={handleDelete}
280+
handleSave={(content, submit) => saveComment(comment.id, content, submit)}
281+
/>
282+
))}
283+
{ waiting ? <Grid item xs={12}><CircularProgress/></Grid> : "" }
284+
<CommentBox
285+
key="new"
286+
openEditor={true}
287+
comment={newComment}
288+
canDelete={false}
289+
handleDelete={null}
290+
handleSave={(content, submit) => saveComment("new", content, submit)}
291+
/>
292+
</Grid>
293+
);
294+
}
295+
296+
export default DiscussionPanel;

apps/model_catalog/src/ModelDetail.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import ModelDetailContent from "./ModelDetailContent";
2828
import ModelDetailMetadata from "./ModelDetailMetadata";
2929
import ModelResultOverview from "./ModelResultOverview";
3030
import ResultGraphs from "./ResultGraphs";
31+
import DiscussionPanel from "./DiscussionPanel";
3132

3233
// if working on the appearance/layout set globals.DevMode=true
3334
// to avoid loading the models and tests over the network every time;
@@ -302,11 +303,12 @@ class ModelDetail extends React.Component {
302303

303304
handleTabChange(event, newValue) {
304305
// 0 : Model Info
305-
// 1 : Validations
306-
// 2 : Validations -> Results
307-
// 3 : Validations -> Figures
308-
if (newValue === 1) {
309-
newValue = 2
306+
// 1 : Discussion
307+
// 2 : Validations
308+
// 3 : Validations -> Results
309+
// 4 : Validations -> Figures
310+
if (newValue === 2) {
311+
newValue = 3
310312
}
311313
this.setState({ tabValue: newValue });
312314
}
@@ -389,6 +391,10 @@ class ModelDetail extends React.Component {
389391

390392
render() {
391393
const { classes } = this.props;
394+
const emptyMessage = ("No-one has commented on this model yet. " +
395+
"Do you have any thoughts about the model or any of its implementations? " +
396+
"If so, please comment!")
397+
392398
return (
393399
<Dialog
394400
fullScreen
@@ -436,18 +442,22 @@ class ModelDetail extends React.Component {
436442
<Tab label="Info" className={this.state.tabValue === 0 ? classes.active_tabStyle : classes.default_tabStyle}
437443
style={{ opacity: 1 }} />
438444

439-
<Tab label={this.state.tabValue >= 1
445+
<Tab label="Discussion" className={this.state.tabValue === 1 ? classes.active_tabStyle : classes.default_tabStyle}
446+
style={{ opacity: 1 }} />
447+
448+
449+
<Tab label={this.state.tabValue >= 2
440450
? <div>Validations<DoubleArrowIcon style={{ verticalAlign: 'bottom', opacity: 1 }} /></div>
441451
: "Validations"}
442-
className={this.state.tabValue >= 1 ? classes.active_tabStyle : classes.default_tabStyle} />
452+
className={this.state.tabValue >= 2 ? classes.active_tabStyle : classes.default_tabStyle} />
443453

444-
{this.state.tabValue >= 1 && <Tab label="Results" className={classes.default_subTabStyle}
454+
{this.state.tabValue >= 2 && <Tab label="Results" className={classes.default_subTabStyle}
445455
style={{
446456
borderTop: "medium solid", borderTopColor: Theme.activeTabColor,
447457
borderBottom: "medium solid", borderBottomColor: Theme.activeTabColor
448458
}} />}
449459

450-
{this.state.tabValue >= 1 && <Tab label="Figures" className={classes.default_subTabStyle}
460+
{this.state.tabValue >= 2 && <Tab label="Figures" className={classes.default_subTabStyle}
451461
style={{
452462
borderTop: "medium solid", borderTopColor: Theme.activeTabColor,
453463
borderBottom: "medium solid", borderBottomColor: Theme.activeTabColor
@@ -511,16 +521,22 @@ class ModelDetail extends React.Component {
511521
</Grid>
512522
</TabPanel>
513523
<TabPanel value={this.state.tabValue} index={1}>
524+
<DiscussionPanel
525+
id={this.props.modelData.id}
526+
emptyMessage={emptyMessage}
527+
/>
514528
</TabPanel>
515529
<TabPanel value={this.state.tabValue} index={2}>
530+
</TabPanel>
531+
<TabPanel value={this.state.tabValue} index={3}>
516532
<ModelResultOverview
517533
id={this.props.modelData.id}
518534
modelJSON={this.props.modelData}
519535
results={this.state.results}
520536
loadingResult={this.state.loadingResult}
521537
/>
522538
</TabPanel>
523-
<TabPanel value={this.state.tabValue} index={3}>
539+
<TabPanel value={this.state.tabValue} index={4}>
524540
<ResultGraphs
525541
id={this.props.modelData.id}
526542
results={this.state.results}

0 commit comments

Comments
 (0)