frontend/app/ObituariesList/ObituariesList.tsx (323 lines of code) (raw):

import React, { ChangeEvent, useEffect, useMemo, useState } from "react"; import { Helmet } from "react-helmet"; import { Grid, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, Typography, FormControl, Box, Button, TextField, } from "@material-ui/core"; import { useGuardianStyles } from "~/misc/utils"; import axios from "axios"; import { Link } from "react-router-dom"; import moment from "moment"; import { SortDirection } from "~/utils/lists"; import ProjectTypeDisplay from "~/common/ProjectTypeDisplay"; import { openProject, updateProjectOpenedStatus, } from "~/ProjectEntryList/helpers"; import { SystemNotifcationKind, SystemNotification, } from "@guardian/pluto-headers"; import AssetFolderLink from "~/ProjectEntryList/AssetFolderLink"; import CommissionEntryView from "../EntryViews/CommissionEntryView"; import { Autocomplete } from "@material-ui/lab"; import { isLoggedIn } from "~/utils/api"; export interface ObituaryProject { commissionId: number; created: string; deep_archive: boolean; deletable: boolean; id: number; isObitProject: string; productionOffice: string; projectTypeId: number; sensitive: boolean; status: string; title: string; updated: string; user: string; workingGroupId: number; confidential: boolean; } const tableHeaderTitles: HeaderTitle<Project>[] = [ { label: "Obituary", key: "isObitProject" }, { label: "Project", key: "title" }, { label: "Commission" }, { label: "Created", key: "created" }, { label: "Type" }, { label: "Owner" }, { label: "Action" }, { label: "Open" }, ]; const ObituariesList = () => { const classes = useGuardianStyles(); const [projects, setProjects] = useState<ObituaryProject[] | []>([]); const [page, setPage] = useState<number>(0); const [rowsPerPage, setRowsPerPage] = useState<number>(25); const [orderBy, setOrderBy] = useState<keyof Project>("isObitProject"); const [order, setOrder] = useState<SortDirection>("asc"); const [name, setName] = useState<string>(""); const [obituaryOptions, setObituaryOptions] = useState<null | string[]>(null); const [user, setUser] = useState<PlutoUser | null>(null); useEffect(() => { const fetchWhoIsLoggedIn = async () => { try { let user = await isLoggedIn(); setUser(user); } catch (error) { console.error("Could not login user:", error); } }; fetchWhoIsLoggedIn(); }, []); const fetchObituaryProjects = async () => { try { const args: { [key: string]: string | undefined } = { name: name == "" ? undefined : name.toLowerCase(), startAt: (page * rowsPerPage).toString(), limit: rowsPerPage.toString(), sort: orderBy, sortDirection: order, }; const queryString = Object.keys(args) .filter((k) => !!args[k]) .map((k) => `${k}=${encodeURIComponent(args[k] as string)}`) //typecast is safe because we already filtered out null values .join("&"); const response = await axios.get<{ result: ObituaryProject[] }>( `/api/project/obits?${queryString}` ); setProjects(response.data.result); } catch (error) { SystemNotification.open( SystemNotifcationKind.Error, `Could not load obituaries: ${error}` ); console.error({ error }); } }; useEffect(() => { fetchObituaryProjects(); }, [page, rowsPerPage, order, orderBy, name]); const handleChangePage = ( _event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, newPage: number ) => { setPage(newPage); }; const handleChangeRowsPerPage = async ( event: React.ChangeEvent<HTMLInputElement> ) => { setRowsPerPage(+event.target.value); setPage(0); }; const sortByColumn = (property: keyof Project) => ( _event: React.MouseEvent<unknown> ) => { const isAsc = orderBy === property && order === "asc"; setOrder(isAsc ? "desc" : "asc"); setOrderBy(property); }; const searchObits = useMemo(() => { const prefixString = name != "" ? `?prefix=${encodeURIComponent(name)}` : ""; return async () => { const response = await axios.get<{ obitNames: string[] }>( `/api/obits/names${prefixString}` ); return response.data.obitNames; }; }, [name]); useEffect(() => { searchObits() .then((obitNames) => { let obitNamesTitleCase: string[] = []; obitNames.map((name) => obitNamesTitleCase.push(toTitleCase(name))); setObituaryOptions(obitNamesTitleCase); }) .catch((err) => { console.error(`Could not get obituary names list: ${err}`); if (err.response) console.log(err.response.data); }); }, [name, searchObits]); const inputDidChange = (evt: ChangeEvent<{}>, newValue: string | null) => { setName(newValue ?? ""); }; const valueDidChange = (evt: ChangeEvent<{}>, newValue: string | null) => { setName(newValue ?? ""); }; function toTitleCase(str: string) { return str.replace(/\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); } const userAllowed = (confidential: Boolean, projectUser: string) => { if (confidential == false) { return true; } if (user != null) { if (user.isAdmin) { return true; } else if (projectUser.split("|").includes(user.uid)) { return true; } else { return false; } } }; return ( <> <Helmet> <title>All Obituaries</title> </Helmet> <> <Grid container> <Grid item xs={9}> <Typography className={classes.obituariesTitle}> Obituaries </Typography> </Grid> <Grid item xs={3}> <Box display="flex" justifyContent="flex-end"> <FormControl> <Autocomplete style={{ width: "100%" }} freeSolo autoComplete includeInputInList value={name} //onChange is fired when an option is selected onChange={valueDidChange} //onInputChange is fired when the user types onInputChange={inputDidChange} options={obituaryOptions ?? []} renderInput={(params) => ( <TextField {...params} label="Name Filter" style={{ width: "300px" }} /> )} /> </FormControl> </Box> </Grid> </Grid> <TableContainer elevation={3} component={Paper}> <Table className={classes.table} aria-label="simple table"> <TableHead> <TableRow> {tableHeaderTitles.map((title, index) => ( <TableCell key={title.label ? title.label : index} sortDirection={order} align={title.label == "Action" ? "right" : "left"} > {title.key ? ( <TableSortLabel active={orderBy === title.key} direction={orderBy === title.key ? order : "asc"} onClick={sortByColumn(title.key)} > {title.label} </TableSortLabel> ) : ( title.label )} </TableCell> ))} </TableRow> </TableHead> <TableBody> {projects.map((project: ObituaryProject) => { if (userAllowed(project.confidential, project.user)) { return ( <TableRow key={project.id}> <TableCell component="th" scope="row" className={classes.title_case_text} > {project.isObitProject} </TableCell> <TableCell>{project.title}</TableCell> <TableCell> <CommissionEntryView entryId={project.commissionId} /> </TableCell> <TableCell> <span className="datetime"> {moment(project.created).format("DD/MM/YYYY HH:mm")} </span> </TableCell> <TableCell> <ProjectTypeDisplay projectTypeId={project.projectTypeId} /> </TableCell> <TableCell>{project.user}</TableCell> <TableCell align="right"> <Link to={"/project/" + project.id}> Edit obituary project </Link> </TableCell> <TableCell> <Button className={classes.openProjectButton} variant="contained" color="primary" onClick={async () => { try { await openProject(project.id); } catch (error) { SystemNotification.open( SystemNotifcationKind.Error, `An error occurred when attempting to open the project.` ); console.error(error); } try { await updateProjectOpenedStatus(project.id); } catch (error) { console.error(error); } }} > Open project </Button> <AssetFolderLink projectId={project.id} onClick={(event) => { event.stopPropagation(); }} /> </TableCell> </TableRow> ); } else { return null; } })} </TableBody> </Table> </TableContainer> <TablePagination component="div" count={-1} rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} onRowsPerPageChange={handleChangeRowsPerPage} labelDisplayedRows={({ from, to }) => `${from}-${to}`} /> </> </> ); }; export default ObituariesList;