Add document delete confirmation
Route document deletion through explicit confirmation dialogs so single and bulk delete actions require users to acknowledge the destructive operation before the mutation runs. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Sacha Al Himdani <SachaProbo@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Spinner,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import { ConnectionHandler, type DataID } from "relay-runtime";
|
||||
|
||||
import {
|
||||
useBulkDeleteDocumentsMutation,
|
||||
useDeleteDocumentMutation,
|
||||
} from "#/hooks/graph/DocumentGraph";
|
||||
|
||||
export type DeleteDocumentDialogRef = {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
type DeleteDocumentDialogProps = {
|
||||
documentId: string;
|
||||
documentTitle: string;
|
||||
connections: DataID[];
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export const DeleteDocumentDialog = forwardRef<
|
||||
DeleteDocumentDialogRef,
|
||||
DeleteDocumentDialogProps
|
||||
>(({ documentId, documentTitle, connections, onSuccess }, ref) => {
|
||||
const { __ } = useTranslate();
|
||||
const dialogRef = useDialogRef();
|
||||
const [deleteDocument, isDeleting] = useDeleteDocumentMutation();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => dialogRef.current?.open(),
|
||||
close: () => dialogRef.current?.close(),
|
||||
}));
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteDocument({
|
||||
variables: {
|
||||
input: { documentId },
|
||||
connections,
|
||||
},
|
||||
});
|
||||
dialogRef.current?.close();
|
||||
onSuccess?.();
|
||||
} catch {
|
||||
// The mutation helper already displays the error toast.
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ref={dialogRef}
|
||||
title={__("Delete document")}
|
||||
className="max-w-md"
|
||||
>
|
||||
<DialogContent padded>
|
||||
<p className="text-txt-secondary">
|
||||
{sprintf(
|
||||
__("Are you sure you want to delete the document \"%s\"?"),
|
||||
documentTitle,
|
||||
)}
|
||||
</p>
|
||||
<p className="text-txt-secondary mt-2">
|
||||
{__("This action cannot be undone.")}
|
||||
</p>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => void handleDelete()}
|
||||
disabled={isDeleting}
|
||||
icon={isDeleting ? Spinner : undefined}
|
||||
>
|
||||
{__("Delete")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
DeleteDocumentDialog.displayName = "DeleteDocumentDialog";
|
||||
|
||||
type DeleteDocumentsDialogProps = {
|
||||
documentIds: string[];
|
||||
connectionId: DataID;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export const DeleteDocumentsDialog = forwardRef<
|
||||
DeleteDocumentDialogRef,
|
||||
DeleteDocumentsDialogProps
|
||||
>(({ documentIds, connectionId, onSuccess }, ref) => {
|
||||
const { __ } = useTranslate();
|
||||
const dialogRef = useDialogRef();
|
||||
const [deleteDocuments, isDeleting] = useBulkDeleteDocumentsMutation();
|
||||
const documentCount = documentIds.length;
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => dialogRef.current?.open(),
|
||||
close: () => dialogRef.current?.close(),
|
||||
}));
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteDocuments({
|
||||
variables: { input: { documentIds } },
|
||||
updater: (store) => {
|
||||
const conn = store.get(connectionId);
|
||||
if (conn) {
|
||||
documentIds.forEach(id => ConnectionHandler.deleteNode(conn, id));
|
||||
}
|
||||
},
|
||||
});
|
||||
dialogRef.current?.close();
|
||||
onSuccess?.();
|
||||
} catch {
|
||||
// The mutation helper already displays the error toast.
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ref={dialogRef}
|
||||
title={__("Delete documents")}
|
||||
className="max-w-md"
|
||||
>
|
||||
<DialogContent padded>
|
||||
<p className="text-txt-secondary">
|
||||
{documentCount === 1
|
||||
? __("Are you sure you want to delete 1 selected document?")
|
||||
: sprintf(
|
||||
__("Are you sure you want to delete %s selected documents?"),
|
||||
documentCount,
|
||||
)}
|
||||
</p>
|
||||
<p className="text-txt-secondary mt-2">
|
||||
{__("This action cannot be undone.")}
|
||||
</p>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => void handleDelete()}
|
||||
disabled={isDeleting || documentCount === 0}
|
||||
icon={isDeleting ? Spinner : undefined}
|
||||
>
|
||||
{__("Delete")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
DeleteDocumentsDialog.displayName = "DeleteDocumentsDialog";
|
||||
@@ -27,10 +27,12 @@ import type { DocumentActionsDropdown_exportVersionMutation } from "#/__generate
|
||||
import type { DocumentActionsDropdown_unarchiveMutation } from "#/__generated__/core/DocumentActionsDropdown_unarchiveMutation.graphql";
|
||||
import type { DocumentActionsDropdown_versionFragment$key } from "#/__generated__/core/DocumentActionsDropdown_versionFragment.graphql";
|
||||
import { PdfDownloadDialog, type PdfDownloadDialogRef } from "#/components/documents/PdfDownloadDialog";
|
||||
import { DocumentsConnectionKey, useDeleteDocumentMutation } from "#/hooks/graph/DocumentGraph";
|
||||
import { DocumentsConnectionKey } from "#/hooks/graph/DocumentGraph";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
import { CurrentUser } from "#/providers/CurrentUser";
|
||||
|
||||
import { DeleteDocumentDialog, type DeleteDocumentDialogRef } from "./DeleteDocumentDialog";
|
||||
|
||||
const documentFragment = graphql`
|
||||
fragment DocumentActionsDropdown_documentFragment on Document {
|
||||
id
|
||||
@@ -121,13 +123,13 @@ export function DocumentActionsDropdown(props: {
|
||||
const { __ } = useTranslate();
|
||||
const { email: defaultEmail } = use(CurrentUser);
|
||||
const pdfDownloadDialogRef = useRef<PdfDownloadDialogRef>(null);
|
||||
const deleteDocumentDialogRef = useRef<DeleteDocumentDialogRef>(null);
|
||||
const confirm = useConfirm();
|
||||
const { toast } = useToast();
|
||||
|
||||
const document = useFragment<DocumentActionsDropdown_documentFragment$key>(documentFragment, documentFragmentRef);
|
||||
const version = useFragment<DocumentActionsDropdown_versionFragment$key>(versionFragment, versionFragmentRef);
|
||||
|
||||
const [deleteDocument, isDeleting] = useDeleteDocumentMutation();
|
||||
const [archiveDocument, isArchiving]
|
||||
= useMutation<DocumentActionsDropdown_archiveMutation>(archiveDocumentMutation);
|
||||
const [unarchiveDocument, isUnarchiving]
|
||||
@@ -214,33 +216,11 @@ export function DocumentActionsDropdown(props: {
|
||||
);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
const connectionId = ConnectionHandler.getConnectionID(
|
||||
organizationId,
|
||||
DocumentsConnectionKey,
|
||||
{ orderBy: { direction: "ASC", field: "TITLE" } },
|
||||
);
|
||||
confirm(
|
||||
() =>
|
||||
deleteDocument({
|
||||
variables: {
|
||||
input: { documentId: document.id },
|
||||
connections: [connectionId],
|
||||
},
|
||||
onSuccess() {
|
||||
void navigate(`/organizations/${organizationId}/documents`);
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: sprintf(
|
||||
__(
|
||||
"This will permanently delete the document \"%s\". This action cannot be undone.",
|
||||
),
|
||||
version.title,
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
const documentsConnectionId = ConnectionHandler.getConnectionID(
|
||||
organizationId,
|
||||
DocumentsConnectionKey,
|
||||
{ orderBy: { direction: "ASC", field: "TITLE" } },
|
||||
);
|
||||
|
||||
const handleExportDocumentVersion = (options: {
|
||||
withWatermark: boolean;
|
||||
@@ -290,6 +270,13 @@ export function DocumentActionsDropdown(props: {
|
||||
isLoading={isExporting}
|
||||
defaultEmail={defaultEmail}
|
||||
/>
|
||||
<DeleteDocumentDialog
|
||||
ref={deleteDocumentDialogRef}
|
||||
documentId={document.id}
|
||||
documentTitle={version.title}
|
||||
connections={[documentsConnectionId]}
|
||||
onSuccess={() => void navigate(`/organizations/${organizationId}/documents`)}
|
||||
/>
|
||||
<ActionDropdown variant="secondary">
|
||||
<DropdownItem
|
||||
onClick={() => pdfDownloadDialogRef.current?.open()}
|
||||
@@ -329,8 +316,7 @@ export function DocumentActionsDropdown(props: {
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
disabled={isDeleting}
|
||||
onClick={handleDelete}
|
||||
onClick={() => deleteDocumentDialogRef.current?.open()}
|
||||
>
|
||||
{__("Delete document")}
|
||||
</DropdownItem>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { documentClassifications, documentTypes, documentWriteModes, getDocumentClassificationLabel, getDocumentTypeLabel, getDocumentWriteModeLabel, sprintf } from "@probo/helpers";
|
||||
import { useList } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Card, Checkbox, IconArchive, IconArrowDown, IconCrossLargeX, IconSignature, IconTrashCan, IconUpload, Option, Select, Tbody, Th, Thead, Tr, useConfirm } from "@probo/ui";
|
||||
import { Button, Card, Checkbox, IconArchive, IconArrowDown, IconCrossLargeX, IconSignature, IconTrashCan, IconUpload, Option, Select, Tbody, Th, Thead, Tr } from "@probo/ui";
|
||||
import { type ComponentProps, use, useEffect, useRef, useState, useTransition } from "react";
|
||||
import { usePaginationFragment } from "react-relay";
|
||||
import { ConnectionHandler, graphql } from "relay-runtime";
|
||||
@@ -26,11 +26,12 @@ import type { DocumentListFragment$key } from "#/__generated__/core/DocumentList
|
||||
import type { DocumentClassification, DocumentOrderField, DocumentsListQuery, DocumentType, DocumentWriteMode } 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 { useBulkExportDocumentsMutation } from "#/hooks/graph/DocumentGraph";
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
import { CurrentUser } from "#/providers/CurrentUser";
|
||||
|
||||
import { type DeleteDocumentDialogRef, DeleteDocumentsDialog } from "./DeleteDocumentDialog";
|
||||
import { DocumentListItem } from "./DocumentListItem";
|
||||
import { PublishDocumentsDialog } from "./PublishDocumentsDialog";
|
||||
import { SignatureDocumentsDialog } from "./SignatureDocumentsDialog";
|
||||
@@ -122,6 +123,7 @@ export function DocumentList(props: {
|
||||
const organizationId = useOrganizationId();
|
||||
const { email: defaultEmail } = use(CurrentUser);
|
||||
const bulkExportDialogRef = useRef<BulkExportDialogRef>(null);
|
||||
const deleteDocumentsDialogRef = useRef<DeleteDocumentDialogRef>(null);
|
||||
const { __ } = useTranslate();
|
||||
|
||||
const pagination = usePaginationFragment<DocumentsListQuery, DocumentListFragment$key>(
|
||||
@@ -152,7 +154,6 @@ export function DocumentList(props: {
|
||||
const documents = pagination.data.documents.edges.map(({ node }) => node);
|
||||
const connectionId = pagination.data.documents.__id;
|
||||
|
||||
const [bulkDeleteDocuments] = useBulkDeleteDocumentsMutation();
|
||||
const [bulkExportDocuments, isBulkExporting] = useBulkExportDocumentsMutation();
|
||||
const [bulkArchiveDocuments, isBulkArchiving] = useMutationWithToasts<DocumentListBulkArchiveMutation>(
|
||||
bulkArchiveMutation,
|
||||
@@ -163,7 +164,6 @@ export function DocumentList(props: {
|
||||
{ successMessage: __("Documents unarchived successfully."), errorMessage: __("Failed to unarchive documents") },
|
||||
);
|
||||
const { list: selection, toggle, clear, reset } = useList<string>([]);
|
||||
const confirm = useConfirm();
|
||||
|
||||
const canDeleteAny = documents.some(({ canDelete }) => canDelete);
|
||||
const canUpdateAny = documents.some(({ canUpdate }) => canUpdate);
|
||||
@@ -247,28 +247,7 @@ export function DocumentList(props: {
|
||||
};
|
||||
|
||||
const handleBulkDelete = () => {
|
||||
const documentCount = selection.length;
|
||||
confirm(
|
||||
() =>
|
||||
bulkDeleteDocuments({
|
||||
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."),
|
||||
documentCount,
|
||||
documentCount > 1 ? "s" : "",
|
||||
),
|
||||
},
|
||||
);
|
||||
deleteDocumentsDialogRef.current?.open();
|
||||
};
|
||||
|
||||
const handleBulkArchive = () => {
|
||||
@@ -342,6 +321,12 @@ export function DocumentList(props: {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<DeleteDocumentsDialog
|
||||
ref={deleteDocumentsDialogRef}
|
||||
documentIds={selection}
|
||||
connectionId={connectionId}
|
||||
onSuccess={clear}
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<Select
|
||||
value={writeModeFilter ?? "ALL"}
|
||||
|
||||
@@ -32,15 +32,17 @@ import {
|
||||
useConfirm,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useRef } from "react";
|
||||
import { useFragment, useMutation } from "react-relay";
|
||||
import { ConnectionHandler, type DataID, graphql } from "relay-runtime";
|
||||
|
||||
import type { DocumentListItem_archiveMutation } from "#/__generated__/core/DocumentListItem_archiveMutation.graphql";
|
||||
import type { DocumentListItem_deleteMutation } from "#/__generated__/core/DocumentListItem_deleteMutation.graphql";
|
||||
import type { DocumentListItem_unarchiveMutation } from "#/__generated__/core/DocumentListItem_unarchiveMutation.graphql";
|
||||
import type { DocumentListItemFragment$key } from "#/__generated__/core/DocumentListItemFragment.graphql";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
import { DeleteDocumentDialog, type DeleteDocumentDialogRef } from "./DeleteDocumentDialog";
|
||||
|
||||
const fragment = graphql`
|
||||
fragment DocumentListItemFragment on Document {
|
||||
id
|
||||
@@ -105,17 +107,6 @@ const archiveDocumentMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteDocumentMutation = graphql`
|
||||
mutation DocumentListItem_deleteMutation(
|
||||
$input: DeleteDocumentInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
deleteDocument(input: $input) {
|
||||
deletedDocumentId @deleteEdge(connections: $connections)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const unarchiveDocumentMutation = graphql`
|
||||
mutation DocumentListItem_unarchiveMutation(
|
||||
$input: UnarchiveDocumentInput!
|
||||
@@ -152,9 +143,9 @@ export function DocumentListItem(props: {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const [archiveDocument, isArchiving] = useMutation<DocumentListItem_archiveMutation>(archiveDocumentMutation);
|
||||
const [deleteDocument] = useMutation<DocumentListItem_deleteMutation>(deleteDocumentMutation);
|
||||
const [unarchiveDocument, isUnarchiving] = useMutation<DocumentListItem_unarchiveMutation>(unarchiveDocumentMutation);
|
||||
const confirm = useConfirm();
|
||||
const deleteDialogRef = useRef<DeleteDocumentDialogRef>(null);
|
||||
const document = useFragment<DocumentListItemFragment$key>(
|
||||
fragment,
|
||||
fragmentRef,
|
||||
@@ -222,27 +213,7 @@ export function DocumentListItem(props: {
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
confirm(
|
||||
() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
deleteDocument({
|
||||
variables: {
|
||||
connections: [connectionId],
|
||||
input: { documentId: document.id },
|
||||
},
|
||||
onCompleted: () => resolve(),
|
||||
onError: err => reject(err),
|
||||
});
|
||||
}),
|
||||
{
|
||||
message: sprintf(
|
||||
__(
|
||||
"This will permanently delete the document \"%s\". This action cannot be undone.",
|
||||
),
|
||||
lastVersion.title,
|
||||
),
|
||||
},
|
||||
);
|
||||
deleteDialogRef.current?.open();
|
||||
};
|
||||
|
||||
const handleUnarchive = () => {
|
||||
@@ -281,87 +252,95 @@ export function DocumentListItem(props: {
|
||||
|| document.canDelete;
|
||||
|
||||
return (
|
||||
<Tr
|
||||
to={`/organizations/${organizationId}/documents/${document.id}`}
|
||||
>
|
||||
<Td noLink className="w-18">
|
||||
<Checkbox checked={checked} onChange={onCheck} />
|
||||
</Td>
|
||||
<Td className="min-w-0">
|
||||
<div className="flex gap-4 items-center">{lastVersion.title}</div>
|
||||
</Td>
|
||||
<Td className="w-24">
|
||||
<Badge variant={statusVariant[lastVersion.status]}>
|
||||
{statusLabel[lastVersion.status]}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td className="w-20">
|
||||
v
|
||||
{lastVersion.major}
|
||||
.
|
||||
{lastVersion.minor}
|
||||
</Td>
|
||||
<Td className="w-28">
|
||||
{getDocumentTypeLabel(__, lastVersion.documentType)}
|
||||
</Td>
|
||||
<Td className="w-32">
|
||||
{getDocumentClassificationLabel(__, lastVersion.classification)}
|
||||
</Td>
|
||||
<Td className="w-60">
|
||||
{(() => {
|
||||
if (lastVersion.status === "PENDING_APPROVAL") {
|
||||
const quorum = lastVersion.approvalQuorums?.edges?.[0]?.node;
|
||||
if (quorum) {
|
||||
if (quorum.status === "REJECTED") return __("Rejected");
|
||||
return `${quorum.approvedDecisions.totalCount}/${quorum.decisions.totalCount}`;
|
||||
}
|
||||
return "—";
|
||||
}
|
||||
if (!document.defaultApprovers.length) return "—";
|
||||
return document.defaultApprovers.map(a => a.fullName).join(", ");
|
||||
})()}
|
||||
</Td>
|
||||
<Td className="w-40">{formatDate(document.updatedAt)}</Td>
|
||||
<Td className="w-20">
|
||||
{lastVersion.signedSignatures.totalCount}
|
||||
/
|
||||
{lastVersion.signatures.totalCount}
|
||||
</Td>
|
||||
{hasAnyAction && (
|
||||
<Td noLink width={50} className="text-end w-18">
|
||||
{hasRowAction && (
|
||||
<ActionDropdown>
|
||||
{document.canArchive && document.status === "ACTIVE" && (
|
||||
<DropdownItem
|
||||
icon={IconArchive}
|
||||
disabled={isArchiving}
|
||||
onClick={handleArchive}
|
||||
>
|
||||
{__("Archive")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{document.canUnarchive && document.status === "ARCHIVED" && (
|
||||
<DropdownItem
|
||||
icon={IconArchive}
|
||||
disabled={isUnarchiving}
|
||||
onClick={handleUnarchive}
|
||||
>
|
||||
{__("Unarchive")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{document.canDelete && (
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</ActionDropdown>
|
||||
)}
|
||||
<>
|
||||
<Tr
|
||||
to={`/organizations/${organizationId}/documents/${document.id}`}
|
||||
>
|
||||
<Td noLink className="w-18">
|
||||
<Checkbox checked={checked} onChange={onCheck} />
|
||||
</Td>
|
||||
)}
|
||||
</Tr>
|
||||
<Td className="min-w-0">
|
||||
<div className="flex gap-4 items-center">{lastVersion.title}</div>
|
||||
</Td>
|
||||
<Td className="w-24">
|
||||
<Badge variant={statusVariant[lastVersion.status]}>
|
||||
{statusLabel[lastVersion.status]}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td className="w-20">
|
||||
v
|
||||
{lastVersion.major}
|
||||
.
|
||||
{lastVersion.minor}
|
||||
</Td>
|
||||
<Td className="w-28">
|
||||
{getDocumentTypeLabel(__, lastVersion.documentType)}
|
||||
</Td>
|
||||
<Td className="w-32">
|
||||
{getDocumentClassificationLabel(__, lastVersion.classification)}
|
||||
</Td>
|
||||
<Td className="w-60">
|
||||
{(() => {
|
||||
if (lastVersion.status === "PENDING_APPROVAL") {
|
||||
const quorum = lastVersion.approvalQuorums?.edges?.[0]?.node;
|
||||
if (quorum) {
|
||||
if (quorum.status === "REJECTED") return __("Rejected");
|
||||
return `${quorum.approvedDecisions.totalCount}/${quorum.decisions.totalCount}`;
|
||||
}
|
||||
return "—";
|
||||
}
|
||||
if (!document.defaultApprovers.length) return "—";
|
||||
return document.defaultApprovers.map(a => a.fullName).join(", ");
|
||||
})()}
|
||||
</Td>
|
||||
<Td className="w-40">{formatDate(document.updatedAt)}</Td>
|
||||
<Td className="w-20">
|
||||
{lastVersion.signedSignatures.totalCount}
|
||||
/
|
||||
{lastVersion.signatures.totalCount}
|
||||
</Td>
|
||||
{hasAnyAction && (
|
||||
<Td noLink width={50} className="text-end w-18">
|
||||
{hasRowAction && (
|
||||
<ActionDropdown>
|
||||
{document.canArchive && document.status === "ACTIVE" && (
|
||||
<DropdownItem
|
||||
icon={IconArchive}
|
||||
disabled={isArchiving}
|
||||
onClick={handleArchive}
|
||||
>
|
||||
{__("Archive")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{document.canUnarchive && document.status === "ARCHIVED" && (
|
||||
<DropdownItem
|
||||
icon={IconArchive}
|
||||
disabled={isUnarchiving}
|
||||
onClick={handleUnarchive}
|
||||
>
|
||||
{__("Unarchive")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{document.canDelete && (
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</ActionDropdown>
|
||||
)}
|
||||
</Td>
|
||||
)}
|
||||
</Tr>
|
||||
<DeleteDocumentDialog
|
||||
ref={deleteDialogRef}
|
||||
documentId={document.id}
|
||||
documentTitle={lastVersion.title}
|
||||
connections={[connectionId]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user