From 8cbe08d1368003582818d242a2c5f15f3e27bbb3 Mon Sep 17 00:00:00 2001 From: gearnode Date: Thu, 13 Mar 2025 10:08:31 +0100 Subject: [PATCH] Add assigned people to a task Signed-off-by: gearnode --- .../console/src/pages/ControlOverviewPage.tsx | 357 +++++++++- apps/console/src/pages/CreatePeoplePage.tsx | 4 + ...lOverviewPageAssignTaskMutation.graphql.ts | 146 ++++ ...lOverviewPageCreateTaskMutation.graphql.ts | 59 +- ...olOverviewPageOrganizationQuery.graphql.ts | 254 +++++++ .../ControlOverviewPageQuery.graphql.ts | 93 ++- ...verviewPageUnassignTaskMutation.graphql.ts | 145 ++++ ...viewPageUpdateTaskStateMutation.graphql.ts | 3 +- pkg/coredata/migrations/20250313T091900Z.sql | 1 + pkg/coredata/task.go | 66 +- pkg/probo/task_service.go | 63 ++ pkg/server/api/console/v1/schema.graphql | 22 + pkg/server/api/console/v1/schema/schema.go | 660 +++++++++++++++++- pkg/server/api/console/v1/types/types.go | 20 + pkg/server/api/console/v1/v1_resolver.go | 49 ++ 15 files changed, 1881 insertions(+), 61 deletions(-) create mode 100644 apps/console/src/pages/__generated__/ControlOverviewPageAssignTaskMutation.graphql.ts create mode 100644 apps/console/src/pages/__generated__/ControlOverviewPageOrganizationQuery.graphql.ts create mode 100644 apps/console/src/pages/__generated__/ControlOverviewPageUnassignTaskMutation.graphql.ts create mode 100644 pkg/coredata/migrations/20250313T091900Z.sql diff --git a/apps/console/src/pages/ControlOverviewPage.tsx b/apps/console/src/pages/ControlOverviewPage.tsx index 26a199893..374cb0d9d 100644 --- a/apps/console/src/pages/ControlOverviewPage.tsx +++ b/apps/console/src/pages/ControlOverviewPage.tsx @@ -31,6 +31,9 @@ import { FileText, Image, X, + UserPlus, + UserMinus, + User, } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { useToast } from "@/hooks/use-toast"; @@ -46,6 +49,11 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Helmet } from "react-helmet-async"; import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql"; @@ -54,6 +62,9 @@ import type { ControlOverviewPageCreateTaskMutation as ControlOverviewPageCreate import type { ControlOverviewPageDeleteTaskMutation as ControlOverviewPageDeleteTaskMutationType } from "./__generated__/ControlOverviewPageDeleteTaskMutation.graphql"; import type { ControlOverviewPageUploadEvidenceMutation as ControlOverviewPageUploadEvidenceMutationType } from "./__generated__/ControlOverviewPageUploadEvidenceMutation.graphql"; import type { ControlOverviewPageDeleteEvidenceMutation as ControlOverviewPageDeleteEvidenceMutationType } from "./__generated__/ControlOverviewPageDeleteEvidenceMutation.graphql"; +import type { ControlOverviewPageAssignTaskMutation as ControlOverviewPageAssignTaskMutationType } from "./__generated__/ControlOverviewPageAssignTaskMutation.graphql"; +import type { ControlOverviewPageUnassignTaskMutation as ControlOverviewPageUnassignTaskMutationType } from "./__generated__/ControlOverviewPageUnassignTaskMutation.graphql"; +import type { ControlOverviewPageOrganizationQuery$data } from "./__generated__/ControlOverviewPageOrganizationQuery.graphql"; import { Textarea } from "@/components/ui/textarea"; // Function to format ISO8601 duration to human-readable format @@ -113,6 +124,11 @@ const controlOverviewPageQuery = graphql` state timeEstimate version + assignedTo { + id + fullName + primaryEmailAddress + } evidences(first: 50) @connection(key: "ControlOverviewPage_evidences") { __id @@ -162,6 +178,11 @@ const createTaskMutation = graphql` description timeEstimate state + assignedTo { + id + fullName + primaryEmailAddress + } } } } @@ -222,6 +243,57 @@ const getEvidenceFileUrlQuery = graphql` } `; +const assignTaskMutation = graphql` + mutation ControlOverviewPageAssignTaskMutation($input: AssignTaskInput!) { + assignTask(input: $input) { + task { + id + version + assignedTo { + id + fullName + primaryEmailAddress + } + } + } + } +`; + +const unassignTaskMutation = graphql` + mutation ControlOverviewPageUnassignTaskMutation($input: UnassignTaskInput!) { + unassignTask(input: $input) { + task { + id + version + assignedTo { + id + fullName + primaryEmailAddress + } + } + } + } +`; + +const organizationQuery = graphql` + query ControlOverviewPageOrganizationQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + id + ... on Organization { + peoples(first: 100) @connection(key: "ControlOverviewPage_peoples") { + edges { + node { + id + fullName + primaryEmailAddress + } + } + } + } + } + } +`; + function ControlOverviewPageContent({ queryRef, }: { @@ -236,6 +308,32 @@ function ControlOverviewPageContent({ const navigate = useNavigate(); const environment = useRelayEnvironment(); + // Load organization data for people selector + const [organizationData, setOrganizationData] = + useState(null); + + useEffect(() => { + if (organizationId) { + fetchQuery(environment, organizationQuery, { + organizationId, + }) + .toPromise() + .then((response) => { + setOrganizationData( + response as ControlOverviewPageOrganizationQuery$data + ); + }) + .catch((error) => { + console.error("Error fetching organization data:", error); + toast({ + title: "Error", + description: "Failed to load people data", + variant: "destructive", + }); + }); + } + }, [organizationId, environment, toast]); + const formatImportance = (importance: string | undefined): string => { if (!importance) return ""; @@ -304,6 +402,12 @@ function ControlOverviewPageContent({ useMutation( deleteEvidenceMutation ); + const [assignTask] = + useMutation(assignTaskMutation); + const [unassignTask] = + useMutation( + unassignTaskMutation + ); const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false); const [newTaskName, setNewTaskName] = useState(""); @@ -354,6 +458,16 @@ function ControlOverviewPageContent({ taskId: string; } | null>(null); + // Add state for people selector + const [peoplePopoverOpen, setPeoplePopoverOpen] = useState<{ + [key: string]: boolean; + }>({}); + + // Add state for people search + const [peopleSearch, setPeopleSearch] = useState<{ + [key: string]: string; + }>({}); + const tasks = data.control.tasks?.edges.map((edge) => edge.node) || []; const getEvidenceConnectionId = useCallback( @@ -796,6 +910,57 @@ function ControlOverviewPageContent({ }); }; + // Function to handle assigning a person to a task + const handleAssignPerson = (taskId: string, personId: string) => { + assignTask({ + variables: { + input: { + taskId, + assignedToId: personId, + }, + }, + onCompleted: () => { + toast({ + title: "Task assigned", + description: "Task has been assigned successfully.", + }); + // Close the popover + setPeoplePopoverOpen((prev) => ({ ...prev, [taskId]: false })); + }, + onError: (error) => { + toast({ + title: "Error assigning task", + description: error.message, + variant: "destructive", + }); + }, + }); + }; + + // Function to handle unassigning a person from a task + const handleUnassignPerson = (taskId: string) => { + unassignTask({ + variables: { + input: { + taskId, + }, + }, + onCompleted: () => { + toast({ + title: "Task unassigned", + description: "Task has been unassigned successfully.", + }); + }, + onError: (error) => { + toast({ + title: "Error unassigning task", + description: error.message, + variant: "destructive", + }); + }, + }); + }; + return ( <> @@ -1015,25 +1180,21 @@ function ControlOverviewPageContent({ ? "border-gray-400 bg-gray-100" : "border-gray-300" } ${isDraggingFile ? "opacity-50" : ""}`} - onClick={() => - task?.id && - task?.state && - handleTaskClick(task.id, task.state, task.version) - } + onClick={(e) => { + e.stopPropagation(); + if (task?.id && task?.state) { + handleTaskClick(task.id, task.state, task.version); + } + }} > {task?.state === "DONE" && ( )}
- task?.id && - task?.state && - handleTaskClick(task.id, task.state, task.version) - } >

{formatDuration(task.timeEstimate)}

)} + {task?.assignedTo && ( +

+ + {task.assignedTo.fullName} +

+ )}

+ {/* People Selector */} + { + if (task?.id) { + setPeoplePopoverOpen((prev) => ({ + ...prev, + [task.id]: open, + })); + } + }} + > + + + + + {task?.assignedTo ? ( +
+
+ +
+

+ {task.assignedTo.fullName} +

+

+ {task.assignedTo.primaryEmailAddress} +

+
+
+ +
+ ) : ( +
+
+ { + if (task?.id) { + setPeopleSearch((prev) => ({ + ...prev, + [task.id]: e.target.value, + })); + } + }} + /> +
+
+ Click on a person to assign them to this task +
+
+ {!organizationData?.organization?.peoples?.edges?.some( + (edge) => { + if (!edge?.node) return false; + const searchTerm = ( + peopleSearch[task?.id || ""] || "" + ).toLowerCase(); + return ( + !searchTerm || + edge.node.fullName + .toLowerCase() + .includes(searchTerm) || + edge.node.primaryEmailAddress + .toLowerCase() + .includes(searchTerm) + ); + } + ) && ( +
+ No people found. +
+ )} + {organizationData?.organization?.peoples?.edges?.map( + (edge) => { + if (!edge?.node) return null; + + const searchTerm = ( + peopleSearch[task?.id || ""] || "" + ).toLowerCase(); + if ( + searchTerm && + !edge.node.fullName + .toLowerCase() + .includes(searchTerm) && + !edge.node.primaryEmailAddress + .toLowerCase() + .includes(searchTerm) + ) { + return null; + } + + return ( +
+ +
+ ); + } + )} +
+
+ )} +
+
+