frontend/app/DeliverableItem/DeliverableItem.tsx (443 lines of code) (raw):

import React, { useEffect, useState } from "react"; import { RouteChildrenProps } from "react-router"; import { Helmet } from "react-helmet"; import { Button, Grid, IconButton, Link, Omit, Paper, Typography, } from "@material-ui/core"; import { makeStyles } from "@material-ui/core/styles"; import axios from "axios"; import { SystemNotifcationKind, SystemNotification, } from "@guardian/pluto-headers"; import { Add, Cancel, ChevronRight, ChevronRightRounded, Edit, Movie, Refresh, } from "@material-ui/icons"; import guardianEnabled from "../static/guardian_enabled.png"; import guardianDisabled from "../static/guardian_disabled.png"; import dailymotionEnabled from "../static/dailymotion_enabled.jpg"; import dailymotionDisabled from "../static/dailymotion_disabled.jpg"; import GuardianMasterForm from "../Master/GuardianMasterForm"; import clsx from "clsx"; import YoutubeMasterForm from "../Master/YoutubeMasterForm"; import DailyMotionMasterForm from "../Master/DailyMotionMasterForm"; import MainstreamMasterForm from "../Master/MainstreamMasterForm"; import { useHistory } from "react-router-dom"; import { format, parseISO } from "date-fns"; import AddNoteDialog from "../DeliverablesDash/AddNoteDialog"; import { requestResync } from "../utils/master-api-service"; interface DeliverableItemParam { assetId: string; } import { useStyles } from "./DeliverableItemStyles"; import EmbeddableYTForm from "./EmbeddableYTForm"; import ErrorCatchingWrapper from "./ErrorCatchingWrapper"; import EmbeddableMSForm from "./EmbeddableMSForm"; import EmbeddableDMForm from "./EmbeddableDMForm"; type PlayerSizing = "S" | "M" | "L" | "X"; const DeliverableItem: React.FC<RouteChildrenProps<DeliverableItemParam>> = ( props ) => { const [deliverable, setDeliverable] = useState< DenormalisedDeliverable | undefined >(undefined); const [loading, setLoading] = useState(true); const [notes, setNotes] = useState<SyndicationNote[]>([]); const [showAddNote, setShowAddNote] = useState(false); const [reloadCounter, setReloadCounter] = useState(0); const [playerSize, setPlayerSize] = useState<PlayerSizing>("S"); const [editingMS, setEditingMS] = useState(false); const [editingDM, setEditingDM] = useState(false); const classes = useStyles(); const history = useHistory(); useEffect(() => { const loadDeliverable = async () => { if (props.match) { try { const response = await axios.get<DenormalisedDeliverable>( `/api/asset/${props.match.params.assetId}` ); setDeliverable(response.data); setLoading(false); } catch (e) { SystemNotification.open( SystemNotifcationKind.Error, `Could not load deliverable, server error ${e}` ); setLoading(false); } } else { setLoading(false); console.error( "Can't load the deliverable because `match` is not defined in `props`.", props ); } }; loadDeliverable(); }, [props.match?.params]); useEffect(() => { const loadNotes = async () => { if (props.match) { try { const response = await axios.get<SyndicationNoteResponse>( `/api/asset/${props.match.params.assetId}/notes?limit=100` ); setNotes(response.data.results ?? []); } catch (err) { SystemNotification.open( SystemNotifcationKind.Error, `Could not load syndication notes: ${err}` ); } } }; loadNotes(); }, [props.match?.params, reloadCounter]); const requestAddNote = () => setShowAddNote(true); 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 saveAndClose = async (newNote: string) => { try { await axios.post( `/api/asset/${props.match?.params.assetId}/notes/new`, newNote, { headers: { "Content-Type": "text/plain" }, } ); setShowAddNote(false); setReloadCounter((prev) => prev + 1); SystemNotification.open(SystemNotifcationKind.Info, "Saved note"); } catch (err) { SystemNotification.open( SystemNotifcationKind.Error, `Could not save your note: ${err}` ); } }; const classForSize = () => { switch (playerSize) { case "S": return classes.playerS; case "M": return classes.playerM; case "L": return classes.playerL; case "X": return classes.playerX; } }; return ( <> <Helmet> <title>Deliverable item</title> </Helmet> <Grid container direction="row" className={classes.fullWidth} spacing={3}> <Grid item style={{ flexGrow: 1 }}> {deliverable ? ( <iframe className={clsx(classes.playerFrame, classForSize())} src={`/vs/embed/player?onlineId=${deliverable.online_item_id}&nearlineId=${deliverable.nearline_item_id}&archiveId=${deliverable.archive_item_id}`} /> ) : undefined} </Grid> <Grid item> <Paper elevation={3} className={classes.basicMetadataBox}> <Typography variant="h6"> <Movie className={classes.inlineIcon} /> {deliverable?.filename} </Typography> <table> <tbody> <tr> <td>Player size</td> <td> <Grid container direction="row" spacing={1}> <Grid item> <Button variant={playerSize == "S" ? "contained" : undefined} onClick={() => setPlayerSize("S")} > S </Button> </Grid> <Grid> <Button variant={playerSize == "M" ? "contained" : undefined} onClick={() => setPlayerSize("M")} > M </Button> </Grid> <Grid> <Button variant={playerSize == "L" ? "contained" : undefined} onClick={() => setPlayerSize("L")} > L </Button> </Grid> <Grid> <Button variant={playerSize == "X" ? "contained" : undefined} onClick={() => setPlayerSize("X")} > X </Button> </Grid> </Grid> </td> </tr> <tr> <td>Deliverable type</td> <td>{deliverable?.type_string}</td> </tr> <tr> <td>Related deliverables</td> <td> {deliverable?.deliverable ? ( <Button endIcon={<ChevronRightRounded />} onClick={() => history.push( `/project/${deliverable?.deliverable.pluto_core_project_id}` ) } > Go to bundle </Button> ) : undefined} </td> </tr> <tr> <td>Atom ID</td> <td> {deliverable?.atom_id ? ( <a href={`not-implemented`}>{deliverable.atom_id}</a> ) : ( <Typography variant="caption"> not from an atom </Typography> )} </td> </tr> <tr> <td>Status</td> <td>{deliverable?.status_string}</td> </tr> <tr> <td>Duration</td> <td>{deliverable?.duration}</td> </tr> <tr> <td>Size</td> <td>{deliverable?.size}</td> </tr> <tr> <td>File last modified</td> <td>{deliverable?.modified_dt}</td> </tr> <tr> <td>Deliverable last updated</td> <td>{deliverable?.changed_dt}</td> </tr> </tbody> </table> </Paper> </Grid> </Grid> <Grid container direction="row" className={classes.fullWidth} spacing={3}> <Grid item className={classes.metaPanel}> <Paper elevation={3} className={classes.basicMetadataBox}> <Grid container justifyContent="space-between"> <Grid item> <Typography variant="h6">Notes</Typography> </Grid> <Grid item> <IconButton onClick={requestAddNote}> <Add /> </IconButton> </Grid> </Grid> {notes.length === 0 ? ( <Typography variant="caption"> No notes present on this item </Typography> ) : ( <Typography variant="caption">{notes.length} notes</Typography> )} <ul style={{ overflowY: "auto", paddingRight: "1em", paddingLeft: "30px", }} > {notes.map((note, idx) => ( <li key={idx}> <Typography>{note.content}</Typography> <Typography className={classes.attributionBox}> {note.username} {formatTime(note.timestamp)} </Typography> </li> ))} </ul> </Paper> </Grid> <Grid item className={classes.metaPanel}> <Paper elevation={3} className={classes.basicMetadataBox}> <Grid container justifyContent="space-between"> <Grid item> <Typography variant="h6"> <img className={clsx(classes.inlineIcon, classes.sizedIcon)} src={ deliverable?.gnm_website_master ? guardianEnabled : guardianDisabled } /> GNM Website </Typography> </Grid> </Grid> <Grid container direction="column" spacing={1} style={{ marginTop: "12px" }} > {" "} {/* marginTop makes up for the lack of an icon button*/} {deliverable?.gnm_website_master ? ( <Grid item> <GuardianMasterForm master={deliverable.gnm_website_master} isReadOnly={true} isEditing={false} isDirty={false} onCommonMasterChanged={(evt, f) => {}} fieldChanged={(evt, f) => {}} /> </Grid> ) : ( <> <Grid item> <Typography variant="caption"> No GNM website data available for this item. This means that it has not been published to the website, which must be done through the Media Atom tool at{" "} <Link href="https://video.gutools.co.uk"> https://video.gutools.co.uk </Link> . </Typography> </Grid> <Grid item> <Typography variant="caption"> Please email multimediatech at theguardian.com for more information. </Typography> </Grid> </> )} {deliverable && deliverable.deliverable ? ( <Grid item> <Button onClick={() => requestResync( deliverable.deliverable.pluto_core_project_id.toString(), deliverable.id.toString() ) } startIcon={<Refresh />} > Resync </Button> </Grid> ) : undefined} </Grid> </Paper> </Grid> <Grid item className={classes.metaPanel}> <ErrorCatchingWrapper> {deliverable ? ( <EmbeddableYTForm content={deliverable?.youtube_master} deliverableId={deliverable?.id} bundleId={deliverable?.deliverable.pluto_core_project_id} didUpdate={(newValue) => setDeliverable((prevValue) => Object.assign({}, prevValue, { youtube_master: newValue }) ) } /> ) : undefined} </ErrorCatchingWrapper> </Grid> <Grid item className={classes.metaPanel}> <ErrorCatchingWrapper> {deliverable ? ( <EmbeddableMSForm content={deliverable?.mainstream_master} deliverableId={deliverable?.id} bundleId={deliverable?.deliverable.pluto_core_project_id} copySource={deliverable?.youtube_master} didUpdate={(newValue) => setDeliverable((prevValue) => Object.assign({}, prevValue, { mainstream_master: newValue, }) ) } /> ) : undefined} </ErrorCatchingWrapper> </Grid> <Grid item className={classes.metaPanel}> <ErrorCatchingWrapper> {deliverable ? ( <EmbeddableDMForm content={deliverable?.DailyMotion_master} deliverableId={deliverable?.id} bundleId={deliverable?.deliverable.pluto_core_project_id} copySource={deliverable?.youtube_master} didUpdate={(newValue) => setDeliverable((prevValue) => Object.assign({}, prevValue, { DailyMotion_master: newValue, }) ) } /> ) : undefined} </ErrorCatchingWrapper> </Grid> </Grid> {showAddNote ? ( <AddNoteDialog closeEntryNoSave={() => setShowAddNote(false)} saveAndClose={saveAndClose} /> ) : undefined} </> ); }; export default DeliverableItem;