From 436151b0fe745a6432690e8a856b869943ae1347 Mon Sep 17 00:00:00 2001 From: gearnode Date: Sat, 1 Mar 2025 18:53:05 +0100 Subject: [PATCH] Fix relay connection handling for evidence lists Implements proper connection identifiers for evidence lists with correct pagination support. Removes custom types in favor of generated ones, replaces useMemo with useCallback for connection IDs, and increases the evidence limit from 10 to 50 items. Ensures UI updates correctly when adding new evidence to tasks. Signed-off-by: gearnode --- .../console/src/pages/ControlOverviewPage.tsx | 175 +++++----- .../ControlOverviewPageQuery.graphql.ts | 302 +++++++++++------- 2 files changed, 268 insertions(+), 209 deletions(-) diff --git a/apps/console/src/pages/ControlOverviewPage.tsx b/apps/console/src/pages/ControlOverviewPage.tsx index c2bd6ab31..40ff80ab5 100644 --- a/apps/console/src/pages/ControlOverviewPage.tsx +++ b/apps/console/src/pages/ControlOverviewPage.tsx @@ -4,7 +4,7 @@ import { useState, useRef, DragEvent, - useMemo, + useCallback, } from "react"; import { useParams, useNavigate } from "react-router"; import { @@ -49,10 +49,7 @@ import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from ". import type { ControlOverviewPageUpdateTaskStateMutation as ControlOverviewPageUpdateTaskStateMutationType } from "./__generated__/ControlOverviewPageUpdateTaskStateMutation.graphql"; import type { ControlOverviewPageCreateTaskMutation as ControlOverviewPageCreateTaskMutationType } from "./__generated__/ControlOverviewPageCreateTaskMutation.graphql"; import type { ControlOverviewPageDeleteTaskMutation as ControlOverviewPageDeleteTaskMutationType } from "./__generated__/ControlOverviewPageDeleteTaskMutation.graphql"; -import type { - ControlOverviewPageUploadEvidenceMutation as ControlOverviewPageUploadEvidenceMutationType, - EvidenceState, -} from "./__generated__/ControlOverviewPageUploadEvidenceMutation.graphql"; +import type { ControlOverviewPageUploadEvidenceMutation as ControlOverviewPageUploadEvidenceMutationType } from "./__generated__/ControlOverviewPageUploadEvidenceMutation.graphql"; const controlOverviewPageQuery = graphql` query ControlOverviewPageQuery($controlId: ID!) { @@ -64,13 +61,16 @@ const controlOverviewPageQuery = graphql` state category tasks(first: 100) @connection(key: "ControlOverviewPage_tasks") { + __id edges { node { id name description state - evidences(first: 10) { + evidences(first: 50) + @connection(key: "ControlOverviewPage_evidences") { + __id edges { node { id @@ -152,29 +152,6 @@ const uploadEvidenceMutation = graphql` } `; -// Use the Relay-generated types with extensions for fields that might not be in the generated types yet -type EvidenceNode = { - id: string; - name: string; - fileUrl: string; - mimeType: string; - size: number; - state: EvidenceState; - createdAt: string; -}; - -type TaskNode = { - id: string; - name: string; - description: string; - state: string; - evidences?: { - edges: Array<{ - node: EvidenceNode | null; - } | null>; - }; -}; - function ControlOverviewPageContent({ queryRef, }: { @@ -199,9 +176,6 @@ function ControlOverviewPageContent({ useMutation( uploadEvidenceMutation ); - const control = data.control; - const tasks = - control?.tasks?.edges.map((edge) => edge?.node as TaskNode) ?? []; const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false); const [newTaskName, setNewTaskName] = useState(""); @@ -232,13 +206,19 @@ function ControlOverviewPageContent({ string | null >(null); - // Get the connection ID for Relay - const connectionId = useMemo(() => { - // The connection key is defined in the GraphQL query as "ControlOverviewPage_tasks" - return control?.id ? `client:${control.id}:tasks{"first":100}` : null; - }, [control?.id]); + const tasks = data.control.tasks?.edges.map((edge) => edge.node) || []; + + const getEvidenceConnectionId = useCallback( + (taskId: string) => { + const task = tasks.find((t) => t.id === taskId); + if (task?.evidences?.__id) { + return task.evidences.__id; + } + return null; + }, + [tasks] + ); - // Add global drag event handlers to detect when a file is being dragged useEffect(() => { const handleDragEnter = (e: globalThis.DragEvent) => { e.preventDefault(); @@ -289,11 +269,9 @@ function ControlOverviewPageContent({ }, optimisticResponse: { updateTaskState: { - taskEdge: { - node: { - id: taskId, - state: newState, - }, + task: { + id: taskId, + state: newState, }, }, }, @@ -325,7 +303,7 @@ function ControlOverviewPageContent({ return; } - if (!control?.id) { + if (!data.control.id) { toast({ title: "Error creating task", description: "Control ID is missing", @@ -336,9 +314,9 @@ function ControlOverviewPageContent({ createTask({ variables: { - connections: [], + connections: [`${data.control.tasks?.__id}`], input: { - controlId: control.id, + controlId: data.control.id, name: newTaskName, description: newTaskDescription, }, @@ -372,7 +350,7 @@ function ControlOverviewPageContent({ deleteTask({ variables: { - connections: connectionId ? [connectionId] : [], + connections: [`${data.control.tasks?.__id}`], input: { taskId: taskToDelete.id, }, @@ -413,6 +391,9 @@ function ControlOverviewPageContent({ const file = fileInputRef.current.files[0]; + // Get the evidence connection ID for this task + const evidenceConnectionId = getEvidenceConnectionId(taskForEvidence.id); + uploadEvidence({ variables: { input: { @@ -420,7 +401,7 @@ function ControlOverviewPageContent({ name: evidenceName || file.name, file: null, }, - connections: connectionId ? [connectionId] : [], + connections: evidenceConnectionId ? [evidenceConnectionId] : [], }, uploadables: { "input.file": file, @@ -479,6 +460,9 @@ function ControlOverviewPageContent({ description: `Uploading ${file.name}...`, }); + // Get the evidence connection ID for this task + const evidenceConnectionId = getEvidenceConnectionId(taskId); + uploadEvidence({ variables: { input: { @@ -486,7 +470,7 @@ function ControlOverviewPageContent({ name: file.name, file: null, }, - connections: connectionId ? [connectionId] : [], + connections: evidenceConnectionId ? [evidenceConnectionId] : [], }, uploadables: { "input.file": file, @@ -555,12 +539,12 @@ function ControlOverviewPageContent({ return ( <> - {control?.name || "Control"} - Probo + {data.control.name || "Control"} - Probo
-

{control?.name}

+

{data.control.name}

-

{control?.description}

+

{data.control.description}

@@ -587,14 +571,14 @@ function ControlOverviewPageContent({ >
- {control?.state === "IMPLEMENTED" + {data.control.state === "IMPLEMENTED" ? "Validated" : "Not validated"} @@ -818,60 +802,57 @@ function ControlOverviewPageContent({ {expandedEvidenceTaskId === task.id && (
- {task.evidences.edges.map( - (edge: { node: EvidenceNode | null } | null) => { - const evidence = edge?.node; - if (!evidence) return null; + {task.evidences.edges.map((edge) => { + if (!edge) return null; + const evidence = edge.node; + if (!evidence) return null; - return ( -
-
- {getFileIcon(evidence.mimeType)} -
-
- {evidence.name} -
-
- - {formatFileSize(evidence.size)} - - • - - {formatDate(evidence.createdAt)} - -
+ return ( +
+
+ {getFileIcon(evidence.mimeType)} +
+
+ {evidence.name} +
+
+ {formatFileSize(evidence.size)} + • + + {formatDate(evidence.createdAt)} +
-
- {evidence.mimeType.startsWith("image/") && ( - - - - )} +
+
+ {evidence.mimeType.startsWith("image/") && ( - + -
+ )} + + +
- ); - } - )} +
+ ); + })}
)} diff --git a/apps/console/src/pages/__generated__/ControlOverviewPageQuery.graphql.ts b/apps/console/src/pages/__generated__/ControlOverviewPageQuery.graphql.ts index 4723e1469..ce7eac119 100644 --- a/apps/console/src/pages/__generated__/ControlOverviewPageQuery.graphql.ts +++ b/apps/console/src/pages/__generated__/ControlOverviewPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<2ce0834e0bd8b3d2937b34ebf91f9a69>> * @lightSyntaxTransform * @nogrep */ @@ -23,10 +23,12 @@ export type ControlOverviewPageQuery$data = { readonly name?: string; readonly state?: ControlState; readonly tasks?: { + readonly __id: string; readonly edges: ReadonlyArray<{ readonly node: { readonly description: string; readonly evidences: { + readonly __id: string; readonly edges: ReadonlyArray<{ readonly node: { readonly createdAt: string; @@ -108,11 +110,55 @@ v7 = { "name": "__typename", "storageKey": null }, -v8 = [ +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v9 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null +}, +v10 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}, +v11 = [ { "alias": null, "args": null, - "concreteType": "TaskEdge", + "concreteType": "EvidenceEdge", "kind": "LinkedField", "name": "edges", "plural": true, @@ -120,130 +166,65 @@ v8 = [ { "alias": null, "args": null, - "concreteType": "Task", + "concreteType": "Evidence", "kind": "LinkedField", "name": "node", "plural": false, "selections": [ (v2/*: any*/), - (v3/*: any*/), - (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "mimeType", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "size", + "storageKey": null + }, (v5/*: any*/), { "alias": null, - "args": [ - { - "kind": "Literal", - "name": "first", - "value": 10 - } - ], - "concreteType": "EvidenceConnection", - "kind": "LinkedField", - "name": "evidences", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "EvidenceEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Evidence", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "fileUrl", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "mimeType", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "size", - "storageKey": null - }, - (v5/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "evidences(first:10)" + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null }, (v7/*: any*/) ], "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } + (v8/*: any*/) ], "storageKey": null }, - { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "endCursor", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasNextPage", - "storageKey": null - } - ], - "storageKey": null - } + (v9/*: any*/), + (v10/*: any*/) ], -v9 = [ +v12 = [ { "kind": "Literal", "name": "first", "value": 100 } +], +v13 = [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } ]; return { "fragment": { @@ -275,7 +256,48 @@ return { "kind": "LinkedField", "name": "__ControlOverviewPage_tasks_connection", "plural": false, - "selections": (v8/*: any*/), + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TaskEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Task", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + { + "alias": "evidences", + "args": null, + "concreteType": "EvidenceConnection", + "kind": "LinkedField", + "name": "__ControlOverviewPage_evidences_connection", + "plural": false, + "selections": (v11/*: any*/), + "storageKey": null + }, + (v7/*: any*/) + ], + "storageKey": null + }, + (v8/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/), + (v10/*: any*/) + ], "storageKey": null } ], @@ -314,17 +336,67 @@ return { (v6/*: any*/), { "alias": null, - "args": (v9/*: any*/), + "args": (v12/*: any*/), "concreteType": "TaskConnection", "kind": "LinkedField", "name": "tasks", "plural": false, - "selections": (v8/*: any*/), + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TaskEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Task", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + { + "alias": null, + "args": (v13/*: any*/), + "concreteType": "EvidenceConnection", + "kind": "LinkedField", + "name": "evidences", + "plural": false, + "selections": (v11/*: any*/), + "storageKey": "evidences(first:50)" + }, + { + "alias": null, + "args": (v13/*: any*/), + "filters": null, + "handle": "connection", + "key": "ControlOverviewPage_evidences", + "kind": "LinkedHandle", + "name": "evidences" + }, + (v7/*: any*/) + ], + "storageKey": null + }, + (v8/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/), + (v10/*: any*/) + ], "storageKey": "tasks(first:100)" }, { "alias": null, - "args": (v9/*: any*/), + "args": (v12/*: any*/), "filters": null, "handle": "connection", "key": "ControlOverviewPage_tasks", @@ -341,10 +413,16 @@ return { ] }, "params": { - "cacheID": "ae3ac6c53719fa3d9c1953ff40c75005", + "cacheID": "9738657b3d99bf6d85fd5333972e68f1", "id": null, "metadata": { "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": null + }, { "count": null, "cursor": null, @@ -358,11 +436,11 @@ return { }, "name": "ControlOverviewPageQuery", "operationKind": "query", - "text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n evidences(first: 10) {\n edges {\n node {\n id\n fileUrl\n mimeType\n size\n state\n createdAt\n }\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" + "text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n evidences(first: 50) {\n edges {\n node {\n id\n fileUrl\n mimeType\n size\n state\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "ad8c4a4980229230ca295c4cb9a2c836"; +(node as any).hash = "5c2829a1e6bc18d96db9e608fbfc4d6f"; export default node;