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( const canSendAnySignatureNotifications = documents.some(
({ canSendSigningNotifications }) => canSendSigningNotifications, ({ canSendSigningNotifications }) => canSendSigningNotifications,
); );
const hasAnyAction = tab === "ARCHIVED" ? canUnarchiveAny || canDeleteAny : canDeleteAny || canUpdateAny; const hasAnyAction = tab === "ARCHIVED" ? canUnarchiveAny || canDeleteAny : canArchiveAny || canDeleteAny || canUpdateAny;
useEffect(() => { useEffect(() => {
onConnectionIdChange(connectionId); onConnectionIdChange(connectionId);

View File

@@ -12,21 +12,43 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE. // 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 { 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 { 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_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 type { DocumentListItemFragment$key } from "#/__generated__/core/DocumentListItemFragment.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
const fragment = graphql` const fragment = graphql`
fragment DocumentListItemFragment on Document { fragment DocumentListItemFragment on Document {
id id
status
updatedAt updatedAt
canArchive: permission(action: "core:document:archive")
canDelete: permission(action: "core:document:delete") canDelete: permission(action: "core:document:delete")
canUnarchive: permission(action: "core:document:unarchive")
defaultApprovers { defaultApprovers {
id id
fullName 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` const deleteDocumentMutation = graphql`
mutation DocumentListItem_deleteMutation( mutation DocumentListItem_deleteMutation(
$input: DeleteDocumentInput! $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: { export function DocumentListItem(props: {
fragmentRef: DocumentListItemFragment$key; fragmentRef: DocumentListItemFragment$key;
connectionId: DataID; connectionId: DataID;
@@ -94,7 +150,10 @@ export function DocumentListItem(props: {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const { toast } = useToast();
const [archiveDocument, isArchiving] = useMutation<DocumentListItem_archiveMutation>(archiveDocumentMutation);
const [deleteDocument] = useMutation<DocumentListItem_deleteMutation>(deleteDocumentMutation); const [deleteDocument] = useMutation<DocumentListItem_deleteMutation>(deleteDocumentMutation);
const [unarchiveDocument, isUnarchiving] = useMutation<DocumentListItem_unarchiveMutation>(unarchiveDocumentMutation);
const confirm = useConfirm(); const confirm = useConfirm();
const document = useFragment<DocumentListItemFragment$key>( const document = useFragment<DocumentListItemFragment$key>(
fragment, fragment,
@@ -117,6 +176,51 @@ export function DocumentListItem(props: {
PUBLISHED: __("Published"), PUBLISHED: __("Published"),
} as const; } 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 = () => { const handleDelete = () => {
confirm( 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 ( return (
<Tr <Tr
to={`/organizations/${organizationId}/documents/${document.id}`} to={`/organizations/${organizationId}/documents/${document.id}`}
@@ -190,7 +329,26 @@ export function DocumentListItem(props: {
</Td> </Td>
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end w-18"> <Td noLink width={50} className="text-end w-18">
{hasRowAction && (
<ActionDropdown> <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 && ( {document.canDelete && (
<DropdownItem <DropdownItem
variant="danger" variant="danger"
@@ -201,6 +359,7 @@ export function DocumentListItem(props: {
</DropdownItem> </DropdownItem>
)} )}
</ActionDropdown> </ActionDropdown>
)}
</Td> </Td>
)} )}
</Tr> </Tr>