From 5646286fc94d37bdcfc9f1af406d16dc35084026 Mon Sep 17 00:00:00 2001 From: gearnode Date: Sat, 1 Mar 2025 18:17:39 +0100 Subject: [PATCH] Add evidences list below each task Signed-off-by: gearnode --- .../console/src/pages/ControlOverviewPage.tsx | 402 +++++++++++++----- .../ControlOverviewPageQuery.graphql.ts | 90 +++- ...rviewPageUploadEvidenceMutation.graphql.ts | 4 +- 3 files changed, 380 insertions(+), 116 deletions(-) diff --git a/apps/console/src/pages/ControlOverviewPage.tsx b/apps/console/src/pages/ControlOverviewPage.tsx index 5b517dfea..c2bd6ab31 100644 --- a/apps/console/src/pages/ControlOverviewPage.tsx +++ b/apps/console/src/pages/ControlOverviewPage.tsx @@ -21,6 +21,13 @@ import { Upload, FileIcon, Loader2, + ChevronDown, + ChevronUp, + Download, + Eye, + File as FileGeneric, + FileText, + Image, } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { useToast } from "@/hooks/use-toast"; @@ -42,7 +49,10 @@ 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 } from "./__generated__/ControlOverviewPageUploadEvidenceMutation.graphql"; +import type { + ControlOverviewPageUploadEvidenceMutation as ControlOverviewPageUploadEvidenceMutationType, + EvidenceState, +} from "./__generated__/ControlOverviewPageUploadEvidenceMutation.graphql"; const controlOverviewPageQuery = graphql` query ControlOverviewPageQuery($controlId: ID!) { @@ -60,6 +70,18 @@ const controlOverviewPageQuery = graphql` name description state + evidences(first: 10) { + edges { + node { + id + fileUrl + mimeType + size + state + createdAt + } + } + } } } } @@ -130,6 +152,29 @@ 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, }: { @@ -155,7 +200,8 @@ function ControlOverviewPageContent({ uploadEvidenceMutation ); const control = data.control; - const tasks = control?.tasks?.edges.map((edge) => edge?.node) ?? []; + const tasks = + control?.tasks?.edges.map((edge) => edge?.node as TaskNode) ?? []; const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false); const [newTaskName, setNewTaskName] = useState(""); @@ -181,6 +227,11 @@ function ControlOverviewPageContent({ const [uploadingTaskId, setUploadingTaskId] = useState(null); const [isDraggingFile, setIsDraggingFile] = useState(false); + // State to track which task's evidence list is expanded + const [expandedEvidenceTaskId, setExpandedEvidenceTaskId] = useState< + string | null + >(null); + // Get the connection ID for Relay const connectionId = useMemo(() => { // The connection key is defined in the GraphQL query as "ControlOverviewPage_tasks" @@ -458,6 +509,49 @@ function ControlOverviewPageContent({ }); }; + // Function to format file size + const formatFileSize = (bytes: number) => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + }; + + // Function to format date + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + }; + + // Function to toggle evidence list expansion + const toggleEvidenceList = (taskId: string) => { + if (expandedEvidenceTaskId === taskId) { + setExpandedEvidenceTaskId(null); + } else { + setExpandedEvidenceTaskId(taskId); + } + }; + + // Function to get file icon based on mime type + const getFileIcon = (mimeType: string) => { + if (mimeType.startsWith("image/")) { + return ; + } else if (mimeType.includes("pdf")) { + return ; + } else if (mimeType.includes("word") || mimeType.includes("document")) { + return ; + } else if (mimeType.includes("excel") || mimeType.includes("spreadsheet")) { + return ; + } else { + return ; + } + }; + return ( <> @@ -578,122 +672,210 @@ function ControlOverviewPageContent({ {tasks.map((task) => (
task?.id && handleDragOver(e, task.id)} - onDragLeave={handleDragLeave} - onDrop={(e) => task?.id && handleDrop(e, task.id)} + className="rounded-md overflow-hidden border border-gray-200" > - {isDraggingFile && draggedOverTaskId !== task?.id && ( -
-
- -

Drop file here

-
-
- )} - - {draggedOverTaskId === task?.id && ( -
-
- -

- Drop file to upload evidence -

-
-
- )} - - {uploadingTaskId === task?.id && ( -
-
- -

Uploading evidence...

-
-
- )} -
- task?.id && - task?.state && - handleTaskClick(task.id, task.state) - } - > - {task?.state === "DONE" && ( - - )} -
-
- task?.id && - task?.state && - handleTaskClick(task.id, task.state) - } + onDragOver={(e) => task?.id && handleDragOver(e, task.id)} + onDragLeave={handleDragLeave} + onDrop={(e) => task?.id && handleDrop(e, task.id)} > -
-

- {task?.name} -

- {task?.description && ( -

- {task.description} -

+ {isDraggingFile && draggedOverTaskId !== task?.id && ( +
+
+ +

Drop file here

+
+
+ )} + + {draggedOverTaskId === task?.id && ( +
+
+ +

+ Drop file to upload evidence +

+
+
+ )} + + {uploadingTaskId === task?.id && ( +
+
+ +

Uploading evidence...

+
+
+ )} + +
+ task?.id && + task?.state && + handleTaskClick(task.id, task.state) + } + > + {task?.state === "DONE" && ( + )}
-
-
06.00 - 07.30
- - +
+ task?.id && + task?.state && + handleTaskClick(task.id, task.state) + } + > +
+

+ {task?.name} +

+ {task?.description && ( +

+ {task.description} +

+ )} +
+
+
06.00 - 07.30
+ + +
+ + {/* Evidence section */} + {task?.evidences?.edges && task.evidences.edges.length > 0 && ( + <> +
task.id && toggleEvidenceList(task.id)} + > +
+ + + {task.evidences.edges.length}{" "} + {task.evidences.edges.length === 1 + ? "Evidence" + : "Evidences"} + +
+ {expandedEvidenceTaskId === task.id ? ( + + ) : ( + + )} +
+ + {expandedEvidenceTaskId === task.id && ( +
+ {task.evidences.edges.map( + (edge: { node: EvidenceNode | null } | null) => { + const evidence = edge?.node; + if (!evidence) return null; + + return ( +
+
+ {getFileIcon(evidence.mimeType)} +
+
+ {evidence.name} +
+
+ + {formatFileSize(evidence.size)} + + • + + {formatDate(evidence.createdAt)} + +
+
+
+
+ {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 a991efb10..4723e1469 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<<297a88bb87d59c4a4ed2f8dce93667c6>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -10,6 +10,7 @@ import { ConcreteRequest } from 'relay-runtime'; export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; +export type EvidenceState = "EXPIRED" | "INVALID" | "VALID"; export type TaskState = "DONE" | "TODO"; export type ControlOverviewPageQuery$variables = { controlId: string; @@ -25,6 +26,18 @@ export type ControlOverviewPageQuery$data = { readonly edges: ReadonlyArray<{ readonly node: { readonly description: string; + readonly evidences: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly createdAt: string; + readonly fileUrl: string; + readonly id: string; + readonly mimeType: string; + readonly size: number; + readonly state: EvidenceState; + }; + }>; + }; readonly id: string; readonly name: string; readonly state: TaskState; @@ -116,6 +129,75 @@ v8 = [ (v3/*: any*/), (v4/*: any*/), (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)" + }, (v7/*: any*/) ], "storageKey": null @@ -259,7 +341,7 @@ return { ] }, "params": { - "cacheID": "a13eb28fcbe162deaff92653c19cc22a", + "cacheID": "ae3ac6c53719fa3d9c1953ff40c75005", "id": null, "metadata": { "connection": [ @@ -276,11 +358,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 __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: 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" } }; })(); -(node as any).hash = "dc8c7670eafe558966932c3fb4d45cd8"; +(node as any).hash = "ad8c4a4980229230ca295c4cb9a2c836"; export default node; diff --git a/apps/console/src/pages/__generated__/ControlOverviewPageUploadEvidenceMutation.graphql.ts b/apps/console/src/pages/__generated__/ControlOverviewPageUploadEvidenceMutation.graphql.ts index 7736b59e0..a7649a7b6 100644 --- a/apps/console/src/pages/__generated__/ControlOverviewPageUploadEvidenceMutation.graphql.ts +++ b/apps/console/src/pages/__generated__/ControlOverviewPageUploadEvidenceMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<09af19b5a7657e04606a42962db9db2c>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -23,7 +23,7 @@ export type ControlOverviewPageUploadEvidenceMutation$data = { readonly uploadEvidence: { readonly evidenceEdge: { readonly node: { - readonly createdAt: any; + readonly createdAt: string; readonly fileUrl: string; readonly id: string; readonly mimeType: string;