frontend/app/ProjectsListComponent.tsx (233 lines of code) (raw):

import React, { useEffect, useState } from "react"; import axios from "axios"; import { Button, IconButton, makeStyles, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, Typography, Tooltip, Dialog, DialogActions, DialogContent, DialogContentText, Grid, CircularProgress, } from "@material-ui/core"; import DeleteIcon from "@material-ui/icons/Delete"; import EditIcon from "@material-ui/icons/Edit"; import { RouteComponentProps, useHistory, useLocation, useParams, } from "react-router-dom"; import HelpIcon from "@material-ui/icons/Help"; import BundleInfoComponent from "./BundleList/BundleInfoComponent"; interface HeaderTitles { label: string; key?: keyof Project; } declare var deploymentRootPath: string; const tableHeaderTitles: HeaderTitles[] = [ { label: "Project title", key: "name" }, { label: "Project ID", key: "pluto_core_project_id" }, { label: "Created", key: "created" }, { label: "Open" }, ]; interface ProjectsListProps {} interface ProjectsListState { loading?: boolean; lastError?: object | null; projectsList?: Array<object>; } const useStyles = makeStyles({ table: { maxWidth: "100%", }, infoIcon: { display: "flex", marginLeft: "auto", marginBottom: "0.625rem", }, visuallyHidden: { border: 0, clip: "rect(0 0 0 0)", height: 1, margin: -1, overflow: "hidden", padding: 0, position: "absolute", top: 20, width: 1, }, }); const pageSizeOptions = [2, 25, 50, 100]; /* */ type SortDirection = "asc" | "desc"; const ActionIcons: React.FC<{ id: string }> = (props) => ( <span className="icons"> <IconButton href={`${deploymentRootPath}project/${props.id}`}> <EditIcon /> </IconButton> </span> ); const ProjectsListComponent: React.FC<RouteComponentProps> = () => { // React Router const history = useHistory(); const { search } = useLocation(); // React state const [order, setOrder] = useState<SortDirection>("desc"); const [orderBy, setOrderBy] = useState<keyof Project>("created"); const [projects, setProjects] = useState<Project[]>([]); const [loading, setLoading] = useState<boolean>(false); const [lastError, setLastError] = useState<object | null>(null); const [openDialog, setOpenDialog] = useState<boolean>(false); const [rowsPerPage, setRowsPerPage] = useState<number>(50); const [page, setPage] = useState<number>(0); // Material-UI const classes = useStyles(); 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 fetchProjectsOnPage = async () => { await setLoading(true); try { const server_response = await axios.get( `/api/bundle?p=${page}&pageSize=${rowsPerPage}&sortBy=${orderBy}&sortOrder=${order}` ); return Promise.all([ setProjects(server_response.data), setLoading(false), setLastError(null), ]); } catch (error) { return Promise.all([setLastError(error), setLoading(false)]); } }; useEffect(() => { fetchProjectsOnPage(); }, []); //empty array => call on component startup not modify useEffect(() => { console.log("filter or search changed, updating..."); fetchProjectsOnPage(); }, [page, rowsPerPage, order, orderBy]); const closeDialog = () => { setOpenDialog(false); }; const sortByColumn = (property: keyof Project) => ( _event: React.MouseEvent<unknown> ) => { const isAsc = orderBy === property && order === "asc"; setOrder(isAsc ? "desc" : "asc"); setOrderBy(property); }; return ( <> <Grid container justifyContent="space-between" alignItems="flex-end"> <Grid item> <h2>Deliverables</h2> </Grid> <Grid item> {loading ? <CircularProgress /> : null} <Tooltip className={classes.infoIcon} title="How do I create deliverables?" > <IconButton onClick={(event) => { event.stopPropagation(); setOpenDialog(true); }} > <HelpIcon /> </IconButton> </Tooltip> </Grid> </Grid> <Paper elevation={3}> <TableContainer> <Table className={classes.table}> <TableHead> <TableRow> {tableHeaderTitles.map((title, idx) => ( <TableCell key={title.label ? title.label : idx} sortDirection={order} > {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((entry, idx) => ( <TableRow key={idx}> <TableCell style={{ maxWidth: "33%" }}> <BundleInfoComponent bundleName={entry.name} projectId={entry.pluto_core_project_id} commissionId={entry.commission_id} /> </TableCell> <TableCell>{entry.pluto_core_project_id}</TableCell> <TableCell>{entry.created}</TableCell> <TableCell> <ActionIcons id={entry.pluto_core_project_id.toString()} /> </TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer> <TablePagination rowsPerPageOptions={pageSizeOptions} component="div" count={-1} rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} onChangeRowsPerPage={handleChangeRowsPerPage} labelDisplayedRows={({ from, to }) => `${from}-${to}`} /> </Paper> <Dialog open={openDialog} onClose={closeDialog} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" > <DialogContent> <DialogContentText id="alert-dialog-description"> All deliverables must be associated with the project that created them. In order to create deliverables, click{" "} <a href="/pluto-core/project/">Projects</a> and find the project you want to create deliverables for. Then, in the lower half of the project screen click the button marked "Create Deliverables" or "View Deliverables". This will create and open a deliverables list which you can add to directly. </DialogContentText> </DialogContent> <DialogActions> <Button onClick={closeDialog}>Close</Button> </DialogActions> </Dialog> </> ); }; export default ProjectsListComponent;