From a4606f95f5cf5228d86f0cbb14ca0f4306be1d6d Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 8 Jul 2025 11:09:41 +0200 Subject: [PATCH] Add checkboxes for documents table Helps #167 Signed-off-by: Sacha Al Himdani --- .../organizations/documents/DocumentsPage.tsx | 81 +++++++++++++++---- packages/hooks/src/index.ts | 1 + packages/hooks/src/useList.ts | 25 ++++++ .../src/Atoms/Checkbox/Checkbox.stories.tsx | 12 +++ packages/ui/src/Atoms/Checkbox/Checkbox.tsx | 37 +++++++++ packages/ui/src/Atoms/Table/Table.tsx | 9 ++- packages/ui/src/index.ts | 1 + 7 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 packages/hooks/src/useList.ts create mode 100644 packages/ui/src/Atoms/Checkbox/Checkbox.stories.tsx create mode 100644 packages/ui/src/Atoms/Checkbox/Checkbox.tsx diff --git a/apps/console/src/pages/organizations/documents/DocumentsPage.tsx b/apps/console/src/pages/organizations/documents/DocumentsPage.tsx index eeb19a5b3..c83286ee5 100644 --- a/apps/console/src/pages/organizations/documents/DocumentsPage.tsx +++ b/apps/console/src/pages/organizations/documents/DocumentsPage.tsx @@ -15,6 +15,10 @@ import { ActionDropdown, DropdownItem, IconBell2, + Checkbox, + IconCrossLargeX, + IconSignature, + IconCheckmark1, } from "@probo/ui"; import { useFragment, @@ -30,7 +34,7 @@ import { useSendSigningNotificationsMutation, } from "/hooks/graph/DocumentGraph"; import type { DocumentsPageListFragment$key } from "./__generated__/DocumentsPageListFragment.graphql"; -import { usePageTitle } from "@probo/hooks"; +import { useList, usePageTitle } from "@probo/hooks"; import { sprintf, getDocumentTypeLabel } from "@probo/helpers"; import { CreateDocumentDialog } from "./dialogs/CreateDocumentDialog"; import type { DocumentsPageRowFragment$key } from "./__generated__/DocumentsPageRowFragment.graphql"; @@ -76,16 +80,19 @@ export default function DocumentsPage(props: Props) { const organization = usePreloadedQuery( documentsQuery, - props.queryRef + props.queryRef, ).organization; const pagination = usePaginationFragment( documentsFragment, - organization as DocumentsPageListFragment$key + organization as DocumentsPageListFragment$key, ); const documents = pagination.data.documents.edges.map((edge) => edge.node); const connectionId = pagination.data.documents.__id; const [sendSigningNotifications] = useSendSigningNotificationsMutation(); + const { list: selection, toggle, clear, reset } = useList([]); + + // TODO : Add mutation to handle publishing multiple documents and request multiple signatures usePageTitle(__("Documents")); @@ -119,19 +126,52 @@ export default function DocumentsPage(props: Props) { - - {__("Name")} - {__("Status")} - {__("Type")} - {__("Owner")} - {__("Last update")} - {__("Signatures")} - - + {selection.length === 0 ? ( + + + reset(documents.map((d) => d.id))} + /> + + {__("Name")} + {__("Status")} + {__("Type")} + {__("Owner")} + {__("Last update")} + {__("Signatures")} + + + ) : ( + + +
+
+ {sprintf(__("%s documents selected"), selection.length)} - + +
+
+ + +
+
+ + + )} {documents.map((document) => ( toggle(document.id)} key={document.id} document={document} organizationId={organization.id} @@ -178,21 +218,25 @@ function DocumentRow({ document: documentKey, organizationId, connectionId, + checked, + onCheck, }: { document: DocumentsPageRowFragment$key; organizationId: string; connectionId: string; + checked: boolean; + onCheck: () => void; }) { const document = useFragment( rowFragment, - documentKey + documentKey, ); const lastVersion = document.versions.edges[0].node; const isDraft = lastVersion.status === "DRAFT"; const { __, dateFormat } = useTranslate(); const signatures = lastVersion.signatures.edges.map((edge) => edge.node); const signedCount = signatures.filter( - (signature) => signature.state === "SIGNED" + (signature) => signature.state === "SIGNED", ).length; const [deleteDocument] = useDeleteDocumentMutation(); const confirm = useConfirm(); @@ -209,16 +253,19 @@ function DocumentRow({ { message: sprintf( __( - 'This will permanently delete the document "%s". This action cannot be undone.' + 'This will permanently delete the document "%s". This action cannot be undone.', ), - document.title + document.title, ), - } + }, ); }; return ( + + +
(items: T[]) { + const [list, setList] = useState(items); + const push = useCallback( + (item: T) => setList((prev) => [...prev, item]), + [], + ); + const remove = useCallback( + (item: T) => setList((prev) => prev.filter((i) => i !== item)), + [], + ); + const toggle = useCallback( + (item: T) => + setList((prev) => + prev.includes(item) + ? prev.filter((i) => i !== item) + : [...prev, item], + ), + [], + ); + const reset = useCallback((items: T[]) => setList(items), []); + const clear = useCallback(() => setList([]), []); + return { list, push, remove, toggle, reset, clear }; +} diff --git a/packages/ui/src/Atoms/Checkbox/Checkbox.stories.tsx b/packages/ui/src/Atoms/Checkbox/Checkbox.stories.tsx new file mode 100644 index 000000000..7758b76ba --- /dev/null +++ b/packages/ui/src/Atoms/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,12 @@ +import { Checkbox } from "./Checkbox"; +import type { Meta, StoryObj } from "@storybook/react"; + +export default { + title: "Atoms/Checkbox", + component: Checkbox, + argTypes: {}, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/ui/src/Atoms/Checkbox/Checkbox.tsx b/packages/ui/src/Atoms/Checkbox/Checkbox.tsx new file mode 100644 index 000000000..cd62314d4 --- /dev/null +++ b/packages/ui/src/Atoms/Checkbox/Checkbox.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { tv } from "tailwind-variants"; +import { IconCheckmark1 } from "../Icons"; + +type Props = { + checked: boolean; + onChange: (checked: boolean) => void; +}; + +const checkbox = tv({ + base: "size-4 border border-border-mid relative rounded-sm flex items-center justify-center", + variants: { + isFocused: { + true: "shadow shadow-focus", + }, + checked: { + true: "bg-accent text-invert", + }, + }, +}); + +export function Checkbox({ checked, onChange }: Props) { + const [isFocused, setFocus] = useState(false); + return ( +
+ onChange(e.target.checked)} + onFocus={() => setFocus(true)} + onBlur={() => setFocus(false)} + /> + {checked && } +
+ ); +} diff --git a/packages/ui/src/Atoms/Table/Table.tsx b/packages/ui/src/Atoms/Table/Table.tsx index bd7e3dd54..d1617e0e6 100644 --- a/packages/ui/src/Atoms/Table/Table.tsx +++ b/packages/ui/src/Atoms/Table/Table.tsx @@ -5,6 +5,7 @@ import { type HTMLAttributes, type PropsWithChildren, type ReactNode, + type ThHTMLAttributes, } from "react"; import { Card } from "../Card/Card"; import { Link } from "react-router"; @@ -34,9 +35,15 @@ export function Th({ children, className, width, -}: PropsWithChildren<{ className?: string; width?: number }>) { + ...props +}: { + className?: string; + width?: number; + colspan?: number; +} & ThHTMLAttributes) { return (