From e6dcb7ea00bd578ee2a62b614319eb2a1b533f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Thu, 16 Jul 2026 13:58:20 +0200 Subject: [PATCH] Add commitment reordering with rank move buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let admins reorder commitment groups and the cards within each group from the console Commitments tab using up/down buttons, driven by the existing rank-aware update mutations. Make the (parent, rank) unique constraints on the commitment tables DEFERRABLE INITIALLY DEFERRED. Reordering shifts several rows in one UPDATE, which transiently duplicates a rank and tripped the immediately enforced constraint with a 23505 error. This matches the other rank-ordered tables (references, compliance frameworks). Signed-off-by: Émile Ré --- .../CompliancePageCommitmentGroupList.tsx | 40 +++++++++++- .../CompliancePageCommitmentGroupListItem.tsx | 65 +++++++++++++++++-- .../CompliancePageCommitmentListItem.tsx | 27 ++++++-- pkg/coredata/migrations/20260716T115429Z.sql | 40 ++++++++++++ 4 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 pkg/coredata/migrations/20260716T115429Z.sql diff --git a/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupList.tsx b/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupList.tsx index 83177af7b..0bef86248 100644 --- a/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupList.tsx +++ b/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupList.tsx @@ -26,6 +26,8 @@ import { graphql } from "relay-runtime"; import type { CompliancePageCommitmentGroupListFragment$key } from "#/__generated__/core/CompliancePageCommitmentGroupListFragment.graphql"; import type { CompliancePageCommitmentGroupListRefetchQuery } from "#/__generated__/core/CompliancePageCommitmentGroupListRefetchQuery.graphql"; +import type { CompliancePageCommitmentGroupListUpdateRankMutation } from "#/__generated__/core/CompliancePageCommitmentGroupListUpdateRankMutation.graphql"; +import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { CompliancePageCommitmentGroupDialog, type CompliancePageCommitmentGroupDialogRef } from "./CompliancePageCommitmentGroupDialog"; import { CompliancePageCommitmentGroupListItem } from "./CompliancePageCommitmentGroupListItem"; @@ -37,6 +39,7 @@ const fragment = graphql` edges { node { id + rank ...CompliancePageCommitmentGroupListItemFragment } } @@ -44,6 +47,19 @@ const fragment = graphql` } `; +const updateRankMutation = graphql` + mutation CompliancePageCommitmentGroupListUpdateRankMutation( + $input: UpdateCompliancePortalCommitmentGroupInput! + ) { + updateCompliancePortalCommitmentGroup(input: $input) { + compliancePortalCommitmentGroup { + id + rank + } + } + } +`; + export function CompliancePageCommitmentGroupList(props: { fragmentRef: CompliancePageCommitmentGroupListFragment$key; trustCenterId: string; @@ -59,10 +75,27 @@ export function CompliancePageCommitmentGroupList(props: { CompliancePageCommitmentGroupListFragment$key >(fragment, fragmentRef); + const [updateRank, isReordering] = useMutationWithToasts( + updateRankMutation, + { successMessage: __("Order updated successfully"), errorMessage: __("Failed to update order") }, + ); + const onChanged = () => refetch({}, { fetchPolicy: "network-only" }); const groups = data.commitmentGroups.edges.map(edge => edge.node); + const moveGroup = async (index: number, direction: "up" | "down") => { + const target = direction === "up" ? groups[index - 1] : groups[index + 1]; + if (!target) { + return; + } + + await updateRank({ + variables: { input: { id: groups[index].id, rank: target.rank } }, + onSuccess: onChanged, + }); + }; + return (
@@ -87,12 +120,17 @@ export function CompliancePageCommitmentGroupList(props: { ) : (
- {groups.map(group => ( + {groups.map((group, index) => ( dialogRef.current?.openEdit(g)} onChanged={onChanged} + isFirst={index === 0} + isLast={index === groups.length - 1} + isReordering={isReordering} + onMoveUp={() => void moveGroup(index, "up")} + onMoveDown={() => void moveGroup(index, "down")} /> ))}
diff --git a/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupListItem.tsx b/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupListItem.tsx index 284547fee..7b7273ada 100644 --- a/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupListItem.tsx +++ b/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupListItem.tsx @@ -20,13 +20,14 @@ import { sprintf } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; -import { Button, Card, Dialog, DialogContent, DialogFooter, IconPencil, IconPlusLarge, IconTrashCan, Spinner, Table, Tbody, Td, Th, Thead, Tr, useDialogRef } from "@probo/ui"; +import { Button, Card, Dialog, DialogContent, DialogFooter, IconChevronDown, IconChevronUp, IconPencil, IconPlusLarge, IconTrashCan, Spinner, Table, Tbody, Td, Th, Thead, Tr, useDialogRef } from "@probo/ui"; import { useRef } from "react"; import { useFragment } from "react-relay"; import { graphql } from "relay-runtime"; import type { CompliancePageCommitmentGroupListItemDeleteMutation } from "#/__generated__/core/CompliancePageCommitmentGroupListItemDeleteMutation.graphql"; import type { CompliancePageCommitmentGroupListItemFragment$data, CompliancePageCommitmentGroupListItemFragment$key } from "#/__generated__/core/CompliancePageCommitmentGroupListItemFragment.graphql"; +import type { CompliancePageCommitmentGroupListItemUpdateRankMutation } from "#/__generated__/core/CompliancePageCommitmentGroupListItemUpdateRankMutation.graphql"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { CompliancePageCommitmentDialog, type CompliancePageCommitmentDialogRef } from "./CompliancePageCommitmentDialog"; @@ -42,6 +43,19 @@ const deleteGroupMutation = graphql` } `; +const updateCommitmentRankMutation = graphql` + mutation CompliancePageCommitmentGroupListItemUpdateRankMutation( + $input: UpdateCompliancePortalCommitmentInput! + ) { + updateCompliancePortalCommitment(input: $input) { + compliancePortalCommitment { + id + rank + } + } + } +`; + const fragment = graphql` fragment CompliancePageCommitmentGroupListItemFragment on CompliancePortalCommitmentGroup { id @@ -54,6 +68,7 @@ const fragment = graphql` edges { node { id + rank ...CompliancePageCommitmentListItemFragment } } @@ -65,8 +80,13 @@ export function CompliancePageCommitmentGroupListItem(props: { fragmentRef: CompliancePageCommitmentGroupListItemFragment$key; onEdit: (group: CompliancePageCommitmentGroupListItemFragment$data) => void; onChanged: () => void; + isFirst: boolean; + isLast: boolean; + isReordering: boolean; + onMoveUp: () => void; + onMoveDown: () => void; }) { - const { fragmentRef, onEdit, onChanged } = props; + const { fragmentRef, onEdit, onChanged, isFirst, isLast, isReordering, onMoveUp, onMoveDown } = props; const { __ } = useTranslate(); const group = useFragment(fragment, fragmentRef); @@ -77,9 +97,27 @@ export function CompliancePageCommitmentGroupListItem(props: { deleteGroupMutation, { successMessage: __("Group deleted successfully"), errorMessage: __("Failed to delete group") }, ); + const [updateCommitmentRank, isReorderingCommitment] = useMutationWithToasts< + CompliancePageCommitmentGroupListItemUpdateRankMutation + >( + updateCommitmentRankMutation, + { successMessage: __("Order updated successfully"), errorMessage: __("Failed to update order") }, + ); const commitments = group.commitments.edges.map(edge => edge.node); + const moveCommitment = async (index: number, direction: "up" | "down") => { + const target = direction === "up" ? commitments[index - 1] : commitments[index + 1]; + if (!target) { + return; + } + + await updateCommitmentRank({ + variables: { input: { id: commitments[index].id, rank: target.rank } }, + onSuccess: onChanged, + }); + }; + const handleDelete = async () => { await deleteGroup({ variables: { input: { id: group.id } }, @@ -99,7 +137,21 @@ export function CompliancePageCommitmentGroupListItem(props: {
{group.canUpdate && ( -