From 5f7d4689aa4ca2f885c45edadf62b4f095f9aea9 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Tue, 10 Feb 2026 11:32:20 +0100 Subject: [PATCH] Simplify UX of compliance access management Signed-off-by: Bryan Frimin --- ...iancePageAccessListItemFragment.graphql.ts | 9 +- .../CompliancePageAccessListQuery.graphql.ts | 8 +- .../CompliancePageAccessPageQuery.graphql.ts | 8 +- ...pliancePageAccessDialogMutation.graphql.ts | 9 +- ...CenterAccessGraphUpdateMutation.graphql.ts | 15 +- .../src/hooks/graph/TrustCenterAccessGraph.ts | 2 +- .../_components/CompliancePageAccessList.tsx | 2 - .../CompliancePageAccessListItem.tsx | 106 ++++++-------- .../NewCompliancePageAccessDialog.tsx | 1 - .../TrustCenterAccessEditDialog.tsx | 19 +-- packages/ui/src/Atoms/Icons/IconArchive.tsx | 13 ++ packages/ui/src/Atoms/Icons/index.tsx | 1 + pkg/coredata/migrations/20260210T120000Z.sql | 14 ++ pkg/coredata/trust_center_access.go | 40 +++--- pkg/coredata/trust_center_access_state.go | 57 ++++++++ pkg/probo/trust_center_access_service.go | 10 +- .../compliancepage/membership_middleware.go | 3 +- pkg/server/api/console/v1/schema.graphql | 19 ++- pkg/server/api/console/v1/schema/schema.go | 134 +++++++++++++----- .../console/v1/types/trust_center_access.go | 24 ++-- pkg/server/api/console/v1/types/types.go | 5 +- pkg/server/api/console/v1/v1_resolver.go | 2 +- pkg/trust/trust_center_access_service.go | 12 +- 23 files changed, 323 insertions(+), 190 deletions(-) create mode 100644 packages/ui/src/Atoms/Icons/IconArchive.tsx create mode 100644 pkg/coredata/migrations/20260210T120000Z.sql create mode 100644 pkg/coredata/trust_center_access_state.go diff --git a/apps/console/src/__generated__/core/CompliancePageAccessListItemFragment.graphql.ts b/apps/console/src/__generated__/core/CompliancePageAccessListItemFragment.graphql.ts index 0f617ad6f..e38b2069d 100644 --- a/apps/console/src/__generated__/core/CompliancePageAccessListItemFragment.graphql.ts +++ b/apps/console/src/__generated__/core/CompliancePageAccessListItemFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,9 +9,9 @@ // @ts-nocheck import { ReaderFragment } from 'relay-runtime'; +export type TrustCenterAccessState = "ACTIVE" | "INACTIVE"; import { FragmentRefs } from "relay-runtime"; export type CompliancePageAccessListItemFragment$data = { - readonly active: boolean; readonly activeCount: number; readonly canDelete: boolean; readonly canUpdate: boolean; @@ -21,6 +21,7 @@ export type CompliancePageAccessListItemFragment$data = { readonly id: string; readonly name: string; readonly pendingRequestCount: number; + readonly state: TrustCenterAccessState; readonly " $fragmentType": "CompliancePageAccessListItemFragment"; }; export type CompliancePageAccessListItemFragment$key = { @@ -66,7 +67,7 @@ const node: ReaderFragment = { "alias": null, "args": null, "kind": "ScalarField", - "name": "active", + "name": "state", "storageKey": null }, { @@ -121,6 +122,6 @@ const node: ReaderFragment = { "abstractKey": null }; -(node as any).hash = "e7120d76d08974b672e2ed1e66bf6cd7"; +(node as any).hash = "4e78486a2c8edc2b5f73fb64dbeb4006"; export default node; diff --git a/apps/console/src/__generated__/core/CompliancePageAccessListQuery.graphql.ts b/apps/console/src/__generated__/core/CompliancePageAccessListQuery.graphql.ts index d27918b97..6488791bb 100644 --- a/apps/console/src/__generated__/core/CompliancePageAccessListQuery.graphql.ts +++ b/apps/console/src/__generated__/core/CompliancePageAccessListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<3c657b1368adbcba6300b5b94eff92f2>> + * @generated SignedSource<<19cd995fe2d7406ddfadd5df6f2b2412>> * @lightSyntaxTransform * @nogrep */ @@ -256,7 +256,7 @@ return { "alias": null, "args": null, "kind": "ScalarField", - "name": "active", + "name": "state", "storageKey": null }, { @@ -349,12 +349,12 @@ return { ] }, "params": { - "cacheID": "75c843f8501c2e5bd2c1422f7cf35b5f", + "cacheID": "27720ebe7c7eee3a27c486be063d9f18", "id": null, "metadata": {}, "name": "CompliancePageAccessListQuery", "operationKind": "query", - "text": "query CompliancePageAccessListQuery(\n $after: CursorKey = null\n $first: Int = 10\n $order: TrustCenterAccessOrder = {field: CREATED_AT, direction: DESC}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...CompliancePageAccessListFragment_gOVF5\n id\n }\n}\n\nfragment CompliancePageAccessListFragment_gOVF5 on TrustCenter {\n accesses(first: $first, after: $after, orderBy: $order) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n ...CompliancePageAccessListItemFragment\n __typename\n }\n }\n }\n id\n}\n\nfragment CompliancePageAccessListItemFragment on TrustCenterAccess {\n id\n name\n email\n createdAt\n active\n activeCount\n pendingRequestCount\n hasAcceptedNonDisclosureAgreement\n canUpdate: permission(action: \"core:trust-center-access:update\")\n canDelete: permission(action: \"core:trust-center-access:delete\")\n}\n" + "text": "query CompliancePageAccessListQuery(\n $after: CursorKey = null\n $first: Int = 10\n $order: TrustCenterAccessOrder = {field: CREATED_AT, direction: DESC}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...CompliancePageAccessListFragment_gOVF5\n id\n }\n}\n\nfragment CompliancePageAccessListFragment_gOVF5 on TrustCenter {\n accesses(first: $first, after: $after, orderBy: $order) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n ...CompliancePageAccessListItemFragment\n __typename\n }\n }\n }\n id\n}\n\nfragment CompliancePageAccessListItemFragment on TrustCenterAccess {\n id\n name\n email\n createdAt\n state\n activeCount\n pendingRequestCount\n hasAcceptedNonDisclosureAgreement\n canUpdate: permission(action: \"core:trust-center-access:update\")\n canDelete: permission(action: \"core:trust-center-access:delete\")\n}\n" } }; })(); diff --git a/apps/console/src/__generated__/core/CompliancePageAccessPageQuery.graphql.ts b/apps/console/src/__generated__/core/CompliancePageAccessPageQuery.graphql.ts index 93397903d..c0d6e9a1f 100644 --- a/apps/console/src/__generated__/core/CompliancePageAccessPageQuery.graphql.ts +++ b/apps/console/src/__generated__/core/CompliancePageAccessPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<2ba5d01d3d1dbb4956627eae14df34ab>> * @lightSyntaxTransform * @nogrep */ @@ -265,7 +265,7 @@ return { "alias": null, "args": null, "kind": "ScalarField", - "name": "active", + "name": "state", "storageKey": null }, { @@ -362,12 +362,12 @@ return { ] }, "params": { - "cacheID": "651e7d8df512ab49f09e5b086a488266", + "cacheID": "6e4d46f7b3fdff2e4650ea27d96b8c5b", "id": null, "metadata": {}, "name": "CompliancePageAccessPageQuery", "operationKind": "query", - "text": "query CompliancePageAccessPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n compliancePage: trustCenter {\n id\n canCreateAccess: permission(action: \"core:trust-center-access:create\")\n ...CompliancePageAccessListFragment\n }\n }\n id\n }\n}\n\nfragment CompliancePageAccessListFragment on TrustCenter {\n accesses(first: 10, orderBy: {field: CREATED_AT, direction: DESC}) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n ...CompliancePageAccessListItemFragment\n __typename\n }\n }\n }\n id\n}\n\nfragment CompliancePageAccessListItemFragment on TrustCenterAccess {\n id\n name\n email\n createdAt\n active\n activeCount\n pendingRequestCount\n hasAcceptedNonDisclosureAgreement\n canUpdate: permission(action: \"core:trust-center-access:update\")\n canDelete: permission(action: \"core:trust-center-access:delete\")\n}\n" + "text": "query CompliancePageAccessPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n compliancePage: trustCenter {\n id\n canCreateAccess: permission(action: \"core:trust-center-access:create\")\n ...CompliancePageAccessListFragment\n }\n }\n id\n }\n}\n\nfragment CompliancePageAccessListFragment on TrustCenter {\n accesses(first: 10, orderBy: {field: CREATED_AT, direction: DESC}) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n ...CompliancePageAccessListItemFragment\n __typename\n }\n }\n }\n id\n}\n\nfragment CompliancePageAccessListItemFragment on TrustCenterAccess {\n id\n name\n email\n createdAt\n state\n activeCount\n pendingRequestCount\n hasAcceptedNonDisclosureAgreement\n canUpdate: permission(action: \"core:trust-center-access:update\")\n canDelete: permission(action: \"core:trust-center-access:delete\")\n}\n" } }; })(); diff --git a/apps/console/src/__generated__/core/NewCompliancePageAccessDialogMutation.graphql.ts b/apps/console/src/__generated__/core/NewCompliancePageAccessDialogMutation.graphql.ts index 3992c4434..72f35db63 100644 --- a/apps/console/src/__generated__/core/NewCompliancePageAccessDialogMutation.graphql.ts +++ b/apps/console/src/__generated__/core/NewCompliancePageAccessDialogMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<953f0fbe58d5428b76797adcd479aa06>> + * @generated SignedSource<<8d43c1f38ab045ccce31f220486fcb88>> * @lightSyntaxTransform * @nogrep */ @@ -11,7 +11,6 @@ import { ConcreteRequest } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type CreateTrustCenterAccessInput = { - active: boolean; email: string; name: string; trustCenterId: string; @@ -182,7 +181,7 @@ return { "alias": null, "args": null, "kind": "ScalarField", - "name": "active", + "name": "state", "storageKey": null }, { @@ -260,12 +259,12 @@ return { ] }, "params": { - "cacheID": "35604e9722512d1d60d70b6bce7d866b", + "cacheID": "ed150f3e2ca1f480ae7d18be081dfec1", "id": null, "metadata": {}, "name": "NewCompliancePageAccessDialogMutation", "operationKind": "mutation", - "text": "mutation NewCompliancePageAccessDialogMutation(\n $input: CreateTrustCenterAccessInput!\n) {\n createTrustCenterAccess(input: $input) {\n trustCenterAccessEdge {\n cursor\n node {\n id\n ...CompliancePageAccessListItemFragment\n }\n }\n }\n}\n\nfragment CompliancePageAccessListItemFragment on TrustCenterAccess {\n id\n name\n email\n createdAt\n active\n activeCount\n pendingRequestCount\n hasAcceptedNonDisclosureAgreement\n canUpdate: permission(action: \"core:trust-center-access:update\")\n canDelete: permission(action: \"core:trust-center-access:delete\")\n}\n" + "text": "mutation NewCompliancePageAccessDialogMutation(\n $input: CreateTrustCenterAccessInput!\n) {\n createTrustCenterAccess(input: $input) {\n trustCenterAccessEdge {\n cursor\n node {\n id\n ...CompliancePageAccessListItemFragment\n }\n }\n }\n}\n\nfragment CompliancePageAccessListItemFragment on TrustCenterAccess {\n id\n name\n email\n createdAt\n state\n activeCount\n pendingRequestCount\n hasAcceptedNonDisclosureAgreement\n canUpdate: permission(action: \"core:trust-center-access:update\")\n canDelete: permission(action: \"core:trust-center-access:delete\")\n}\n" } }; })(); diff --git a/apps/console/src/__generated__/core/TrustCenterAccessGraphUpdateMutation.graphql.ts b/apps/console/src/__generated__/core/TrustCenterAccessGraphUpdateMutation.graphql.ts index 81e0d1556..828977595 100644 --- a/apps/console/src/__generated__/core/TrustCenterAccessGraphUpdateMutation.graphql.ts +++ b/apps/console/src/__generated__/core/TrustCenterAccessGraphUpdateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<8060916e92063b503b064a3ce7675e1a>> * @lightSyntaxTransform * @nogrep */ @@ -9,13 +9,14 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; +export type TrustCenterAccessState = "ACTIVE" | "INACTIVE"; export type TrustCenterDocumentAccessStatus = "GRANTED" | "REJECTED" | "REQUESTED" | "REVOKED"; export type UpdateTrustCenterAccessInput = { - active?: boolean | null | undefined; documents?: ReadonlyArray | null | undefined; id: string; name?: string | null | undefined; reports?: ReadonlyArray | null | undefined; + state?: TrustCenterAccessState | null | undefined; trustCenterFiles?: ReadonlyArray | null | undefined; }; export type TrustCenterDocumentAccessInput = { @@ -28,7 +29,6 @@ export type TrustCenterAccessGraphUpdateMutation$variables = { export type TrustCenterAccessGraphUpdateMutation$data = { readonly updateTrustCenterAccess: { readonly trustCenterAccess: { - readonly active: boolean; readonly activeCount: number; readonly createdAt: string; readonly email: string; @@ -36,6 +36,7 @@ export type TrustCenterAccessGraphUpdateMutation$data = { readonly id: string; readonly name: string; readonly pendingRequestCount: number; + readonly state: TrustCenterAccessState; readonly updatedAt: string; }; }; @@ -101,7 +102,7 @@ v1 = [ "alias": null, "args": null, "kind": "ScalarField", - "name": "active", + "name": "state", "storageKey": null }, { @@ -164,16 +165,16 @@ return { "selections": (v1/*: any*/) }, "params": { - "cacheID": "1367c160aa881878e5eed860509e7610", + "cacheID": "bfc1209ce4d692ddb3692305aff7a33d", "id": null, "metadata": {}, "name": "TrustCenterAccessGraphUpdateMutation", "operationKind": "mutation", - "text": "mutation TrustCenterAccessGraphUpdateMutation(\n $input: UpdateTrustCenterAccessInput!\n) {\n updateTrustCenterAccess(input: $input) {\n trustCenterAccess {\n id\n email\n name\n active\n hasAcceptedNonDisclosureAgreement\n createdAt\n updatedAt\n pendingRequestCount\n activeCount\n }\n }\n}\n" + "text": "mutation TrustCenterAccessGraphUpdateMutation(\n $input: UpdateTrustCenterAccessInput!\n) {\n updateTrustCenterAccess(input: $input) {\n trustCenterAccess {\n id\n email\n name\n state\n hasAcceptedNonDisclosureAgreement\n createdAt\n updatedAt\n pendingRequestCount\n activeCount\n }\n }\n}\n" } }; })(); -(node as any).hash = "7e5d2ce261f3e877ef54e5acfbd00bde"; +(node as any).hash = "750e100701bf905c8fff08f3f5155b5b"; export default node; diff --git a/apps/console/src/hooks/graph/TrustCenterAccessGraph.ts b/apps/console/src/hooks/graph/TrustCenterAccessGraph.ts index 0e98849fe..f4e4f804f 100644 --- a/apps/console/src/hooks/graph/TrustCenterAccessGraph.ts +++ b/apps/console/src/hooks/graph/TrustCenterAccessGraph.ts @@ -9,7 +9,7 @@ export const updateTrustCenterAccessMutation = graphql` id email name - active + state hasAcceptedNonDisclosureAgreement createdAt updatedAt diff --git a/apps/console/src/pages/organizations/compliance-page/access/_components/CompliancePageAccessList.tsx b/apps/console/src/pages/organizations/compliance-page/access/_components/CompliancePageAccessList.tsx index 885dd6ccc..ea0dab526 100644 --- a/apps/console/src/pages/organizations/compliance-page/access/_components/CompliancePageAccessList.tsx +++ b/apps/console/src/pages/organizations/compliance-page/access/_components/CompliancePageAccessList.tsx @@ -77,7 +77,6 @@ export function CompliancePageAccessList(props: { {__("Name")} {__("Email")} {__("Date")} - {__("Active")} {__("Access")} {__("Requests")} {__("NDA")} @@ -89,7 +88,6 @@ export function CompliancePageAccessList(props: { ))} diff --git a/apps/console/src/pages/organizations/compliance-page/access/_components/CompliancePageAccessListItem.tsx b/apps/console/src/pages/organizations/compliance-page/access/_components/CompliancePageAccessListItem.tsx index e9edf8ba7..dab790ea9 100644 --- a/apps/console/src/pages/organizations/compliance-page/access/_components/CompliancePageAccessListItem.tsx +++ b/apps/console/src/pages/organizations/compliance-page/access/_components/CompliancePageAccessListItem.tsx @@ -1,12 +1,13 @@ import { formatDate } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; -import { Button, IconCheckmark1, IconCrossLargeX, IconPencil, IconTrashCan, Td, Tr } from "@probo/ui"; +import { ActionDropdown, DropdownItem, IconArchive, IconCheckmark1, IconPencil, IconRotateCw, Td, Tr } from "@probo/ui"; import { useCallback, useState } from "react"; import { useFragment } from "react-relay"; -import { type DataID, graphql } from "relay-runtime"; +import { graphql } from "relay-runtime"; -import type { CompliancePageAccessListItem_deleteMutation } from "#/__generated__/core/CompliancePageAccessListItem_deleteMutation.graphql"; import type { CompliancePageAccessListItemFragment$key } from "#/__generated__/core/CompliancePageAccessListItemFragment.graphql"; +import type { TrustCenterAccessGraphUpdateMutation } from "#/__generated__/core/TrustCenterAccessGraphUpdateMutation.graphql"; +import { updateTrustCenterAccessMutation } from "#/hooks/graph/TrustCenterAccessGraph"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { TrustCenterAccessEditDialog } from "#/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessEditDialog"; @@ -16,79 +17,61 @@ const fragment = graphql` name email createdAt - active + state activeCount pendingRequestCount hasAcceptedNonDisclosureAgreement canUpdate: permission(action: "core:trust-center-access:update") - canDelete: permission(action: "core:trust-center-access:delete") - } -`; - -const deleteCompliancePageAccessMutation = graphql` - mutation CompliancePageAccessListItem_deleteMutation( - $input: DeleteTrustCenterAccessInput! - $connections: [ID!]! - ) { - deleteTrustCenterAccess(input: $input) { - deletedTrustCenterAccessId @deleteEdge(connections: $connections) - } } `; export function CompliancePageAccessListItem(props: { - connectionId: DataID; fragmentRef: CompliancePageAccessListItemFragment$key; dialogOpen: boolean; }) { - const { connectionId, fragmentRef, dialogOpen: initialDialogOpen } = props; + const { fragmentRef, dialogOpen: initialDialogOpen } = props; const { __ } = useTranslate(); const [dialogOpen, setDialogOpen] = useState(initialDialogOpen); const access = useFragment(fragment, fragmentRef); - const [deleteInvitation, isDeleting] = useMutationWithToasts( - deleteCompliancePageAccessMutation, + const isActive = access.state === "ACTIVE"; + + const [updateAccess, isUpdating] = useMutationWithToasts( + updateTrustCenterAccessMutation, { - successMessage: __("Access deleted successfully"), - errorMessage: __("Failed to delete access"), + successMessage: isActive + ? __("Access deactivated successfully") + : __("Access activated successfully"), + errorMessage: isActive + ? __("Failed to deactivate access") + : __("Failed to activate access"), }, ); - const handleDelete = useCallback( - async (id: string) => { - await deleteInvitation({ - variables: { - input: { id }, - connections: [connectionId], + const handleToggleState = useCallback(() => { + updateAccess({ + variables: { + input: { + id: access.id, + name: access.name, + state: isActive ? "INACTIVE" : "ACTIVE", }, - }); - }, - [deleteInvitation, connectionId], - ); + }, + }); + }, [updateAccess, access.id, access.name, isActive]); return ( <> access.canUpdate && setDialogOpen(true)} - className="cursor-pointer hover:bg-bg-secondary transition-colors" + onClick={() => access.canUpdate && isActive && setDialogOpen(true)} + className={`cursor-pointer hover:bg-bg-secondary transition-colors${!isActive ? " opacity-50" : ""}`} > {access.name} {access.email} {formatDate(access.createdAt)} - -
- {access.active - ? ( - - ) - : ( - - )} -
- {access.activeCount} {access.pendingRequestCount > 0 ? access.pendingRequestCount : ""} @@ -106,25 +89,30 @@ export function CompliancePageAccessListItem(props: { onClick={e => e.stopPropagation()} > {access.canUpdate && ( -