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_unarchiveMutation } from "#/__generated__/core/DocumentActionsDropdown_unarchiveMutation.graphql";
|
||||||
import type { DocumentActionsDropdown_versionFragment$key } from "#/__generated__/core/DocumentActionsDropdown_versionFragment.graphql";
|
import type { DocumentActionsDropdown_versionFragment$key } from "#/__generated__/core/DocumentActionsDropdown_versionFragment.graphql";
|
||||||
import { PdfDownloadDialog, type PdfDownloadDialogRef } from "#/components/documents/PdfDownloadDialog";
|
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 { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||||
import { CurrentUser } from "#/providers/CurrentUser";
|
import { CurrentUser } from "#/providers/CurrentUser";
|
||||||
|
|
||||||
|
import { DeleteDocumentDialog, type DeleteDocumentDialogRef } from "./DeleteDocumentDialog";
|
||||||
|
|
||||||
const documentFragment = graphql`
|
const documentFragment = graphql`
|
||||||
fragment DocumentActionsDropdown_documentFragment on Document {
|
fragment DocumentActionsDropdown_documentFragment on Document {
|
||||||
id
|
id
|
||||||
@@ -121,13 +123,13 @@ export function DocumentActionsDropdown(props: {
|
|||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { email: defaultEmail } = use(CurrentUser);
|
const { email: defaultEmail } = use(CurrentUser);
|
||||||
const pdfDownloadDialogRef = useRef<PdfDownloadDialogRef>(null);
|
const pdfDownloadDialogRef = useRef<PdfDownloadDialogRef>(null);
|
||||||
|
const deleteDocumentDialogRef = useRef<DeleteDocumentDialogRef>(null);
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const document = useFragment<DocumentActionsDropdown_documentFragment$key>(documentFragment, documentFragmentRef);
|
const document = useFragment<DocumentActionsDropdown_documentFragment$key>(documentFragment, documentFragmentRef);
|
||||||
const version = useFragment<DocumentActionsDropdown_versionFragment$key>(versionFragment, versionFragmentRef);
|
const version = useFragment<DocumentActionsDropdown_versionFragment$key>(versionFragment, versionFragmentRef);
|
||||||
|
|
||||||
const [deleteDocument, isDeleting] = useDeleteDocumentMutation();
|
|
||||||
const [archiveDocument, isArchiving]
|
const [archiveDocument, isArchiving]
|
||||||
= useMutation<DocumentActionsDropdown_archiveMutation>(archiveDocumentMutation);
|
= useMutation<DocumentActionsDropdown_archiveMutation>(archiveDocumentMutation);
|
||||||
const [unarchiveDocument, isUnarchiving]
|
const [unarchiveDocument, isUnarchiving]
|
||||||
@@ -214,33 +216,11 @@ export function DocumentActionsDropdown(props: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const documentsConnectionId = ConnectionHandler.getConnectionID(
|
||||||
const connectionId = ConnectionHandler.getConnectionID(
|
organizationId,
|
||||||
organizationId,
|
DocumentsConnectionKey,
|
||||||
DocumentsConnectionKey,
|
{ orderBy: { direction: "ASC", field: "TITLE" } },
|
||||||
{ 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 handleExportDocumentVersion = (options: {
|
const handleExportDocumentVersion = (options: {
|
||||||
withWatermark: boolean;
|
withWatermark: boolean;
|
||||||
@@ -290,6 +270,13 @@ export function DocumentActionsDropdown(props: {
|
|||||||
isLoading={isExporting}
|
isLoading={isExporting}
|
||||||
defaultEmail={defaultEmail}
|
defaultEmail={defaultEmail}
|
||||||
/>
|
/>
|
||||||
|
<DeleteDocumentDialog
|
||||||
|
ref={deleteDocumentDialogRef}
|
||||||
|
documentId={document.id}
|
||||||
|
documentTitle={version.title}
|
||||||
|
connections={[documentsConnectionId]}
|
||||||
|
onSuccess={() => void navigate(`/organizations/${organizationId}/documents`)}
|
||||||
|
/>
|
||||||
<ActionDropdown variant="secondary">
|
<ActionDropdown variant="secondary">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={() => pdfDownloadDialogRef.current?.open()}
|
onClick={() => pdfDownloadDialogRef.current?.open()}
|
||||||
@@ -329,8 +316,7 @@ export function DocumentActionsDropdown(props: {
|
|||||||
<DropdownItem
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
disabled={isDeleting}
|
onClick={() => deleteDocumentDialogRef.current?.open()}
|
||||||
onClick={handleDelete}
|
|
||||||
>
|
>
|
||||||
{__("Delete document")}
|
{__("Delete document")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import { documentClassifications, documentTypes, documentWriteModes, getDocumentClassificationLabel, getDocumentTypeLabel, getDocumentWriteModeLabel, sprintf } from "@probo/helpers";
|
import { documentClassifications, documentTypes, documentWriteModes, getDocumentClassificationLabel, getDocumentTypeLabel, getDocumentWriteModeLabel, sprintf } from "@probo/helpers";
|
||||||
import { useList } from "@probo/hooks";
|
import { useList } from "@probo/hooks";
|
||||||
import { useTranslate } from "@probo/i18n";
|
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 { type ComponentProps, use, useEffect, useRef, useState, useTransition } from "react";
|
||||||
import { usePaginationFragment } from "react-relay";
|
import { usePaginationFragment } from "react-relay";
|
||||||
import { ConnectionHandler, graphql } from "relay-runtime";
|
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 type { DocumentClassification, DocumentOrderField, DocumentsListQuery, DocumentType, DocumentWriteMode } from "#/__generated__/core/DocumentsListQuery.graphql";
|
||||||
import { BulkExportDialog, type BulkExportDialogRef } from "#/components/documents/BulkExportDialog";
|
import { BulkExportDialog, type BulkExportDialogRef } from "#/components/documents/BulkExportDialog";
|
||||||
import { type Order, SortableTable, SortableTh } from "#/components/SortableTable";
|
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 { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||||
import { CurrentUser } from "#/providers/CurrentUser";
|
import { CurrentUser } from "#/providers/CurrentUser";
|
||||||
|
|
||||||
|
import { type DeleteDocumentDialogRef, DeleteDocumentsDialog } from "./DeleteDocumentDialog";
|
||||||
import { DocumentListItem } from "./DocumentListItem";
|
import { DocumentListItem } from "./DocumentListItem";
|
||||||
import { PublishDocumentsDialog } from "./PublishDocumentsDialog";
|
import { PublishDocumentsDialog } from "./PublishDocumentsDialog";
|
||||||
import { SignatureDocumentsDialog } from "./SignatureDocumentsDialog";
|
import { SignatureDocumentsDialog } from "./SignatureDocumentsDialog";
|
||||||
@@ -122,6 +123,7 @@ export function DocumentList(props: {
|
|||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { email: defaultEmail } = use(CurrentUser);
|
const { email: defaultEmail } = use(CurrentUser);
|
||||||
const bulkExportDialogRef = useRef<BulkExportDialogRef>(null);
|
const bulkExportDialogRef = useRef<BulkExportDialogRef>(null);
|
||||||
|
const deleteDocumentsDialogRef = useRef<DeleteDocumentDialogRef>(null);
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
const pagination = usePaginationFragment<DocumentsListQuery, DocumentListFragment$key>(
|
const pagination = usePaginationFragment<DocumentsListQuery, DocumentListFragment$key>(
|
||||||
@@ -152,7 +154,6 @@ export function DocumentList(props: {
|
|||||||
const documents = pagination.data.documents.edges.map(({ node }) => node);
|
const documents = pagination.data.documents.edges.map(({ node }) => node);
|
||||||
const connectionId = pagination.data.documents.__id;
|
const connectionId = pagination.data.documents.__id;
|
||||||
|
|
||||||
const [bulkDeleteDocuments] = useBulkDeleteDocumentsMutation();
|
|
||||||
const [bulkExportDocuments, isBulkExporting] = useBulkExportDocumentsMutation();
|
const [bulkExportDocuments, isBulkExporting] = useBulkExportDocumentsMutation();
|
||||||
const [bulkArchiveDocuments, isBulkArchiving] = useMutationWithToasts<DocumentListBulkArchiveMutation>(
|
const [bulkArchiveDocuments, isBulkArchiving] = useMutationWithToasts<DocumentListBulkArchiveMutation>(
|
||||||
bulkArchiveMutation,
|
bulkArchiveMutation,
|
||||||
@@ -163,7 +164,6 @@ export function DocumentList(props: {
|
|||||||
{ successMessage: __("Documents unarchived successfully."), errorMessage: __("Failed to unarchive documents") },
|
{ successMessage: __("Documents unarchived successfully."), errorMessage: __("Failed to unarchive documents") },
|
||||||
);
|
);
|
||||||
const { list: selection, toggle, clear, reset } = useList<string>([]);
|
const { list: selection, toggle, clear, reset } = useList<string>([]);
|
||||||
const confirm = useConfirm();
|
|
||||||
|
|
||||||
const canDeleteAny = documents.some(({ canDelete }) => canDelete);
|
const canDeleteAny = documents.some(({ canDelete }) => canDelete);
|
||||||
const canUpdateAny = documents.some(({ canUpdate }) => canUpdate);
|
const canUpdateAny = documents.some(({ canUpdate }) => canUpdate);
|
||||||
@@ -247,28 +247,7 @@ export function DocumentList(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkDelete = () => {
|
const handleBulkDelete = () => {
|
||||||
const documentCount = selection.length;
|
deleteDocumentsDialogRef.current?.open();
|
||||||
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" : "",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkArchive = () => {
|
const handleBulkArchive = () => {
|
||||||
@@ -342,6 +321,12 @@ export function DocumentList(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<DeleteDocumentsDialog
|
||||||
|
ref={deleteDocumentsDialogRef}
|
||||||
|
documentIds={selection}
|
||||||
|
connectionId={connectionId}
|
||||||
|
onSuccess={clear}
|
||||||
|
/>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Select
|
<Select
|
||||||
value={writeModeFilter ?? "ALL"}
|
value={writeModeFilter ?? "ALL"}
|
||||||
|
|||||||
@@ -32,15 +32,17 @@ import {
|
|||||||
useConfirm,
|
useConfirm,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
|
import { useRef } from "react";
|
||||||
import { useFragment, useMutation } from "react-relay";
|
import { useFragment, useMutation } from "react-relay";
|
||||||
import { ConnectionHandler, type DataID, graphql } from "relay-runtime";
|
import { ConnectionHandler, type DataID, graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { DocumentListItem_archiveMutation } from "#/__generated__/core/DocumentListItem_archiveMutation.graphql";
|
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 { DocumentListItem_unarchiveMutation } from "#/__generated__/core/DocumentListItem_unarchiveMutation.graphql";
|
||||||
import type { DocumentListItemFragment$key } from "#/__generated__/core/DocumentListItemFragment.graphql";
|
import type { DocumentListItemFragment$key } from "#/__generated__/core/DocumentListItemFragment.graphql";
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||||
|
|
||||||
|
import { DeleteDocumentDialog, type DeleteDocumentDialogRef } from "./DeleteDocumentDialog";
|
||||||
|
|
||||||
const fragment = graphql`
|
const fragment = graphql`
|
||||||
fragment DocumentListItemFragment on Document {
|
fragment DocumentListItemFragment on Document {
|
||||||
id
|
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`
|
const unarchiveDocumentMutation = graphql`
|
||||||
mutation DocumentListItem_unarchiveMutation(
|
mutation DocumentListItem_unarchiveMutation(
|
||||||
$input: UnarchiveDocumentInput!
|
$input: UnarchiveDocumentInput!
|
||||||
@@ -152,9 +143,9 @@ export function DocumentListItem(props: {
|
|||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [archiveDocument, isArchiving] = useMutation<DocumentListItem_archiveMutation>(archiveDocumentMutation);
|
const [archiveDocument, isArchiving] = useMutation<DocumentListItem_archiveMutation>(archiveDocumentMutation);
|
||||||
const [deleteDocument] = useMutation<DocumentListItem_deleteMutation>(deleteDocumentMutation);
|
|
||||||
const [unarchiveDocument, isUnarchiving] = useMutation<DocumentListItem_unarchiveMutation>(unarchiveDocumentMutation);
|
const [unarchiveDocument, isUnarchiving] = useMutation<DocumentListItem_unarchiveMutation>(unarchiveDocumentMutation);
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
const deleteDialogRef = useRef<DeleteDocumentDialogRef>(null);
|
||||||
const document = useFragment<DocumentListItemFragment$key>(
|
const document = useFragment<DocumentListItemFragment$key>(
|
||||||
fragment,
|
fragment,
|
||||||
fragmentRef,
|
fragmentRef,
|
||||||
@@ -222,27 +213,7 @@ export function DocumentListItem(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
confirm(
|
deleteDialogRef.current?.open();
|
||||||
() =>
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUnarchive = () => {
|
const handleUnarchive = () => {
|
||||||
@@ -281,87 +252,95 @@ export function DocumentListItem(props: {
|
|||||||
|| document.canDelete;
|
|| document.canDelete;
|
||||||
|
|
||||||
return (
|
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 noLink className="w-18">
|
||||||
</Td>
|
<Checkbox checked={checked} onChange={onCheck} />
|
||||||
<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>
|
</Td>
|
||||||
)}
|
<Td className="min-w-0">
|
||||||
</Tr>
|
<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