Add document archive row action

Allow documents to be archived or unarchived directly from the
list row actions, matching the detail-page behavior. Remove the
row from the active or archived connection after the status change
so filtered lists update immediately.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
Cursor Agent
2026-05-30 00:48:33 +00:00
committed by Bryan Frimin
parent 7191a28be2
commit 4084554daa
2 changed files with 174 additions and 15 deletions

View File

@@ -173,7 +173,7 @@ export function DocumentList(props: {
const canSendAnySignatureNotifications = documents.some(
({ canSendSigningNotifications }) => canSendSigningNotifications,
);
const hasAnyAction = tab === "ARCHIVED" ? canUnarchiveAny || canDeleteAny : canDeleteAny || canUpdateAny;
const hasAnyAction = tab === "ARCHIVED" ? canUnarchiveAny || canDeleteAny : canArchiveAny || canDeleteAny || canUpdateAny;
useEffect(() => {
onConnectionIdChange(connectionId);

View File

@@ -12,21 +12,43 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { formatDate, getDocumentClassificationLabel, getDocumentTypeLabel, sprintf } from "@probo/helpers";
import {
formatDate,
formatError,
getDocumentClassificationLabel,
getDocumentTypeLabel,
sprintf,
} from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { ActionDropdown, Badge, Checkbox, DropdownItem, IconTrashCan, Td, Tr, useConfirm } from "@probo/ui";
import {
ActionDropdown,
Badge,
Checkbox,
DropdownItem,
IconArchive,
IconTrashCan,
Td,
Tr,
useConfirm,
useToast,
} from "@probo/ui";
import { useFragment, useMutation } from "react-relay";
import { 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_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";
const fragment = graphql`
fragment DocumentListItemFragment on Document {
id
status
updatedAt
canArchive: permission(action: "core:document:archive")
canDelete: permission(action: "core:document:delete")
canUnarchive: permission(action: "core:document:unarchive")
defaultApprovers {
id
fullName
@@ -66,6 +88,23 @@ const fragment = graphql`
}
`;
const archiveDocumentMutation = graphql`
mutation DocumentListItem_archiveMutation(
$input: ArchiveDocumentInput!
) {
archiveDocument(input: $input) {
document {
id
status
archivedAt
canArchive: permission(action: "core:document:archive")
canUnarchive: permission(action: "core:document:unarchive")
canDelete: permission(action: "core:document:delete")
}
}
}
`;
const deleteDocumentMutation = graphql`
mutation DocumentListItem_deleteMutation(
$input: DeleteDocumentInput!
@@ -77,6 +116,23 @@ const deleteDocumentMutation = graphql`
}
`;
const unarchiveDocumentMutation = graphql`
mutation DocumentListItem_unarchiveMutation(
$input: UnarchiveDocumentInput!
) {
unarchiveDocument(input: $input) {
document {
id
status
archivedAt
canArchive: permission(action: "core:document:archive")
canUnarchive: permission(action: "core:document:unarchive")
canDelete: permission(action: "core:document:delete")
}
}
}
`;
export function DocumentListItem(props: {
fragmentRef: DocumentListItemFragment$key;
connectionId: DataID;
@@ -94,7 +150,10 @@ export function DocumentListItem(props: {
const organizationId = useOrganizationId();
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 document = useFragment<DocumentListItemFragment$key>(
fragment,
@@ -117,6 +176,51 @@ export function DocumentListItem(props: {
PUBLISHED: __("Published"),
} as const;
const handleArchive = () => {
confirm(
() =>
new Promise<void>((resolve) => {
archiveDocument({
variables: { input: { documentId: document.id } },
updater(store) {
const conn = store.get(connectionId);
if (conn) {
ConnectionHandler.deleteNode(conn, 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."),
lastVersion.title,
),
variant: "danger",
label: __("Archive"),
},
);
};
const handleDelete = () => {
confirm(
() =>
@@ -141,6 +245,41 @@ export function DocumentListItem(props: {
);
};
const handleUnarchive = () => {
unarchiveDocument({
variables: { input: { documentId: document.id } },
updater(store) {
const conn = store.get(connectionId);
if (conn) {
ConnectionHandler.deleteNode(conn, document.id);
}
},
onCompleted(_, errors) {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Failed to unarchive document"), errors),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Document unarchived successfully."),
variant: "success",
});
},
onError(error) {
toast({ title: __("Error"), description: error.message, variant: "error" });
},
});
};
const hasRowAction
= (document.canArchive && document.status === "ACTIVE")
|| (document.canUnarchive && document.status === "ARCHIVED")
|| document.canDelete;
return (
<Tr
to={`/organizations/${organizationId}/documents/${document.id}`}
@@ -190,17 +329,37 @@ export function DocumentListItem(props: {
</Td>
{hasAnyAction && (
<Td noLink width={50} className="text-end w-18">
<ActionDropdown>
{document.canDelete && (
<DropdownItem
variant="danger"
icon={IconTrashCan}
onClick={handleDelete}
>
{__("Delete")}
</DropdownItem>
)}
</ActionDropdown>
{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>