From fc008c27f41c232cbbe9f59a1144a82ee818fb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Reu=C3=9Fner?= Date: Mon, 2 Jun 2025 10:47:26 +0200 Subject: [PATCH] Add deadline on task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andreas Reußner --- .../organizations/measures/MeasureView.tsx | 120 ++++++++++++++++++ .../MeasureViewCreateTaskMutation.graphql.ts | 17 ++- .../__generated__/MeasureViewQuery.graphql.ts | 34 +++-- ...sureViewUpdateTaskStateMutation.graphql.ts | 17 ++- .../organizations/tasks/ListTaskView.tsx | 38 +++++- .../ListTaskViewCreateTaskMutation.graphql.ts | 17 ++- .../ListTaskViewQuery.graphql.ts | 16 ++- ...TaskViewUpdateTaskStateMutation.graphql.ts | 3 +- pkg/coredata/migrations/20250603T083618Z.sql | 1 + pkg/coredata/task.go | 15 ++- pkg/probo/task_service.go | 7 + pkg/server/api/console/v1/schema.graphql | 3 + pkg/server/api/console/v1/schema/schema.go | 82 +++++++++++- pkg/server/api/console/v1/types/task.go | 1 + pkg/server/api/console/v1/types/types.go | 3 + pkg/server/api/console/v1/v1_resolver.go | 2 + 16 files changed, 343 insertions(+), 33 deletions(-) create mode 100644 pkg/coredata/migrations/20250603T083618Z.sql diff --git a/apps/console/src/pages/organizations/measures/MeasureView.tsx b/apps/console/src/pages/organizations/measures/MeasureView.tsx index 5d7c865cf..046e6c3e6 100644 --- a/apps/console/src/pages/organizations/measures/MeasureView.tsx +++ b/apps/console/src/pages/organizations/measures/MeasureView.tsx @@ -109,6 +109,7 @@ import { MeasureViewRisksQuery } from "./__generated__/MeasureViewRisksQuery.gra import { MeasureViewDeleteMeasureMutation } from "./__generated__/MeasureViewDeleteMeasureMutation.graphql"; import remarkGfm from "remark-gfm"; import rehypeRaw from "rehype-raw"; +import { format, formatISO, parseISO } from "date-fns"; // Function to format ISO8601 duration to human-readable format const formatDuration = (isoDuration: string): string => { @@ -181,6 +182,7 @@ const measureViewQuery = graphql` description state timeEstimate + deadline assignedTo { id fullName @@ -218,6 +220,7 @@ const updateTaskStateMutation = graphql` id state timeEstimate + deadline } } } @@ -235,6 +238,7 @@ const createTaskMutation = graphql` name description timeEstimate + deadline state assignedTo { id @@ -743,6 +747,7 @@ function MeasureViewContent({ const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false); const [newTaskName, setNewTaskName] = useState(""); const [newTaskDescription, setNewTaskDescription] = useState(""); + const [newDeadline, setNewDeadline] = useState(""); const [timeEstimateDays, setTimeEstimateDays] = useState(""); const [timeEstimateHours, setTimeEstimateHours] = useState(""); const [timeEstimateMinutes, setTimeEstimateMinutes] = useState(""); @@ -991,6 +996,7 @@ function MeasureViewContent({ name: newTaskName, description: newTaskDescription, timeEstimate: isoTimeEstimate === "" ? null : isoTimeEstimate, + deadline: newDeadline === "" ? null : formatISO(newDeadline), }, }, onCompleted: () => { @@ -1004,6 +1010,7 @@ function MeasureViewContent({ setTimeEstimateHours(""); setTimeEstimateMinutes(""); setIsCreateTaskOpen(false); + setNewDeadline(""); }, onError: (error) => { toast({ @@ -1718,9 +1725,11 @@ function MeasureViewContent({ // Add state variables for tracking edit mode and duration components const [isEditingDuration, setIsEditingDuration] = useState(false); + const [isEditingDeadline, setIsEditingDeadline] = useState(false); const [editTimeEstimateDays, setEditTimeEstimateDays] = useState(""); const [editTimeEstimateHours, setEditTimeEstimateHours] = useState(""); const [editTimeEstimateMinutes, setEditTimeEstimateMinutes] = useState(""); + const [editDeadline, setEditDeadline] = useState(""); // Function to parse ISO duration string into components for editing const parseISODuration = useCallback( @@ -1750,6 +1759,40 @@ function MeasureViewContent({ [] ); + // Function to handle saving the updated deadline + const handleSaveDeadline = useCallback( + (taskId: string) => { + const newDeadline = editDeadline === "" ? null : formatISO(editDeadline); + updateTask({ + variables: { + input: { + taskId, + deadline: newDeadline, + }, + }, + onCompleted: () => { + setIsEditingDeadline(false); + + // Update the selected task state if it's the current task + if (selectedTask && selectedTask.id === taskId) { + setSelectedTask({ + ...selectedTask, + deadline: newDeadline, + }); + } + }, + onError: (error) => { + toast({ + title: "Error updating task", + description: error.message, + variant: "destructive", + }); + }, + }); + }, + [editDeadline, updateTask, toast, selectedTask, setSelectedTask] + ); + // Function to handle saving the updated duration const handleSaveDuration = useCallback( (taskId: string) => { @@ -2605,6 +2648,11 @@ function MeasureViewContent({ {formatDuration(task.timeEstimate)} )} + {task.deadline && ( +
+ {formatDate(task.deadline)} +
+ )} @@ -3063,6 +3111,69 @@ function MeasureViewContent({ )} + {/* Deadline */} +
+

+ Deadline +

+ {isEditingDeadline ? ( +
+
+
+ + setEditDeadline(e.target.value) + } + /> +
+
+
+ + +
+
+ ) : ( +
+
+ {selectedTask.deadline ? ( + + {formatDate(selectedTask.deadline)} + + ) : ( + + No deadline + + )} +
+ +
+ )} +
+ {/* Evidence section */}
@@ -3846,6 +3957,15 @@ function MeasureViewContent({
+
+ + setNewDeadline(e.target.value)} + /> +
diff --git a/apps/console/src/pages/organizations/measures/__generated__/MeasureViewCreateTaskMutation.graphql.ts b/apps/console/src/pages/organizations/measures/__generated__/MeasureViewCreateTaskMutation.graphql.ts index 2dd0e3a6b..3a9eb03b9 100644 --- a/apps/console/src/pages/organizations/measures/__generated__/MeasureViewCreateTaskMutation.graphql.ts +++ b/apps/console/src/pages/organizations/measures/__generated__/MeasureViewCreateTaskMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime'; export type TaskState = "DONE" | "TODO"; export type CreateTaskInput = { assignedToId?: string | null | undefined; + deadline?: string | null | undefined; description: string; measureId?: string | null | undefined; name: string; @@ -31,6 +32,7 @@ export type MeasureViewCreateTaskMutation$data = { readonly id: string; readonly primaryEmailAddress: string; } | null | undefined; + readonly deadline: string | null | undefined; readonly description: string; readonly id: string; readonly name: string; @@ -108,6 +110,13 @@ v4 = { "name": "timeEstimate", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deadline", + "storageKey": null + }, { "alias": null, "args": null, @@ -213,16 +222,16 @@ return { ] }, "params": { - "cacheID": "53b934e0e7a3d4abf8789ad8672687df", + "cacheID": "8f9aeb0df92f9535d728ef27a7dee963", "id": null, "metadata": {}, "name": "MeasureViewCreateTaskMutation", "operationKind": "mutation", - "text": "mutation MeasureViewCreateTaskMutation(\n $input: CreateTaskInput!\n) {\n createTask(input: $input) {\n taskEdge {\n node {\n id\n name\n description\n timeEstimate\n state\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n }\n}\n" + "text": "mutation MeasureViewCreateTaskMutation(\n $input: CreateTaskInput!\n) {\n createTask(input: $input) {\n taskEdge {\n node {\n id\n name\n description\n timeEstimate\n deadline\n state\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "0df98f5aee4233dc4ffedcff8256891a"; +(node as any).hash = "f323b5caa0912478e715f0c409898eff"; export default node; diff --git a/apps/console/src/pages/organizations/measures/__generated__/MeasureViewQuery.graphql.ts b/apps/console/src/pages/organizations/measures/__generated__/MeasureViewQuery.graphql.ts index 4af0ee973..ddd51b931 100644 --- a/apps/console/src/pages/organizations/measures/__generated__/MeasureViewQuery.graphql.ts +++ b/apps/console/src/pages/organizations/measures/__generated__/MeasureViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<03d8e2eca1cc7ffaf797c66ef7fa11c1>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -48,6 +48,7 @@ export type MeasureViewQuery$data = { readonly id: string; readonly primaryEmailAddress: string; } | null | undefined; + readonly deadline: string | null | undefined; readonly description: string; readonly evidences: { readonly __id: string; @@ -261,6 +262,13 @@ v12 = { "storageKey": null }, v13 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deadline", + "storageKey": null +}, +v14 = { "alias": null, "args": null, "concreteType": "People", @@ -286,14 +294,14 @@ v13 = { ], "storageKey": null }, -v14 = [ +v15 = [ { "kind": "Literal", "name": "first", "value": 100 } ], -v15 = [ +v16 = [ { "kind": "Literal", "name": "first", @@ -363,6 +371,7 @@ return { (v5/*: any*/), (v12/*: any*/), (v13/*: any*/), + (v14/*: any*/), { "alias": "evidences", "args": null, @@ -422,7 +431,7 @@ return { (v6/*: any*/), { "alias": null, - "args": (v14/*: any*/), + "args": (v15/*: any*/), "concreteType": "EvidenceConnection", "kind": "LinkedField", "name": "evidences", @@ -432,7 +441,7 @@ return { }, { "alias": null, - "args": (v14/*: any*/), + "args": (v15/*: any*/), "filters": null, "handle": "connection", "key": "MeasureView_evidences", @@ -441,7 +450,7 @@ return { }, { "alias": null, - "args": (v14/*: any*/), + "args": (v15/*: any*/), "concreteType": "TaskConnection", "kind": "LinkedField", "name": "tasks", @@ -469,9 +478,10 @@ return { (v5/*: any*/), (v12/*: any*/), (v13/*: any*/), + (v14/*: any*/), { "alias": null, - "args": (v15/*: any*/), + "args": (v16/*: any*/), "concreteType": "EvidenceConnection", "kind": "LinkedField", "name": "evidences", @@ -481,7 +491,7 @@ return { }, { "alias": null, - "args": (v15/*: any*/), + "args": (v16/*: any*/), "filters": null, "handle": "connection", "key": "MeasureView_task_evidences", @@ -503,7 +513,7 @@ return { }, { "alias": null, - "args": (v14/*: any*/), + "args": (v15/*: any*/), "filters": null, "handle": "connection", "key": "MeasureView_tasks", @@ -520,7 +530,7 @@ return { ] }, "params": { - "cacheID": "d3e4d9723d12bce8158b83eb76d05a90", + "cacheID": "2693fa3e0141f58235566f539194fcd6", "id": null, "metadata": { "connection": [ @@ -552,11 +562,11 @@ return { }, "name": "MeasureViewQuery", "operationKind": "query", - "text": "query MeasureViewQuery(\n $measureId: ID!\n) {\n measure: node(id: $measureId) {\n __typename\n id\n ... on Measure {\n name\n description\n state\n category\n evidences(first: 100) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n description\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n description\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" + "text": "query MeasureViewQuery(\n $measureId: ID!\n) {\n measure: node(id: $measureId) {\n __typename\n id\n ... on Measure {\n name\n description\n state\n category\n evidences(first: 100) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n description\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n deadline\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n description\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 = "288412cccb1f4c14b1852504e5536af1"; +(node as any).hash = "4e211c36d94ebd914b6de254dfbdf946"; export default node; diff --git a/apps/console/src/pages/organizations/measures/__generated__/MeasureViewUpdateTaskStateMutation.graphql.ts b/apps/console/src/pages/organizations/measures/__generated__/MeasureViewUpdateTaskStateMutation.graphql.ts index e5b0f3b24..c1c40ab90 100644 --- a/apps/console/src/pages/organizations/measures/__generated__/MeasureViewUpdateTaskStateMutation.graphql.ts +++ b/apps/console/src/pages/organizations/measures/__generated__/MeasureViewUpdateTaskStateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<1595fa9771d6baa14534b181f4a1384a>> + * @generated SignedSource<<28c68c446adeac27a47de2b754d48d5d>> * @lightSyntaxTransform * @nogrep */ @@ -11,6 +11,7 @@ import { ConcreteRequest } from 'relay-runtime'; export type TaskState = "DONE" | "TODO"; export type UpdateTaskInput = { + deadline?: string | null | undefined; description?: string | null | undefined; name?: string | null | undefined; state?: TaskState | null | undefined; @@ -23,6 +24,7 @@ export type MeasureViewUpdateTaskStateMutation$variables = { export type MeasureViewUpdateTaskStateMutation$data = { readonly updateTask: { readonly task: { + readonly deadline: string | null | undefined; readonly id: string; readonly state: TaskState; readonly timeEstimate: any | null | undefined; @@ -85,6 +87,13 @@ v1 = [ "kind": "ScalarField", "name": "timeEstimate", "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deadline", + "storageKey": null } ], "storageKey": null @@ -111,16 +120,16 @@ return { "selections": (v1/*: any*/) }, "params": { - "cacheID": "f545a0b12edb8f231a1a7a7c38b6e905", + "cacheID": "a0001fad3af2ba3fc16a1f0af4f19370", "id": null, "metadata": {}, "name": "MeasureViewUpdateTaskStateMutation", "operationKind": "mutation", - "text": "mutation MeasureViewUpdateTaskStateMutation(\n $input: UpdateTaskInput!\n) {\n updateTask(input: $input) {\n task {\n id\n state\n timeEstimate\n }\n }\n}\n" + "text": "mutation MeasureViewUpdateTaskStateMutation(\n $input: UpdateTaskInput!\n) {\n updateTask(input: $input) {\n task {\n id\n state\n timeEstimate\n deadline\n }\n }\n}\n" } }; })(); -(node as any).hash = "e1d67c030523e56d6bcd595c27fcb3dc"; +(node as any).hash = "b3b0d47e0632b106b6313edbd2de38ce"; export default node; diff --git a/apps/console/src/pages/organizations/tasks/ListTaskView.tsx b/apps/console/src/pages/organizations/tasks/ListTaskView.tsx index 890c690e1..5ad300d2a 100644 --- a/apps/console/src/pages/organizations/tasks/ListTaskView.tsx +++ b/apps/console/src/pages/organizations/tasks/ListTaskView.tsx @@ -1,4 +1,4 @@ -import { Suspense, useEffect, useState } from "react"; +import {ChangeEvent, Suspense, useEffect, useState} from "react"; import { useQueryLoader, graphql, @@ -47,6 +47,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ListTaskViewOrganizationQuery } from "./__generated__/ListTaskViewOrganizationQuery.graphql"; +import {formatDate, parseISO} from "date-fns"; // Function to format ISO8601 duration to human-readable format const formatDuration = (isoDuration: string | null | undefined): string => { @@ -121,6 +122,7 @@ const createTaskMutation = graphql` description state timeEstimate + deadline measure { id name @@ -164,6 +166,7 @@ const listTaskViewQuery = graphql` description state timeEstimate + deadline measure { id name @@ -213,6 +216,7 @@ interface Task { description?: string; state: string; timeEstimate?: number | null; + deadline?: string | null; assignedTo?: { id: string; fullName: string; @@ -273,6 +277,11 @@ function TaskCard({ task, onClick, onToggleState }: { {formatDuration(task.timeEstimate.toString())} )} + {task.deadline && ( + + {formatDate(task.deadline, "dd MMM yyyy")} + + )} {task.parent && (
@@ -346,6 +355,8 @@ function ListTaskContent({ const [selectedMeasure, setSelectedMeasure] = useState<{id: string, name: string} | null>(null); const [timeEstimate, setTimeEstimate] = useState(null); const [timeUnit, setTimeUnit] = useState<"minutes" | "hours" | "days" | "weeks">("hours"); + const [deadlineString, setDeadlineString] = useState(''); + const [deadline, setDeadline] = useState(null); // Create task mutation const [createTask, isCreatingTask] = useMutation(createTaskMutation); @@ -382,6 +393,19 @@ function ListTaskContent({ } }, [environment, organization?.id]); + // Parse deadline string into Date-object + const handleDeadlineChange = (event: ChangeEvent) => { + const value = event.target.value; + setDeadlineString(value); + + if (value) { + const date = parseISO(value); + setDeadline(date); + } else { + setDeadline(null) + } + }; + // Filter measures based on search query const filteredMeasures = measures.filter(measure => measure.name.toLowerCase().includes(measureSearchQuery.toLowerCase()) || @@ -439,6 +463,7 @@ function ListTaskContent({ measureId: selectedMeasure?.id || null, assignedToId: assignee?.id, timeEstimate: formattedTimeEstimate, + deadline: deadline }, connections: [data.node?.tasks?.__id || ""], }, @@ -462,6 +487,8 @@ function ListTaskContent({ setTimeEstimate(null); setTimeUnit("hours"); setMeasureSearchQuery(""); + setDeadlineString(''); + setDeadline(null); }; // Get initials from name @@ -825,6 +852,15 @@ function ListTaskContent({ )}
+ + {/* Deadline */} +
+ +
+ +
+
diff --git a/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewCreateTaskMutation.graphql.ts b/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewCreateTaskMutation.graphql.ts index 50b39c423..768897952 100644 --- a/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewCreateTaskMutation.graphql.ts +++ b/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewCreateTaskMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<84a35602a155b0f1fa8504cf32c10932>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime'; export type TaskState = "DONE" | "TODO"; export type CreateTaskInput = { assignedToId?: string | null | undefined; + deadline?: string | null | undefined; description: string; measureId?: string | null | undefined; name: string; @@ -30,6 +31,7 @@ export type ListTaskViewCreateTaskMutation$data = { readonly fullName: string; readonly id: string; } | null | undefined; + readonly deadline: string | null | undefined; readonly description: string; readonly id: string; readonly measure: { @@ -119,6 +121,13 @@ v5 = { "name": "timeEstimate", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deadline", + "storageKey": null + }, { "alias": null, "args": null, @@ -223,16 +232,16 @@ return { ] }, "params": { - "cacheID": "7b9680f637411402ff51239a43b44a14", + "cacheID": "6fe245ddb913332138beda8377ebd88c", "id": null, "metadata": {}, "name": "ListTaskViewCreateTaskMutation", "operationKind": "mutation", - "text": "mutation ListTaskViewCreateTaskMutation(\n $input: CreateTaskInput!\n) {\n createTask(input: $input) {\n taskEdge {\n node {\n id\n name\n description\n state\n timeEstimate\n measure {\n id\n name\n }\n assignedTo {\n id\n fullName\n }\n }\n }\n }\n}\n" + "text": "mutation ListTaskViewCreateTaskMutation(\n $input: CreateTaskInput!\n) {\n createTask(input: $input) {\n taskEdge {\n node {\n id\n name\n description\n state\n timeEstimate\n deadline\n measure {\n id\n name\n }\n assignedTo {\n id\n fullName\n }\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "43ccaf49d31ec84dcf3b6b3f2e3a851c"; +(node as any).hash = "934ee4a5b5bfd4c9e9c4acab49d11304"; export default node; diff --git a/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewQuery.graphql.ts b/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewQuery.graphql.ts index 50bec47a7..b11720095 100644 --- a/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewQuery.graphql.ts +++ b/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<7732676a00f7ed0f6817555b68540aca>> + * @generated SignedSource<<14a914bdd269eb18124d002d2c61a5b9>> * @lightSyntaxTransform * @nogrep */ @@ -25,6 +25,7 @@ export type ListTaskViewQuery$data = { readonly fullName: string; readonly id: string; } | null | undefined; + readonly deadline: string | null | undefined; readonly description: string; readonly id: string; readonly measure: { @@ -120,6 +121,13 @@ v5 = [ "name": "timeEstimate", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deadline", + "storageKey": null + }, { "alias": null, "args": null, @@ -301,7 +309,7 @@ return { ] }, "params": { - "cacheID": "52b577b08c305249855293949d6e2339", + "cacheID": "7519a481d7930f14c53c8c1a72d66f58", "id": null, "metadata": { "connection": [ @@ -318,11 +326,11 @@ return { }, "name": "ListTaskViewQuery", "operationKind": "query", - "text": "query ListTaskViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n name\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n measure {\n id\n name\n }\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" + "text": "query ListTaskViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n name\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n deadline\n measure {\n id\n name\n }\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "5ca091444c547744068f2b2a83cb31df"; +(node as any).hash = "52b99d58a5b95dadd4e66bfad588d924"; export default node; diff --git a/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewUpdateTaskStateMutation.graphql.ts b/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewUpdateTaskStateMutation.graphql.ts index 653c11f2a..75ee77ccb 100644 --- a/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewUpdateTaskStateMutation.graphql.ts +++ b/apps/console/src/pages/organizations/tasks/__generated__/ListTaskViewUpdateTaskStateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<7098e326d01cc6831f293726096a7275>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -11,6 +11,7 @@ import { ConcreteRequest } from 'relay-runtime'; export type TaskState = "DONE" | "TODO"; export type UpdateTaskInput = { + deadline?: string | null | undefined; description?: string | null | undefined; name?: string | null | undefined; state?: TaskState | null | undefined; diff --git a/pkg/coredata/migrations/20250603T083618Z.sql b/pkg/coredata/migrations/20250603T083618Z.sql new file mode 100644 index 000000000..0c6f6668f --- /dev/null +++ b/pkg/coredata/migrations/20250603T083618Z.sql @@ -0,0 +1 @@ +ALTER TABLE tasks ADD COLUMN deadline TIMESTAMP WITH TIME ZONE; diff --git a/pkg/coredata/task.go b/pkg/coredata/task.go index b94a6b65e..df6e1f919 100644 --- a/pkg/coredata/task.go +++ b/pkg/coredata/task.go @@ -38,6 +38,7 @@ type ( ReferenceID string `db:"reference_id"` TimeEstimate *time.Duration `db:"time_estimate"` AssignedToID *gid.GID `db:"assigned_to"` + Deadline *time.Time `db:"deadline"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } @@ -71,6 +72,7 @@ SELECT reference_id, time_estimate, assigned_to, + deadline, created_at, updated_at FROM @@ -119,6 +121,7 @@ INSERT INTO state, time_estimate, assigned_to, + deadline, created_at, updated_at ) @@ -133,6 +136,7 @@ VALUES ( @state, @time_estimate, @assigned_to, + @deadline, @created_at, @updated_at ); @@ -149,6 +153,7 @@ VALUES ( "state": c.State, "time_estimate": c.TimeEstimate, "assigned_to": c.AssignedToID, + "deadline": c.Deadline, "created_at": c.CreatedAt, "updated_at": c.UpdatedAt, } @@ -174,6 +179,7 @@ INSERT INTO state, time_estimate, assigned_to, + deadline, created_at, updated_at ) @@ -188,6 +194,7 @@ VALUES ( @state, @time_estimate, @assigned_to, + @deadline, @created_at, @updated_at ) @@ -205,6 +212,7 @@ RETURNING state, time_estimate, assigned_to, + deadline, created_at, updated_at ` @@ -220,6 +228,7 @@ RETURNING "state": c.State, "time_estimate": c.TimeEstimate, "assigned_to": c.AssignedToID, + "deadline": c.Deadline, "created_at": c.CreatedAt, "updated_at": c.UpdatedAt, } @@ -256,6 +265,7 @@ func (c *Tasks) LoadByOrganizationID( reference_id, time_estimate, assigned_to, + deadline, created_at, updated_at FROM @@ -304,6 +314,7 @@ SELECT reference_id, time_estimate, assigned_to, + deadline, created_at, updated_at FROM @@ -347,7 +358,8 @@ SET state = @state, time_estimate = @time_estimate, updated_at = @updated_at, - assigned_to = @assigned_to + assigned_to = @assigned_to, + deadline = @deadline WHERE %s AND id = @task_id ` @@ -361,6 +373,7 @@ WHERE %s "time_estimate": c.TimeEstimate, "updated_at": c.UpdatedAt, "assigned_to": c.AssignedToID, + "deadline": c.Deadline, } maps.Copy(args, scope.SQLArguments()) diff --git a/pkg/probo/task_service.go b/pkg/probo/task_service.go index 4655164dc..f8f9e77a4 100644 --- a/pkg/probo/task_service.go +++ b/pkg/probo/task_service.go @@ -38,6 +38,7 @@ type ( Description string TimeEstimate *time.Duration AssignedToID *gid.GID + Deadline *time.Time } UpdateTaskRequest struct { @@ -46,6 +47,7 @@ type ( Description *string State *coredata.TaskState TimeEstimate *time.Duration + Deadline *time.Time } ) @@ -69,6 +71,7 @@ func (s TaskService) Create( Description: req.Description, TimeEstimate: req.TimeEstimate, AssignedToID: req.AssignedToID, + Deadline: req.Deadline, State: coredata.TaskStateTodo, ReferenceID: "custom-task-" + referenceID.String(), CreatedAt: now, @@ -203,6 +206,10 @@ func (s TaskService) Update( task.TimeEstimate = req.TimeEstimate } + if req.Deadline != nil { + task.Deadline = req.Deadline + } + task.UpdatedAt = time.Now() if err := task.Update(ctx, conn, s.svc.scope); err != nil { diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index b22970f46..78a67f81c 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -758,6 +758,7 @@ type Task implements Node { description: String! state: TaskState! timeEstimate: Duration + deadline: Datetime assignedTo: People @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true) @@ -1329,6 +1330,7 @@ input CreateTaskInput { description: String! timeEstimate: Duration assignedToId: ID + deadline: Datetime } input UpdateTaskInput { @@ -1337,6 +1339,7 @@ input UpdateTaskInput { description: String state: TaskState timeEstimate: Duration + deadline: Datetime } input DeleteTaskInput { diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index cefaa60e5..e113a06db 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -680,6 +680,7 @@ type ComplexityRoot struct { Task struct { AssignedTo func(childComplexity int) int CreatedAt func(childComplexity int) int + Deadline func(childComplexity int) int Description func(childComplexity int) int Evidences func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.EvidenceOrderBy) int ID func(childComplexity int) int @@ -3834,6 +3835,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Task.CreatedAt(childComplexity), true + case "Task.deadline": + if e.complexity.Task.Deadline == nil { + break + } + + return e.complexity.Task.Deadline(childComplexity), true + case "Task.description": if e.complexity.Task.Description == nil { break @@ -5481,6 +5489,7 @@ type Task implements Node { description: String! state: TaskState! timeEstimate: Duration + deadline: Datetime assignedTo: People @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true) @@ -6052,6 +6061,7 @@ input CreateTaskInput { description: String! timeEstimate: Duration assignedToId: ID + deadline: Datetime } input UpdateTaskInput { @@ -6060,6 +6070,7 @@ input UpdateTaskInput { description: String state: TaskState timeEstimate: Duration + deadline: Datetime } input DeleteTaskInput { @@ -12379,6 +12390,8 @@ func (ec *executionContext) fieldContext_AssignTaskPayload_task(_ context.Contex return ec.fieldContext_Task_state(ctx, field) case "timeEstimate": return ec.fieldContext_Task_timeEstimate(ctx, field) + case "deadline": + return ec.fieldContext_Task_deadline(ctx, field) case "assignedTo": return ec.fieldContext_Task_assignedTo(ctx, field) case "organization": @@ -18910,6 +18923,8 @@ func (ec *executionContext) fieldContext_Evidence_task(_ context.Context, field return ec.fieldContext_Task_state(ctx, field) case "timeEstimate": return ec.fieldContext_Task_timeEstimate(ctx, field) + case "deadline": + return ec.fieldContext_Task_deadline(ctx, field) case "assignedTo": return ec.fieldContext_Task_assignedTo(ctx, field) case "organization": @@ -28879,6 +28894,47 @@ func (ec *executionContext) fieldContext_Task_timeEstimate(_ context.Context, fi return fc, nil } +func (ec *executionContext) _Task_deadline(ctx context.Context, field graphql.CollectedField, obj *types.Task) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Task_deadline(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Deadline, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*time.Time) + fc.Result = res + return ec.marshalODatetime2ᚖtimeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Task_deadline(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Task", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Task_assignedTo(ctx context.Context, field graphql.CollectedField, obj *types.Task) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Task_assignedTo(ctx, field) if err != nil { @@ -29431,6 +29487,8 @@ func (ec *executionContext) fieldContext_TaskEdge_node(_ context.Context, field return ec.fieldContext_Task_state(ctx, field) case "timeEstimate": return ec.fieldContext_Task_timeEstimate(ctx, field) + case "deadline": + return ec.fieldContext_Task_deadline(ctx, field) case "assignedTo": return ec.fieldContext_Task_assignedTo(ctx, field) case "organization": @@ -29499,6 +29557,8 @@ func (ec *executionContext) fieldContext_UnassignTaskPayload_task(_ context.Cont return ec.fieldContext_Task_state(ctx, field) case "timeEstimate": return ec.fieldContext_Task_timeEstimate(ctx, field) + case "deadline": + return ec.fieldContext_Task_deadline(ctx, field) case "assignedTo": return ec.fieldContext_Task_assignedTo(ctx, field) case "organization": @@ -30193,6 +30253,8 @@ func (ec *executionContext) fieldContext_UpdateTaskPayload_task(_ context.Contex return ec.fieldContext_Task_state(ctx, field) case "timeEstimate": return ec.fieldContext_Task_timeEstimate(ctx, field) + case "deadline": + return ec.fieldContext_Task_deadline(ctx, field) case "assignedTo": return ec.fieldContext_Task_assignedTo(ctx, field) case "organization": @@ -36789,7 +36851,7 @@ func (ec *executionContext) unmarshalInputCreateTaskInput(ctx context.Context, o asMap[k] = v } - fieldsInOrder := [...]string{"organizationId", "measureId", "name", "description", "timeEstimate", "assignedToId"} + fieldsInOrder := [...]string{"organizationId", "measureId", "name", "description", "timeEstimate", "assignedToId", "deadline"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -36838,6 +36900,13 @@ func (ec *executionContext) unmarshalInputCreateTaskInput(ctx context.Context, o return it, err } it.AssignedToID = data + case "deadline": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("deadline")) + data, err := ec.unmarshalODatetime2ᚖtimeᚐTime(ctx, v) + if err != nil { + return it, err + } + it.Deadline = data } } @@ -38947,7 +39016,7 @@ func (ec *executionContext) unmarshalInputUpdateTaskInput(ctx context.Context, o asMap[k] = v } - fieldsInOrder := [...]string{"taskId", "name", "description", "state", "timeEstimate"} + fieldsInOrder := [...]string{"taskId", "name", "description", "state", "timeEstimate", "deadline"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -38989,6 +39058,13 @@ func (ec *executionContext) unmarshalInputUpdateTaskInput(ctx context.Context, o return it, err } it.TimeEstimate = data + case "deadline": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("deadline")) + data, err := ec.unmarshalODatetime2ᚖtimeᚐTime(ctx, v) + if err != nil { + return it, err + } + it.Deadline = data } } @@ -46022,6 +46098,8 @@ func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj } case "timeEstimate": out.Values[i] = ec._Task_timeEstimate(ctx, field, obj) + case "deadline": + out.Values[i] = ec._Task_deadline(ctx, field, obj) case "assignedTo": field := field diff --git a/pkg/server/api/console/v1/types/task.go b/pkg/server/api/console/v1/types/task.go index c9553cad7..4757b1a71 100644 --- a/pkg/server/api/console/v1/types/task.go +++ b/pkg/server/api/console/v1/types/task.go @@ -52,5 +52,6 @@ func NewTask(t *coredata.Task) *Task { TimeEstimate: t.TimeEstimate, CreatedAt: t.CreatedAt, UpdatedAt: t.UpdatedAt, + Deadline: t.Deadline, } } diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index f3dbc819d..df4a8ec8a 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -313,6 +313,7 @@ type CreateTaskInput struct { Description string `json:"description"` TimeEstimate *time.Duration `json:"timeEstimate,omitempty"` AssignedToID *gid.GID `json:"assignedToId,omitempty"` + Deadline *time.Time `json:"deadline,omitempty"` } type CreateTaskPayload struct { @@ -934,6 +935,7 @@ type Task struct { Description string `json:"description"` State coredata.TaskState `json:"state"` TimeEstimate *time.Duration `json:"timeEstimate,omitempty"` + Deadline *time.Time `json:"deadline,omitempty"` AssignedTo *People `json:"assignedTo,omitempty"` Organization *Organization `json:"organization"` Measure *Measure `json:"measure,omitempty"` @@ -1083,6 +1085,7 @@ type UpdateTaskInput struct { Description *string `json:"description,omitempty"` State *coredata.TaskState `json:"state,omitempty"` TimeEstimate *time.Duration `json:"timeEstimate,omitempty"` + Deadline *time.Time `json:"deadline,omitempty"` } type UpdateTaskPayload struct { diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 7dbee7c2d..3538365cf 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1114,6 +1114,7 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTas Name: input.Name, Description: input.Description, TimeEstimate: input.TimeEstimate, + Deadline: input.Deadline, }) if err != nil { panic(fmt.Errorf("cannot create task: %w", err)) @@ -1134,6 +1135,7 @@ func (r *mutationResolver) UpdateTask(ctx context.Context, input types.UpdateTas Description: input.Description, State: input.State, TimeEstimate: input.TimeEstimate, + Deadline: input.Deadline, }) if err != nil { panic(fmt.Errorf("cannot update task: %w", err))