Allow editing SOA generated document approvers inline

Adds an "Approvers" row to the SOA details card with the same inline
edit pattern as DocumentDetailsCard, calling updateDocument with
defaultApproverIds. Visible only after first publish, when the SOA
has an associated document.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-05-07 14:59:14 +02:00
parent 8bd5454ad7
commit a052d6f481

View File

@@ -45,7 +45,9 @@ import { z } from "zod";
import type { StatementOfApplicabilityDetailPageDeleteMutation } from "#/__generated__/core/StatementOfApplicabilityDetailPageDeleteMutation.graphql";
import type { StatementOfApplicabilityDetailPageQuery } from "#/__generated__/core/StatementOfApplicabilityDetailPageQuery.graphql";
import type { StatementOfApplicabilityDetailPageUpdateApproversMutation } from "#/__generated__/core/StatementOfApplicabilityDetailPageUpdateApproversMutation.graphql";
import type { StatementOfApplicabilityDetailPageUpdateMutation } from "#/__generated__/core/StatementOfApplicabilityDetailPageUpdateMutation.graphql";
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useOrganizationId } from "#/hooks/useOrganizationId";
@@ -62,8 +64,11 @@ export const statementOfApplicabilityDetailPageQuery = graphql`
updatedAt
document {
id
canUpdateDocument: permission(action: "core:document:update")
defaultApprovers {
id
fullName
emailAddress
}
}
canUpdate: permission(action: "core:statement-of-applicability:update")
@@ -101,6 +106,27 @@ const deleteMutation = graphql`
}
`;
const updateApproversMutation = graphql`
mutation StatementOfApplicabilityDetailPageUpdateApproversMutation(
$input: UpdateDocumentInput!
) {
updateDocument(input: $input) {
document {
id
defaultApprovers {
id
fullName
emailAddress
}
}
}
}
`;
const approversSchema = z.object({
approverIds: z.array(z.string()),
});
const StatementOfApplicabilityConnectionKey = "StatementsOfApplicabilityPage_statementsOfApplicability";
type Props = {
@@ -233,7 +259,59 @@ export default function StatementOfApplicabilityDetailPage(props: Props) {
});
};
const defaultApproverIds = (statementOfApplicability.document?.defaultApprovers ?? []).map(a => a.id);
const defaultApprovers = statementOfApplicability.document?.defaultApprovers ?? [];
const defaultApproverIds = defaultApprovers.map(a => a.id);
const documentId = statementOfApplicability.document?.id;
const canUpdateDocument = statementOfApplicability.document?.canUpdateDocument ?? false;
const [isEditingApprovers, setIsEditingApprovers] = useState(false);
const {
control: approversControl,
handleSubmit: handleApproversSubmit,
reset: resetApprovers,
} = useFormWithSchema(approversSchema, {
values: {
approverIds: defaultApproverIds,
},
});
const [updateApprovers, isUpdatingApprovers]
= useMutation<StatementOfApplicabilityDetailPageUpdateApproversMutation>(updateApproversMutation);
const handleUpdateApprovers = handleApproversSubmit((data) => {
if (!documentId) return;
updateApprovers({
variables: {
input: {
id: documentId,
defaultApproverIds: data.approverIds,
},
},
onCompleted() {
toast({
title: __("Success"),
description: __("Approvers updated successfully."),
variant: "success",
});
setIsEditingApprovers(false);
},
onError(error) {
toast({
title: __("Error"),
description: formatError(
__("Failed to update approvers"),
error as GraphQLError,
),
variant: "error",
});
},
});
});
const handleCancelApproversEdit = () => {
setIsEditingApprovers(false);
resetApprovers({ approverIds: defaultApproverIds });
};
const listUrl = `/organizations/${organizationId}/statements-of-applicability`;
@@ -343,7 +421,7 @@ export default function StatementOfApplicabilityDetailPage(props: Props) {
<div className="space-y-4">
<h2 className="text-base font-medium">{__("Details")}</h2>
<Card className="space-y-4" padded>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-3 gap-4">
<div>
<div className="text-xs text-txt-tertiary font-semibold mb-1">
{__("Created at")}
@@ -360,6 +438,58 @@ export default function StatementOfApplicabilityDetailPage(props: Props) {
{formatDate(statementOfApplicability.updatedAt)}
</div>
</div>
{documentId && (
<div>
<div className="text-xs text-txt-tertiary font-semibold mb-1">
{__("Approvers")}
</div>
{isEditingApprovers
? (
<div className="flex items-center gap-2">
<div className="flex-1">
<PeopleMultiSelectField
name="approverIds"
control={approversControl}
organizationId={organizationId}
selectedPeople={defaultApprovers.map(a => ({
id: a.id,
fullName: a.fullName,
emailAddress: a.emailAddress,
}))}
placeholder={__("Add approvers...")}
/>
</div>
<Button
variant="quaternary"
icon={IconCheckmark1}
onClick={() => void handleUpdateApprovers()}
disabled={isUpdatingApprovers}
/>
<Button
variant="quaternary"
icon={IconCrossLargeX}
onClick={handleCancelApproversEdit}
/>
</div>
)
: (
<div className="flex items-center gap-2">
<div className="text-sm text-txt-primary">
{defaultApprovers.length > 0
? defaultApprovers.map(a => a.fullName).join(", ")
: __("None")}
</div>
{canUpdateDocument && (
<Button
variant="quaternary"
icon={IconPencil}
onClick={() => setIsEditingApprovers(true)}
/>
)}
</div>
)}
</div>
)}
</div>
</Card>
</div>