From 9cb0c32957ad722dd75b8d87dc7f4f78d36a0dc0 Mon Sep 17 00:00:00 2001 From: gearnode Date: Mon, 24 Mar 2025 10:46:04 +0100 Subject: [PATCH] Add task view sidebar Signed-off-by: gearnode --- .../frameworks/controls/ControlView.tsx | 434 +++++++++++++++++- 1 file changed, 430 insertions(+), 4 deletions(-) diff --git a/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx b/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx index 7ac76668d..ef19fc2c6 100644 --- a/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx +++ b/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx @@ -6,7 +6,7 @@ import { DragEvent, useCallback, } from "react"; -import { useParams, useNavigate } from "react-router"; +import { useParams, useNavigate, useSearchParams } from "react-router"; import { graphql, PreloadedQuery, @@ -63,6 +63,13 @@ import ReactMarkdown from "react-markdown"; import { Textarea } from "@/components/ui/textarea"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Label } from "@/components/ui/label"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetClose, +} from "@/components/ui/sheet"; import type { ControlViewQuery as ControlViewQueryType } from "./__generated__/ControlViewQuery.graphql"; import type { ControlViewUpdateTaskStateMutation as ControlViewUpdateTaskStateMutationType } from "./__generated__/ControlViewUpdateTaskStateMutation.graphql"; @@ -335,6 +342,10 @@ function ControlViewContent({ const navigate = useNavigate(); const environment = useRelayEnvironment(); + // Add URLSearchParams handling for task persistence + const [searchParams, setSearchParams] = useSearchParams(); + const taskIdFromUrl = searchParams.get("taskId"); + // Load organization data for people selector const [organizationData, setOrganizationData] = useState(null); @@ -498,8 +509,32 @@ function ControlViewContent({ const [linkEvidenceDescription, setLinkEvidenceDescription] = useState(""); const [activeTab, setActiveTab] = useState<"file" | "link">("file"); + // Add state for selected task panel + const [selectedTask, setSelectedTask] = useState<(typeof tasks)[0] | null>( + null + ); + + // Track if task panel is open + const [isTaskPanelOpen, setIsTaskPanelOpen] = useState(false); + const tasks = data.control.tasks?.edges.map((edge) => edge.node) || []; + // Add useEffect to handle URL parameters for task selection + useEffect(() => { + // If there's a taskId in the URL, find that task and select it + if (taskIdFromUrl && tasks.length > 0) { + const taskFromUrl = tasks.find((task) => task.id === taskIdFromUrl); + if (taskFromUrl) { + setSelectedTask(taskFromUrl); + setIsTaskPanelOpen(true); + } else { + // If task ID is invalid, remove it from URL + searchParams.delete("taskId"); + setSearchParams(searchParams); + } + } + }, [taskIdFromUrl, tasks, searchParams, setSearchParams]); + const getEvidenceConnectionId = useCallback( (taskId: string) => { const task = tasks.find((t) => t.id === taskId); @@ -576,7 +611,17 @@ function ControlViewContent({ }; }, []); - const handleTaskClick = ( + const handleTaskClick = (task: (typeof tasks)[0]) => { + if (!task) return; + setSelectedTask(task); + setIsTaskPanelOpen(true); + + // Add the task ID to URL parameters + searchParams.set("taskId", task.id); + setSearchParams(searchParams); + }; + + const handleToggleTaskState = ( taskId: string, currentState: string, version: number @@ -598,6 +643,15 @@ function ControlViewContent({ newState === "DONE" ? "completed" : "reopened" }.`, }); + + // Update the selected task state if it's the current task + if (selectedTask && selectedTask.id === taskId) { + setSelectedTask({ + ...selectedTask, + state: newState, + version: version + 1, + }); + } }, onError: (error) => { toast({ @@ -1115,6 +1169,15 @@ function ControlViewContent({ }); }; + // Update SheetContent to handle closing + const handleCloseTaskPanel = () => { + setIsTaskPanelOpen(false); + + // Remove the task ID from URL parameters when closing + searchParams.delete("taskId"); + setSearchParams(searchParams); + }; + return ( task && handleTaskClick(task)} onDragOver={(e) => task?.id && handleDragOver(e, task.id)} onDragLeave={handleDragLeave} onDrop={(e) => task?.id && handleDrop(e, task.id)} @@ -1345,9 +1413,9 @@ function ControlViewContent({ : "border-gray-300" } ${isDraggingFile ? "opacity-50" : ""}`} onClick={(e) => { - e.stopPropagation(); + e.stopPropagation(); // Prevent task selection when checkbox is clicked if (task?.id && task?.state) { - handleTaskClick(task.id, task.state, task.version); + handleToggleTaskState(task.id, task.state, task.version); } }} > @@ -1723,6 +1791,364 @@ function ControlViewContent({ + {/* Right task panel */} + + + {selectedTask && ( +
+ +
+ + Task Details + + + + +
+
+ +
+ {/* Task name and state */} +
+

{selectedTask.name}

+
+
+ {selectedTask.state === "DONE" + ? "Completed" + : "In Progress"} +
+ {selectedTask.timeEstimate && ( +
+ ⏱️ + {formatDuration(selectedTask.timeEstimate)} +
+ )} +
+
+ + {/* Task description */} + {selectedTask.description && ( +
+

+ Description +

+
+ {selectedTask.description} +
+
+ )} + + {/* Assigned person */} +
+

+ Assignment +

+ {selectedTask.assignedTo ? ( +
+
+
+ +
+
+

+ {selectedTask.assignedTo.fullName} +

+

+ {selectedTask.assignedTo.primaryEmailAddress} +

+
+
+ +
+ ) : ( +
+

+ No one is assigned to this task +

+ + + + + +
+ { + setPeopleSearch((prev) => ({ + ...prev, + [selectedTask.id]: e.target.value, + })); + }} + /> +
+
+ {organizationData?.organization?.peoples?.edges?.map( + (edge) => { + if (!edge?.node) return null; + + const searchTerm = ( + peopleSearch[selectedTask.id || ""] || "" + ).toLowerCase(); + if ( + searchTerm && + !edge.node.fullName + .toLowerCase() + .includes(searchTerm) && + !edge.node.primaryEmailAddress + .toLowerCase() + .includes(searchTerm) + ) { + return null; + } + + return ( +
+ +
+ ); + } + )} +
+
+
+
+ )} +
+ + {/* Evidence section */} +
+
+

+ Evidence +

+ +
+ +
+ {selectedTask.evidences?.edges && + selectedTask.evidences.edges.length > 0 ? ( + selectedTask.evidences.edges.map((edge) => { + if (!edge) return null; + const evidence = edge.node; + if (!evidence) return null; + + return ( +
+
+
+ {getFileIcon(evidence.mimeType, evidence.type)} +
+
+
+ {evidence.filename} +
+
+ {evidence.type === "FILE" ? ( + <> + + {formatFileSize(evidence.size)} + + • + + ) : evidence.url ? ( + <> + + {evidence.url} + + • + + ) : null} + {formatDate(evidence.createdAt)} +
+
+
+
+ {evidence.type === "FILE" ? ( + <> + {evidence.mimeType.startsWith("image/") ? ( + + ) : ( + + )} + + ) : evidence.url ? ( + + ) : null} + +
+
+ ); + }) + ) : ( +
+ +

+ No evidence attached yet +

+

+ Upload files or add links to provide evidence +

+
+ )} +
+
+
+ + {/* Task actions */} +
+
+ {selectedTask.state !== "DONE" ? ( + + ) : ( + + )} + +
+
+
+ )} +
+
+ {/* Evidence Add Dialog */}