Add document archiving

Documents can be archived and unarchived. Archived documents are
read-only, excluded from the trust center, and moved to a dedicated
Archived tab in the document list.

- Add archived_at timestamp and status (ACTIVE/ARCHIVED) PG enum column
- Rename DocumentStatus → DocumentVersionStatus, introduce DocumentStatus
- Archive/unarchive mutations in GraphQL, MCP, and CLI
- Bulk archive/unarchive mutations with Active/Archived tabs in the list
- ABAC policies: write actions denied on archived docs, unarchive denied
  on active docs
- Remove control/risk mappings and reset trust center visibility on archive
- Exclude archived documents from mapping dialogs and trust center tab

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-03-19 14:15:16 +01:00
parent 2e12c11c0c
commit 1db8e7133e
29 changed files with 1194 additions and 199 deletions

View File

@@ -52,6 +52,7 @@ const documentsFragment = graphql`
last: $last
before: $before
orderBy: $order
filter: { status: [ACTIVE] }
) @connection(key: "LinkedDocumentsDialogQuery_documents") {
edges {
node {

View File

@@ -61,7 +61,7 @@ const bulkDeleteDocumentsMutation = graphql`
$input: BulkDeleteDocumentsInput!
) {
bulkDeleteDocuments(input: $input) {
deletedDocumentIds @deleteRecord
deletedDocumentIds
}
}
`;

View File

@@ -12,7 +12,7 @@ const fragment = graphql`
compliancePage: trustCenter @required(action: THROW) {
...CompliancePageDocumentListItem_compliancePageFragment
}
documents(first: 100) {
documents(first: 100 filter: { status: [ACTIVE] }) {
edges {
node {
id

View File

@@ -36,6 +36,7 @@ export const documentLayoutQuery = graphql`
... on Document {
id
title
status
canPublish: permission(action: "core:document-version:publish")
controlInfo: controls(first: 0) {
totalCount

View File

@@ -5,8 +5,10 @@ import {
IconBell2,
IconPlusLarge,
PageHeader,
TabItem,
Tabs,
} from "@probo/ui";
import { useMemo, useState } from "react";
import { useState } from "react";
import {
type PreloadedQuery,
usePreloadedQuery,
@@ -29,15 +31,6 @@ export const documentsPageQuery = graphql`
... on Organization {
canCreateDocument: permission(action: "core:document:create")
...DocumentListFragment @arguments(first: 50, order: { field: TITLE, direction: ASC })
allDocuments: documents(first: 50, orderBy: { field: TITLE, direction: ASC }) {
edges {
node {
canSendSigningNotifications: permission(
action: "core:document:send-signing-notifications"
)
}
}
}
}
}
}
@@ -63,25 +56,16 @@ export default function DocumentsPage(props: {
usePageTitle(__("Documents"));
const canSendAnySignatureNotifications = organization.allDocuments.edges.some(
({ node: { canSendSigningNotifications } }) => canSendSigningNotifications,
const [canSendAnySignatureNotifications, setCanSendAnySignatureNotifications] = useState(false);
const [tab, setTab] = useState<"ACTIVE" | "ARCHIVED">("ACTIVE");
const [documentListConnectionId, setDocumentListConnectionId] = useState(
ConnectionHandler.getConnectionID(
organizationId,
"DocumentsListQuery_documents",
{ orderBy: { direction: "ASC", field: "TITLE" } },
),
);
const unfilteredConnectionId = useMemo(
() =>
ConnectionHandler.getConnectionID(
organizationId,
"DocumentsListQuery_documents",
{
orderBy: { direction: "ASC", field: "TITLE" },
filter: { documentTypes: null },
},
),
[organizationId],
);
const [, setDocumentListConnectionId] = useState(unfilteredConnectionId);
const handleSendSigningNotifications = async () => {
await sendSigningNotifications({
variables: {
@@ -106,9 +90,9 @@ export default function DocumentsPage(props: {
{__("Send signing notifications")}
</Button>
)}
{organization.canCreateDocument && (
{organization.canCreateDocument && tab === "ACTIVE" && (
<CreateDocumentDialog
connection={unfilteredConnectionId}
connection={documentListConnectionId}
trigger={
<Button icon={IconPlusLarge}>{__("New document")}</Button>
}
@@ -116,9 +100,19 @@ export default function DocumentsPage(props: {
)}
</div>
</PageHeader>
<Tabs>
<TabItem active={tab === "ACTIVE"} onClick={() => setTab("ACTIVE")}>
{__("Active")}
</TabItem>
<TabItem active={tab === "ARCHIVED"} onClick={() => setTab("ARCHIVED")}>
{__("Archived")}
</TabItem>
</Tabs>
<DocumentList
fKey={organization}
onConnectionIdChange={setDocumentListConnectionId}
onCanSendNotificationsChange={setCanSendAnySignatureNotifications}
tab={tab}
/>
</div>
);

View File

@@ -39,6 +39,12 @@ const createDocumentMutation = graphql`
documentEdge @prependEdge(connections: $connections) {
node {
id
canUpdate: permission(action: "core:document:update")
canDelete: permission(action: "core:document:delete")
canRequestSignatures: permission(action: "core:document-version:request-signature")
canArchive: permission(action: "core:document:archive")
canUnarchive: permission(action: "core:document:unarchive")
canSendSigningNotifications: permission(action: "core:document:send-signing-notifications")
...DocumentListItemFragment
}
}

View File

@@ -1,12 +1,14 @@
import { sprintf } from "@probo/helpers";
import { formatError, sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { ActionDropdown, DropdownItem, IconArrowDown, IconPencil, IconTrashCan, useConfirm } from "@probo/ui";
import { ActionDropdown, DropdownItem, IconArchive, IconArrowDown, IconPencil, IconTrashCan, useConfirm, useToast } from "@probo/ui";
import { use, useRef } from "react";
import { useFragment } from "react-relay";
import { useFragment, useMutation } from "react-relay";
import { useNavigate, useParams } from "react-router";
import { ConnectionHandler, graphql } from "relay-runtime";
import type { DocumentActionsDropdown_archiveMutation } from "#/__generated__/core/DocumentActionsDropdown_archiveMutation.graphql";
import type { DocumentActionsDropdown_documentFragment$key } from "#/__generated__/core/DocumentActionsDropdown_documentFragment.graphql";
import type { DocumentActionsDropdown_unarchiveMutation } from "#/__generated__/core/DocumentActionsDropdown_unarchiveMutation.graphql";
import type { DocumentActionsDropdown_versionFragment$key } from "#/__generated__/core/DocumentActionsDropdown_versionFragment.graphql";
import type { DocumentActionsDropdownn_exportVersionMutation } from "#/__generated__/core/DocumentActionsDropdownn_exportVersionMutation.graphql";
import { PdfDownloadDialog, type PdfDownloadDialogRef } from "#/components/documents/PdfDownloadDialog";
@@ -21,7 +23,10 @@ const documentFragment = graphql`
fragment DocumentActionsDropdown_documentFragment on Document {
id
title
status
canUpdate: permission(action: "core:document:update")
canArchive: permission(action: "core:document:archive")
canUnarchive: permission(action: "core:document:unarchive")
canDelete: permission(action: "core:document:delete")
versions(first: 20) {
__id
@@ -31,6 +36,42 @@ const documentFragment = graphql`
}
`;
const archiveDocumentMutation = graphql`
mutation DocumentActionsDropdown_archiveMutation(
$input: ArchiveDocumentInput!
) {
archiveDocument(input: $input) {
document {
id
status
archivedAt
canUpdate: permission(action: "core:document:update")
canArchive: permission(action: "core:document:archive")
canUnarchive: permission(action: "core:document:unarchive")
canDelete: permission(action: "core:document:delete")
}
}
}
`;
const unarchiveDocumentMutation = graphql`
mutation DocumentActionsDropdown_unarchiveMutation(
$input: UnarchiveDocumentInput!
) {
unarchiveDocument(input: $input) {
document {
id
status
archivedAt
canUpdate: permission(action: "core:document:update")
canArchive: permission(action: "core:document:archive")
canUnarchive: permission(action: "core:document:unarchive")
canDelete: permission(action: "core:document:delete")
}
}
}
`;
const versionFragment = graphql`
fragment DocumentActionsDropdown_versionFragment on DocumentVersion {
id
@@ -65,6 +106,7 @@ export function DocumentActionsDropdownn(props: {
const updateDialogRef = useRef<{ open: () => void }>(null);
const pdfDownloadDialogRef = useRef<PdfDownloadDialogRef>(null);
const confirm = useConfirm();
const { toast } = useToast();
const document = useFragment<DocumentActionsDropdown_documentFragment$key>(documentFragment, documentFragmentRef);
const version = useFragment<DocumentActionsDropdown_versionFragment$key>(versionFragment, versionFragmentRef);
@@ -72,6 +114,10 @@ export function DocumentActionsDropdownn(props: {
const isDraft = version.status === "DRAFT";
const [deleteDocument, isDeleting] = useDeleteDocumentMutation();
const [archiveDocument, isArchiving]
= useMutation<DocumentActionsDropdown_archiveMutation>(archiveDocumentMutation);
const [unarchiveDocument, isUnarchiving]
= useMutation<DocumentActionsDropdown_unarchiveMutation>(unarchiveDocumentMutation);
const [deleteDraftDocumentVersion, isDeletingDraft]
= useDeleteDraftDocumentVersionMutation();
const [exportDocumentVersion, isExporting]
@@ -83,6 +129,53 @@ export function DocumentActionsDropdownn(props: {
},
);
const handleArchive = () => {
confirm(
() =>
new Promise<void>((resolve) => {
archiveDocument({
variables: { input: { documentId: document.id } },
onCompleted(_, errors) {
if (errors?.length) {
toast({ title: __("Error"), description: formatError(__("Failed to archive document"), errors), variant: "error" });
} else {
toast({ title: __("Success"), description: __("Document archived successfully."), variant: "success" });
}
resolve();
},
onError(error) {
toast({ title: __("Error"), description: error.message, variant: "error" });
resolve();
},
});
}),
{
message: sprintf(
__("This will archive the document \"%s\". It will no longer be editable."),
document.title,
),
variant: "danger",
label: __("Archive"),
},
);
};
const handleUnarchive = () => {
unarchiveDocument({
variables: { input: { documentId: document.id } },
onCompleted(_, errors) {
if (errors?.length) {
toast({ title: __("Error"), description: formatError(__("Failed to unarchive document"), errors), variant: "error" });
} else {
toast({ title: __("Success"), description: __("Document unarchived successfully."), variant: "success" });
}
},
onError(error) {
toast({ title: __("Error"), description: error.message, variant: "error" });
},
});
};
const handleDelete = () => {
const connectionId = ConnectionHandler.getConnectionID(
organizationId,
@@ -214,6 +307,24 @@ export function DocumentActionsDropdownn(props: {
>
{__("Download PDF")}
</DropdownItem>
{document.canArchive && (
<DropdownItem
icon={IconArchive}
disabled={isArchiving}
onClick={handleArchive}
>
{__("Archive document")}
</DropdownItem>
)}
{document.canUnarchive && (
<DropdownItem
icon={IconArchive}
disabled={isUnarchiving}
onClick={handleUnarchive}
>
{__("Unarchive document")}
</DropdownItem>
)}
{document.canDelete && (
<DropdownItem
variant="danger"

View File

@@ -21,6 +21,8 @@ const documentFragment = graphql`
fragment DocumentLayoutDrawer_documentFragment on Document {
id
documentType
status
archivedAt
canUpdate: permission(action: "core:document:update")
approvers(first: 100) {
edges {
@@ -87,6 +89,7 @@ export function DocumentLayoutDrawer(props: {
const version = useFragment<DocumentLayoutDrawer_versionFragment$key>(versionFragment, versionFragmentRef);
const isDraft = version.status === "DRAFT";
const canEdit = document.canUpdate;
const approvers = document.approvers.edges.map(e => e.node);
@@ -184,7 +187,7 @@ export function DocumentLayoutDrawer(props: {
: (
<ReadOnlyPropertyContent
onEdit={() => setIsEditingApprover(true)}
canEdit={document.canUpdate}
canEdit={canEdit}
>
<div className="flex flex-wrap gap-2">
{approvers.map(approver => (
@@ -220,7 +223,7 @@ export function DocumentLayoutDrawer(props: {
: (
<ReadOnlyPropertyContent
onEdit={() => setIsEditingType(true)}
canEdit={document.canUpdate}
canEdit={canEdit}
>
<div className="text-sm text-txt-secondary">
{getDocumentTypeLabel(__, document.documentType)}
@@ -251,7 +254,7 @@ export function DocumentLayoutDrawer(props: {
: (
<ReadOnlyPropertyContent
onEdit={() => setIsEditingClassification(true)}
canEdit={document.canUpdate}
canEdit={canEdit}
>
<div className="text-sm text-txt-secondary">
{getDocumentClassificationLabel(
@@ -288,6 +291,13 @@ export function DocumentLayoutDrawer(props: {
</div>
</PropertyRow>
)}
{document.archivedAt && (
<PropertyRow label={__("Archived on")}>
<Badge variant="danger" size="md" className="gap-2">
{formatDate(document.archivedAt)}
</Badge>
</PropertyRow>
)}
</Drawer>
);
}

View File

@@ -1,16 +1,19 @@
import { documentTypes, getDocumentTypeLabel, sprintf } from "@probo/helpers";
import { useList } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, Card, Checkbox, IconArrowDown, IconCheckmark1, IconCrossLargeX, IconSignature, IconTrashCan, Option, Select, Tbody, Th, Thead, Tr, useConfirm } from "@probo/ui";
import { type ComponentProps, use, useRef, useState, useTransition } from "react";
import { Button, Card, Checkbox, IconArchive, IconArrowDown, IconCheckmark1, IconCrossLargeX, IconSignature, IconTrashCan, Option, Select, Tbody, Th, Thead, Tr, useConfirm } from "@probo/ui";
import { type ComponentProps, use, useEffect, useRef, useState, useTransition } from "react";
import { usePaginationFragment } from "react-relay";
import { ConnectionHandler, graphql } from "relay-runtime";
import type { DocumentListBulkArchiveMutation } from "#/__generated__/core/DocumentListBulkArchiveMutation.graphql";
import type { DocumentListBulkUnarchiveMutation } from "#/__generated__/core/DocumentListBulkUnarchiveMutation.graphql";
import type { DocumentListFragment$key } from "#/__generated__/core/DocumentListFragment.graphql";
import type { DocumentsListQuery, DocumentType } from "#/__generated__/core/DocumentsListQuery.graphql";
import type { DocumentType, DocumentsListQuery } from "#/__generated__/core/DocumentsListQuery.graphql";
import { BulkExportDialog, type BulkExportDialogRef } from "#/components/documents/BulkExportDialog";
import { type Order, SortableTable, SortableTh } from "#/components/SortableTable";
import { useBulkDeleteDocumentsMutation, useBulkExportDocumentsMutation } from "#/hooks/graph/DocumentGraph";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { CurrentUser } from "#/providers/CurrentUser";
@@ -30,6 +33,7 @@ const fragment = graphql`
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
status: { type: "[DocumentStatus!]", defaultValue: [ACTIVE] }
documentTypes: { type: "[DocumentType!]", defaultValue: null }
) {
documents(
@@ -38,7 +42,7 @@ const fragment = graphql`
last: $last
before: $before
orderBy: $order
filter: { documentTypes: $documentTypes }
filter: { status: $status documentTypes: $documentTypes }
) @connection(key: "DocumentsListQuery_documents" filters: ["orderBy", "filter"]) {
__id
edges {
@@ -49,6 +53,11 @@ const fragment = graphql`
canRequestSignatures: permission(
action: "core:document-version:request-signature"
)
canArchive: permission(action: "core:document:archive")
canUnarchive: permission(action: "core:document:unarchive")
canSendSigningNotifications: permission(
action: "core:document:send-signing-notifications"
)
...DocumentListItemFragment
}
}
@@ -56,11 +65,43 @@ const fragment = graphql`
}
`;
const bulkArchiveMutation = graphql`
mutation DocumentListBulkArchiveMutation($input: BulkArchiveDocumentsInput!) {
bulkArchiveDocuments(input: $input) {
documents {
id
status
archivedAt
canUpdate: permission(action: "core:document:update")
canArchive: permission(action: "core:document:archive")
canUnarchive: permission(action: "core:document:unarchive")
}
}
}
`;
const bulkUnarchiveMutation = graphql`
mutation DocumentListBulkUnarchiveMutation($input: BulkUnarchiveDocumentsInput!) {
bulkUnarchiveDocuments(input: $input) {
documents {
id
status
archivedAt
canUpdate: permission(action: "core:document:update")
canArchive: permission(action: "core:document:archive")
canUnarchive: permission(action: "core:document:unarchive")
}
}
}
`;
export function DocumentList(props: {
fKey: DocumentListFragment$key;
onConnectionIdChange: (connectionId: string) => void;
onCanSendNotificationsChange?: (can: boolean) => void;
tab: "ACTIVE" | "ARCHIVED";
}) {
const { fKey, onConnectionIdChange } = props;
const { fKey, onConnectionIdChange, onCanSendNotificationsChange, tab } = props;
const organizationId = useOrganizationId();
const { email: defaultEmail } = use(CurrentUser);
@@ -72,17 +113,48 @@ export function DocumentList(props: {
fKey,
);
const documents = pagination.data.documents.edges
.map(({ node }) => node);
const [documentTypeFilter, setDocumentTypeFilter] = useState<DocumentType | null>(null);
const [isPending, startTransition] = useTransition();
const refetch = pagination.refetch;
useEffect(() => {
refetch(
{ status: [tab], documentTypes: documentTypeFilter ? [documentTypeFilter] : null },
{ fetchPolicy: "store-and-network" },
);
}, [tab, refetch]);
const documents = pagination.data.documents.edges.map(({ node }) => node);
const connectionId = pagination.data.documents.__id;
const [bulkDeleteDocuments] = useBulkDeleteDocumentsMutation();
const [bulkExportDocuments, isBulkExporting]
= useBulkExportDocumentsMutation();
const [bulkExportDocuments, isBulkExporting] = useBulkExportDocumentsMutation();
const [bulkArchiveDocuments, isBulkArchiving] = useMutationWithToasts<DocumentListBulkArchiveMutation>(
bulkArchiveMutation,
{ successMessage: __("Documents archived successfully."), errorMessage: __("Failed to archive documents") },
);
const [bulkUnarchiveDocuments, isBulkUnarchiving] = useMutationWithToasts<DocumentListBulkUnarchiveMutation>(
bulkUnarchiveMutation,
{ successMessage: __("Documents unarchived successfully."), errorMessage: __("Failed to unarchive documents") },
);
const { list: selection, toggle, clear, reset } = useList<string>([]);
const confirm = useConfirm();
const [isPending, startTransition] = useTransition();
const [documentTypeFilter, setDocumentTypeFilter] = useState<DocumentType | null>(null);
const canDeleteAny = documents.some(({ canDelete }) => canDelete);
const canUpdateAny = documents.some(({ canUpdate }) => canUpdate);
const canRequestAnySignatures = documents.some(({ canRequestSignatures }) => canRequestSignatures);
const canArchiveAny = documents.some(({ canArchive }) => canArchive);
const canUnarchiveAny = documents.some(({ canUnarchive }) => canUnarchive);
const canSendAnySignatureNotifications = documents.some(({ canSendSigningNotifications }) => canSendSigningNotifications);
const hasAnyAction = tab === "ARCHIVED" ? canUnarchiveAny || canDeleteAny : canDeleteAny || canUpdateAny;
useEffect(() => {
onConnectionIdChange(connectionId);
}, [connectionId, onConnectionIdChange]);
useEffect(() => {
onCanSendNotificationsChange?.(canSendAnySignatureNotifications);
}, [canSendAnySignatureNotifications, onCanSendNotificationsChange]);
const handleDocumentTypeFilterChange = (value: string) => {
const newType = value === "ALL" ? null : (value as DocumentType);
@@ -94,41 +166,36 @@ export function DocumentList(props: {
"DocumentsListQuery_documents",
{
orderBy: { direction: "ASC", field: "TITLE" },
filter: { documentTypes: newType ? [newType] : null },
filter: { status: [tab], documentTypes: newType ? [newType] : null },
},
),
);
startTransition(() => {
pagination.refetch(
{ documentTypes: newType ? [newType] : null },
{ fetchPolicy: "network-only" },
{ status: [tab], documentTypes: newType ? [newType] : null },
{ fetchPolicy: "store-and-network" },
);
});
};
const canDeleteAny = documents.some(({ canDelete }) => canDelete);
const canUpdateAny = documents.some(({ canUpdate }) => canUpdate);
const canRequestAnySignatures = documents.some(
({ canRequestSignatures }) => canRequestSignatures,
);
const hasAnyAction = canDeleteAny || canUpdateAny;
const handleBulkDelete = () => {
const documentCount = selection.length;
confirm(
() =>
bulkDeleteDocuments({
variables: {
input: { documentIds: selection },
variables: { input: { documentIds: selection } },
updater: (store) => {
const conn = store.get(connectionId);
if (conn) {
selection.forEach(id => ConnectionHandler.deleteNode(conn, id));
}
},
}).then(() => {
clear();
}),
{
message: sprintf(
__(
"This will permanently delete %s document%s. This action cannot be undone.",
),
__("This will permanently delete %s document%s. This action cannot be undone."),
documentCount,
documentCount > 1 ? "s" : "",
),
@@ -136,6 +203,32 @@ export function DocumentList(props: {
);
};
const handleBulkArchive = () => {
void bulkArchiveDocuments({
variables: { input: { documentIds: selection } },
updater: (store) => {
const conn = store.get(connectionId);
if (conn) {
selection.forEach(id => ConnectionHandler.deleteNode(conn, id));
}
},
onSuccess: clear,
});
};
const handleBulkUnarchive = () => {
void bulkUnarchiveDocuments({
variables: { input: { documentIds: selection } },
updater: (store) => {
const conn = store.get(connectionId);
if (conn) {
selection.forEach(id => ConnectionHandler.deleteNode(conn, id));
}
},
onSuccess: clear,
});
};
const handleBulkExport = async (options: {
withWatermark: boolean;
withSignatures: boolean;
@@ -145,13 +238,9 @@ export function DocumentList(props: {
documentIds: selection,
withWatermark: options.withWatermark,
withSignatures: options.withSignatures,
...(options.withWatermark
&& options.watermarkEmail && { watermarkEmail: options.watermarkEmail }),
...(options.withWatermark && options.watermarkEmail && { watermarkEmail: options.watermarkEmail }),
};
await bulkExportDocuments({
variables: { input },
});
await bulkExportDocuments({ variables: { input } });
clear();
};
@@ -162,12 +251,21 @@ export function DocumentList(props: {
"DocumentsListQuery_documents",
{
orderBy: order,
filter: { documentTypes: documentTypeFilter ? [documentTypeFilter] : null },
filter: { status: [tab], documentTypes: documentTypeFilter ? [documentTypeFilter] : null },
},
),
);
};
const refetchWithFilters: ComponentProps<typeof SortableTable>["refetch"] = ({ order }) => {
pagination.refetch({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
order: order as any,
status: [tab],
documentTypes: documentTypeFilter ? [documentTypeFilter] : null,
});
};
return (
<div className="space-y-4">
<div className="flex items-center gap-4">
@@ -188,7 +286,7 @@ export function DocumentList(props: {
? (
<SortableTable
{...pagination}
refetch={pagination.refetch as ComponentProps<typeof SortableTable>["refetch"]}
refetch={refetchWithFilters}
>
<Thead>
{selection.length === 0
@@ -196,10 +294,7 @@ export function DocumentList(props: {
<Tr>
<Th className="w-18">
<Checkbox
checked={
selection.length === documents.length
&& documents.length > 0
}
checked={selection.length === documents.length && documents.length > 0}
onChange={() => reset(documents.map(d => d.id))}
/>
</Th>
@@ -235,58 +330,90 @@ export function DocumentList(props: {
</button>
</div>
<div className="flex gap-2 items-center">
{canUpdateAny && (
<PublishDocumentsDialog
documentIds={selection}
onSave={clear}
>
<Button
icon={IconCheckmark1}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Publish")}
</Button>
</PublishDocumentsDialog>
)}
{canRequestAnySignatures && (
<SignatureDocumentsDialog
documentIds={selection}
onSave={clear}
>
<Button
variant="secondary"
icon={IconSignature}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Request signature")}
</Button>
</SignatureDocumentsDialog>
)}
<BulkExportDialog
ref={bulkExportDialogRef}
onExport={handleBulkExport}
isLoading={isBulkExporting}
defaultEmail={defaultEmail}
selectedCount={selection.length}
>
<Button
variant="secondary"
icon={IconArrowDown}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Export")}
</Button>
</BulkExportDialog>
{canDeleteAny && (
<Button
variant="danger"
icon={IconTrashCan}
onClick={handleBulkDelete}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Delete")}
</Button>
)}
{tab === "ARCHIVED"
? (
<>
{canUnarchiveAny && (
<Button
variant="secondary"
icon={IconArchive}
onClick={handleBulkUnarchive}
disabled={isBulkUnarchiving}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Unarchive")}
</Button>
)}
{canDeleteAny && (
<Button
variant="danger"
icon={IconTrashCan}
onClick={handleBulkDelete}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Delete")}
</Button>
)}
</>
)
: (
<>
{canUpdateAny && (
<PublishDocumentsDialog documentIds={selection} onSave={clear}>
<Button icon={IconCheckmark1} className="py-0.5 px-2 text-xs h-6 min-h-6">
{__("Publish")}
</Button>
</PublishDocumentsDialog>
)}
{canRequestAnySignatures && (
<SignatureDocumentsDialog documentIds={selection} onSave={clear}>
<Button
variant="secondary"
icon={IconSignature}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Request signature")}
</Button>
</SignatureDocumentsDialog>
)}
<BulkExportDialog
ref={bulkExportDialogRef}
onExport={handleBulkExport}
isLoading={isBulkExporting}
defaultEmail={defaultEmail}
selectedCount={selection.length}
>
<Button
variant="secondary"
icon={IconArrowDown}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Export")}
</Button>
</BulkExportDialog>
{canArchiveAny && (
<Button
variant="secondary"
icon={IconArchive}
onClick={handleBulkArchive}
disabled={isBulkArchiving}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Archive")}
</Button>
)}
{canDeleteAny && (
<Button
variant="danger"
icon={IconTrashCan}
onClick={handleBulkDelete}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Delete")}
</Button>
)}
</>
)}
</div>
</div>
</Th>
@@ -311,11 +438,13 @@ export function DocumentList(props: {
<Card padded>
<div className="text-center py-12">
<h3 className="text-lg font-semibold mb-2">
{__("No documents yet")}
{tab === "ARCHIVED" ? __("No archived documents") : __("No documents yet")}
</h3>
<p className="text-txt-tertiary mb-4">
{__("Create your first document to get started.")}
</p>
{tab !== "ARCHIVED" && (
<p className="text-txt-tertiary mb-4">
{__("Create your first document to get started.")}
</p>
)}
</div>
</Card>
)}

View File

@@ -109,7 +109,9 @@ export function DocumentListItem(props: {
};
return (
<Tr to={`/organizations/${organizationId}/documents/${document.id}`}>
<Tr
to={`/organizations/${organizationId}/documents/${document.id}`}
>
<Td noLink className="w-18">
<Checkbox checked={checked} onChange={onCheck} />
</Td>