diff --git a/targets/frontend/src/components/data/Head.tsx b/targets/frontend/src/components/data/Head.tsx new file mode 100644 index 000000000..56ce7bc71 --- /dev/null +++ b/targets/frontend/src/components/data/Head.tsx @@ -0,0 +1,40 @@ +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import TableCell from "@mui/material/TableCell"; +import Checkbox from "@mui/material/Checkbox"; +import * as React from "react"; +import { Data, HeadCell } from "./type"; + +export type EnhancedTableProps = { + readonly headCells: HeadCell[]; + numSelected: number; + onSelectAllClick: (event: React.ChangeEvent) => void; + rowCount: number; +}; + +export const EnhancedTableHead = ({ + onSelectAllClick, + numSelected, + rowCount, + headCells, +}: EnhancedTableProps) => { + return ( + + + + 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={onSelectAllClick} + /> + + {headCells.map((headCell) => ( + + {headCell.label} + + ))} + + + ); +}; diff --git a/targets/frontend/src/components/data/PublishModal/ListContent.tsx b/targets/frontend/src/components/data/PublishModal/ListContent.tsx new file mode 100644 index 000000000..d38dd7381 --- /dev/null +++ b/targets/frontend/src/components/data/PublishModal/ListContent.tsx @@ -0,0 +1,64 @@ +import { + CircularProgress, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import { Content } from "./index"; +import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; +import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; + +type ContentWithProgression = Content & { + status: "pending" | "processing" | "done" | "error"; +}; + +type ListContentProps = { + contents: ContentWithProgression[]; +}; + +const Status = ({ status }: Pick) => { + switch (status) { + case "pending": + return ; + case "processing": + return ; + case "done": + return ; + case "error": + return ; + } +}; + +export function ListContent({ contents }: ListContentProps): JSX.Element { + return ( + + + + + Titre + Progression + + + + {contents.map((row) => ( + + + {row.title} + + + + + + ))} + +
+
+ ); +} diff --git a/targets/frontend/src/components/data/PublishModal/index.tsx b/targets/frontend/src/components/data/PublishModal/index.tsx new file mode 100644 index 000000000..ae3b5ecc0 --- /dev/null +++ b/targets/frontend/src/components/data/PublishModal/index.tsx @@ -0,0 +1,151 @@ +import { Alert, Box, Button, Modal, Stack, Typography } from "@mui/material"; +import { styled } from "@mui/system"; +import { fr } from "@codegouvfr/react-dsfr"; +import { usePublishMutation } from "../../../modules/documents/components/publish.mutation"; +import { Source } from "../type"; +import { useEffect, useState } from "react"; +import { ListContent } from "./ListContent"; + +export type Content = { + id: string; + title: string; +}; + +export type PublishModalProps = { + source: Source; + contents: Content[]; + open: boolean; + onClose: () => void; + onCancel: () => void; +}; + +type ContentProgression = { + [id: string]: "pending" | "processing" | "done" | "error"; +}; + +export function PublishModal({ + contents, + open, + onClose, + onCancel, + source, +}: PublishModalProps): JSX.Element { + const publish = usePublishMutation(); + + const [contentProgression, setContentProgression] = + useState({}); + const [processing, setProcessing] = useState<"waiting" | "process" | "done">( + "waiting" + ); + + useEffect(() => { + setContentProgression( + contents.reduce((acc, item) => { + acc[item.id] = "pending"; + return acc; + }, {} as ContentProgression) + ); + }, [contents]); + + const onValidate = async () => { + setProcessing("process"); + let currentProgression = contents.reduce((acc, item) => { + acc[item.id] = "pending"; + return acc; + }, {} as ContentProgression); + for (const content of contents) { + currentProgression = { + ...currentProgression, + [content.id]: "processing", + }; + setContentProgression(currentProgression); + try { + const { data, error } = await publish({ + id: content.id, + source: source, + }); + + if (error || data === undefined) { + currentProgression = { + ...currentProgression, + [content.id]: "error", + }; + setContentProgression(currentProgression); + } else { + currentProgression = { + ...currentProgression, + [content.id]: "done", + }; + setContentProgression(currentProgression); + } + } catch (e) { + currentProgression = { + ...currentProgression, + [content.id]: "error", + }; + setContentProgression(currentProgression); + } + } + setProcessing("done"); + }; + + return ( + + + + Publication de contenu + + +

+ Vous êtes sur le point de mettre à jour les {contents.length}{" "} + contenus suivant. +

+ ({ + ...item, + status: contentProgression[item.id] ?? "pending", + }))} + /> +

+ Ces derniers seront disponible sur le site après une mise en + production des données. Êtes-vous sûr de vouloir publier ces + contenus ? +

+
+ + {processing === "waiting" && ( + + )} + {processing === "waiting" && ( + + )} + {processing === "process" && ( + + Merci de ne pas fermer cette fenêtre avant la fin de la mise à + jour. + + )} + {processing === "done" && ( + + )} + +
+
+ ); +} + +const StyledBox = styled(Box)({ + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: 800, + backgroundColor: `${fr.colors.decisions.background.default.grey.default}`, + padding: `${fr.spacing("8v")}`, +}); diff --git a/targets/frontend/src/components/data/Toolbard.tsx b/targets/frontend/src/components/data/Toolbard.tsx new file mode 100644 index 000000000..4ed668204 --- /dev/null +++ b/targets/frontend/src/components/data/Toolbard.tsx @@ -0,0 +1,79 @@ +import Toolbar from "@mui/material/Toolbar"; +import { alpha } from "@mui/material/styles"; +import Typography from "@mui/material/Typography"; +import PublishIcon from "@mui/icons-material/Publish"; +import * as React from "react"; +import { Button, FormGroup, Stack, TextField } from "@mui/material"; + +interface EnhancedTableToolbarProps { + numSelected: number; + onClickPublish: () => void; + onClickCreation: () => void; + setSearch: (value: string | undefined) => void; + customFilters?: React.ReactNode; +} + +export const EnhancedTableToolbar = ({ + numSelected, + onClickPublish, + onClickCreation, + setSearch, + customFilters = undefined, +}: EnhancedTableToolbarProps) => { + return ( + 0 && { + bgcolor: (theme) => + alpha( + theme.palette.primary.main, + theme.palette.action.activatedOpacity + ), + }, + ]} + > + {numSelected > 0 ? ( + + {numSelected} contenu{numSelected > 1 ? "s" : ""} sélectionné + {numSelected > 1 ? "s" : ""} + + ) : ( + + { + const value = event.target.value; + setSearch(value ? `%${value}%` : undefined); + }} + data-testid="list-search" + /> + {customFilters} + + )} + {numSelected > 0 ? ( + + ) : ( + + )} + + ); +}; diff --git a/targets/frontend/src/components/data/index.tsx b/targets/frontend/src/components/data/index.tsx new file mode 100644 index 000000000..e9bcb08f9 --- /dev/null +++ b/targets/frontend/src/components/data/index.tsx @@ -0,0 +1,146 @@ +import * as React from "react"; +import { useState } from "react"; +import Box from "@mui/material/Box"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableRow from "@mui/material/TableRow"; +import Checkbox from "@mui/material/Checkbox"; +import { EnhancedTableToolbar } from "./Toolbard"; +import { EnhancedTableHead } from "./Head"; +import { Data, HeadCell, Source } from "./type"; +import { PublishModal } from "./PublishModal"; + +type Props = { + source: Source; + readonly headCells: HeadCell[]; + rows: T[]; + onClickItem: (id: string) => void; + onClickCreation: () => void; + setSearch: (value: string | undefined) => void; + customFilters?: React.ReactNode; +}; + +type ItemCheck = { + [id: string]: boolean; +}; + +export const EnhancedTable = ({ + source, + headCells, + rows, + onClickItem, + onClickCreation, + setSearch, + customFilters, +}: Props) => { + const [itemsCheck, setItemCheck] = useState({}); + const [isOpen, showModal] = useState(false); + + const handleSelectAllClick = (event: React.ChangeEvent) => { + setItemCheck( + rows.reduce((acc, item) => { + acc[item.id] = event.target.checked; + return acc; + }, {} as ItemCheck) + ); + }; + + const handleItemCheck = (id: string, checked: boolean) => { + setItemCheck({ + ...itemsCheck, + [id]: checked, + }); + }; + + const handleClick = (id: string) => { + const atLeastOneChecked = Object.values(itemsCheck).some( + (checked) => checked + ); + if (atLeastOneChecked) { + handleItemCheck(id, !itemsCheck[id]); + } else { + onClickItem(id); + } + }; + + const numSelected = (): number => { + return Object.values(itemsCheck).filter((item) => item).length; + }; + + return ( + + showModal(true)} + onClickCreation={onClickCreation} + setSearch={setSearch} + customFilters={customFilters} + /> + + + + + {rows.map((row, index) => { + const isItemSelected = itemsCheck[row.id] ?? false; + const labelId = `${source}-table-checkbox-${index}`; + + return ( + + + + handleItemCheck(row.id, event.target.checked) + } + /> + + {headCells.map((head) => ( + handleClick(row.id)} + > + {head.render + ? head.render(row[head.dataIndex]) + : (row[head.dataIndex] as React.ReactNode)} + + ))} + + ); + })} + +
+
+ {isOpen && ( + value) + .map(([key, _]) => { + const find = rows.find((item) => item.id === key)!!; + return { + id: find.id, + title: find.title, + }; + })} + open={isOpen} + onCancel={() => showModal(false)} + onClose={() => showModal(false)} + /> + )} +
+ ); +}; diff --git a/targets/frontend/src/components/data/type.ts b/targets/frontend/src/components/data/type.ts new file mode 100644 index 000000000..673494ab9 --- /dev/null +++ b/targets/frontend/src/components/data/type.ts @@ -0,0 +1,18 @@ +export type Data = { + id: string; + title: string; + [key: string]: unknown; +}; + +export type HeadCell = { + id: string; + dataIndex: keyof T; + label: string; + render?: (value: T[HeadCell["dataIndex"]]) => React.ReactNode; +}; + +export type Source = + | "information" + | "modeles_de_courriers" + | "contributions" + | "conventions_collectives";