frontend/app/DeliverablesDash/SyndicationNotes.tsx (155 lines of code) (raw):

import React, { useEffect, useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import axios from "axios"; import { SystemNotifcationKind, SystemNotification, } from "@guardian/pluto-headers"; import { Grid, IconButton, Typography } from "@material-ui/core"; import { format, parseISO } from "date-fns"; import AddIcon from "@material-ui/icons/Add"; import ShowAllSelector from "./ShowAllSelector"; import clsx from "clsx"; import AddNoteDialog from "./AddNoteDialog"; interface SyndicationNotesProps { deliverableId: bigint; initialMax?: number; updateCounter?: number; //increment to trigger an update } const useStyles = makeStyles((theme) => ({ basicList: { listStyle: "none", paddingLeft: "0", }, contentBox: { fontSize: "0.9em", }, attributionBox: { fontSize: "0.8em", height: "1rem", overflow: "hidden", textAlign: "right", textOverflow: "ellipsis", fontStyle: "italic", }, collapsed: { overflow: "hidden", "-webkit-line-clamp": 2, //see https://stackoverflow.com/questions/33058004/applying-an-ellipsis-to-multiline-text, this is actually well supported "-webkit-box-orient": "vertical", display: "-webkit-box", // height: "2.3rem", // textOverflow: "ellipsis" }, headNote: { fontSize: "0.9em", overflow: "hidden", height: "1rem", }, })); const SyndicationNotes: React.FC<SyndicationNotesProps> = (props) => { const [notes, setNotes] = useState<SyndicationNote[]>([]); const [reloadCounter, setReloadCounter] = useState(0); const [showingEntry, setShowingEntry] = useState(false); const [expanded, setExpanded] = useState(false); const [totalNotes, setTotalNotes] = useState(0); const [loading, setLoading] = useState(true); const classes = useStyles(); useEffect(() => { const loadNotes = async () => { setLoading(true); try { const response = await axios.get<SyndicationNoteResponse>( `/api/asset/${props.deliverableId}/notes?limit=${ props.initialMax ?? 100 }` ); setNotes(response.data.results); setTotalNotes(response.data.count); setLoading(false); } catch (err) { setLoading(false); SystemNotification.open( SystemNotifcationKind.Error, `Could not load notes for ${props.deliverableId}` ); } }; loadNotes(); }, [ props.deliverableId, props.initialMax, reloadCounter, props.updateCounter, ]); const formatTime = (timeStr: string) => { try { const date = parseISO(timeStr); return format(date, "do MMM yy"); } catch (e) { console.error("Could not parse time string ", timeStr, ": ", e); return "(invalid)"; } }; const addNoteRequested = () => setShowingEntry(true); const closeEntryNoSave = () => setShowingEntry(false); const saveAndClose = async (newNote: string) => { try { await axios.post(`/api/asset/${props.deliverableId}/notes/new`, newNote, { headers: { "Content-Type": "text/plain" }, }); setShowingEntry(false); setReloadCounter((prev) => prev + 1); SystemNotification.open(SystemNotifcationKind.Info, "Saved note"); } catch (err) { SystemNotification.open( SystemNotifcationKind.Error, `Could not save your note: ${err}` ); } }; const notesToDisplay = expanded ? notes : notes.slice(0, 3); return ( <> <Grid container direction="row" justify="space-between" alignItems="center" > <Grid item> <Typography className={classes.headNote}> {loading ? "Loading" : `${totalNotes} notes`} </Typography> <ShowAllSelector value={expanded} onChange={(newValue) => setExpanded(newValue)} /> </Grid> <Grid item> <IconButton onClick={addNoteRequested}> <AddIcon /> </IconButton> </Grid> </Grid> <ul className={classes.basicList}> {notesToDisplay.map((note, idx) => ( <li key={idx} className={classes.basicList}> <Typography className={clsx( classes.contentBox, expanded ? undefined : classes.collapsed )} > {note.content} </Typography> <Typography className={classes.attributionBox}> {note.username} {formatTime(note.timestamp)} </Typography> </li> ))} </ul> {showingEntry ? ( <AddNoteDialog closeEntryNoSave={closeEntryNoSave} saveAndClose={saveAndClose} /> ) : undefined} </> ); }; export default SyndicationNotes;