From 58871440548da9381bdc01b490744f873d7b37e5 Mon Sep 17 00:00:00 2001 From: gearnode Date: Mon, 24 Mar 2025 11:09:50 +0100 Subject: [PATCH] Allow to edit time estimate Signed-off-by: gearnode --- .../frameworks/controls/ControlView.tsx | 195 +++++++++++++++++- ...trolViewUpdateTaskStateMutation.graphql.ts | 16 +- pkg/coredata/task.go | 4 +- pkg/server/api/console/v1/v1_resolver.go | 1 + 4 files changed, 208 insertions(+), 8 deletions(-) diff --git a/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx b/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx index d4ece5c45..ead4699c2 100644 --- a/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx +++ b/apps/console/src/pages/organizations/frameworks/controls/ControlView.tsx @@ -176,6 +176,7 @@ const updateTaskStateMutation = graphql` task { id state + timeEstimate version } } @@ -1178,6 +1179,124 @@ function ControlViewContent({ setSearchParams(searchParams); }; + // Add state variables for tracking edit mode and duration components + const [isEditingDuration, setIsEditingDuration] = useState(false); + const [editTimeEstimateDays, setEditTimeEstimateDays] = useState(""); + const [editTimeEstimateHours, setEditTimeEstimateHours] = useState(""); + const [editTimeEstimateMinutes, setEditTimeEstimateMinutes] = useState(""); + + // Function to parse ISO duration string into components for editing + const parseISODuration = useCallback( + (duration: string | null | undefined) => { + if (!duration || !duration.startsWith("P")) { + return { days: "", hours: "", minutes: "" }; + } + + try { + const durationRegex = + /P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/; + const matches = duration.match(durationRegex); + + if (!matches) return { days: "", hours: "", minutes: "" }; + + // We only care about days, hours, and minutes + const days = matches[3] ? matches[3] : ""; + const hours = matches[4] ? matches[4] : ""; + const minutes = matches[5] ? matches[5] : ""; + + return { days, hours, minutes }; + } catch (error) { + console.error("Error parsing duration:", error); + return { days: "", hours: "", minutes: "" }; + } + }, + [] + ); + + // Function to handle saving the updated duration + const handleSaveDuration = useCallback( + (taskId: string, version: number) => { + // Convert to ISO duration format + let duration = "P"; + + if (editTimeEstimateDays && parseInt(editTimeEstimateDays) > 0) { + duration += `${parseInt(editTimeEstimateDays)}D`; + } + + if ( + (editTimeEstimateHours && parseInt(editTimeEstimateHours) > 0) || + (editTimeEstimateMinutes && parseInt(editTimeEstimateMinutes) > 0) + ) { + duration += "T"; + + if (editTimeEstimateHours && parseInt(editTimeEstimateHours) > 0) { + duration += `${parseInt(editTimeEstimateHours)}H`; + } + + if (editTimeEstimateMinutes && parseInt(editTimeEstimateMinutes) > 0) { + duration += `${parseInt(editTimeEstimateMinutes)}M`; + } + } + + // If no valid time components were provided, use null (remove the time estimate) + const timeEstimate = duration === "P" ? null : duration; + + updateTask({ + variables: { + input: { + taskId, + timeEstimate, + expectedVersion: version, + }, + }, + onCompleted: () => { + toast({ + title: "Task updated", + description: "Time estimate has been updated successfully.", + }); + setIsEditingDuration(false); + + // Update the selected task state if it's the current task + if (selectedTask && selectedTask.id === taskId) { + setSelectedTask({ + ...selectedTask, + timeEstimate, + version: version + 1, + }); + } + }, + onError: (error) => { + toast({ + title: "Error updating task", + description: error.message, + variant: "destructive", + }); + }, + }); + }, + [ + editTimeEstimateDays, + editTimeEstimateHours, + editTimeEstimateMinutes, + updateTask, + toast, + selectedTask, + setSelectedTask, + ] + ); + + // Function to start editing duration + const startEditingDuration = useCallback( + (duration: string | null | undefined) => { + const { days, hours, minutes } = parseISODuration(duration); + setEditTimeEstimateDays(days); + setEditTimeEstimateHours(hours); + setEditTimeEstimateMinutes(minutes); + setIsEditingDuration(true); + }, + [parseISODuration] + ); + return ( - {selectedTask.timeEstimate && ( -
+ {!isEditingDuration ? ( +
+ startEditingDuration(selectedTask.timeEstimate) + } + > ⏱️ - {formatDuration(selectedTask.timeEstimate)} + + {selectedTask.timeEstimate + ? formatDuration(selectedTask.timeEstimate) + : "Add time estimate"} + +
+ ) : ( +
+
+ + setEditTimeEstimateDays(e.target.value) + } + className="w-12 p-1 text-xs border rounded" + placeholder="0" + /> + d +
+
+ + setEditTimeEstimateHours(e.target.value) + } + className="w-12 p-1 text-xs border rounded" + placeholder="0" + /> + h +
+
+ + setEditTimeEstimateMinutes(e.target.value) + } + className="w-12 p-1 text-xs border rounded" + placeholder="0" + /> + m +
+ +
)}
diff --git a/apps/console/src/pages/organizations/frameworks/controls/__generated__/ControlViewUpdateTaskStateMutation.graphql.ts b/apps/console/src/pages/organizations/frameworks/controls/__generated__/ControlViewUpdateTaskStateMutation.graphql.ts index 703d8d0d3..225593897 100644 --- a/apps/console/src/pages/organizations/frameworks/controls/__generated__/ControlViewUpdateTaskStateMutation.graphql.ts +++ b/apps/console/src/pages/organizations/frameworks/controls/__generated__/ControlViewUpdateTaskStateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<70ae17b69b366175da4d00bfae78ae7f>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -26,6 +26,7 @@ export type ControlViewUpdateTaskStateMutation$data = { readonly task: { readonly id: string; readonly state: TaskState; + readonly timeEstimate: any | null | undefined; readonly version: number; }; }; @@ -80,6 +81,13 @@ v1 = [ "name": "state", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "timeEstimate", + "storageKey": null + }, { "alias": null, "args": null, @@ -112,16 +120,16 @@ return { "selections": (v1/*: any*/) }, "params": { - "cacheID": "1b9a74365007e7c46668815c9e37cd0a", + "cacheID": "f8f00bd63de4f5eac131954a8039b40e", "id": null, "metadata": {}, "name": "ControlViewUpdateTaskStateMutation", "operationKind": "mutation", - "text": "mutation ControlViewUpdateTaskStateMutation(\n $input: UpdateTaskInput!\n) {\n updateTask(input: $input) {\n task {\n id\n state\n version\n }\n }\n}\n" + "text": "mutation ControlViewUpdateTaskStateMutation(\n $input: UpdateTaskInput!\n) {\n updateTask(input: $input) {\n task {\n id\n state\n timeEstimate\n version\n }\n }\n}\n" } }; })(); -(node as any).hash = "7cb2f42aadcc5f709377d2bf9dfea2da"; +(node as any).hash = "f53df99b24c2f5f233f8d865885718ea"; export default node; diff --git a/pkg/coredata/task.go b/pkg/coredata/task.go index e3cb9d892..417619819 100644 --- a/pkg/coredata/task.go +++ b/pkg/coredata/task.go @@ -233,6 +233,8 @@ WHERE AND version = @expected_version RETURNING state, + time_estimate, + updated_at, version; ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -248,7 +250,7 @@ RETURNING } maps.Copy(args, scope.SQLArguments()) - err := conn.QueryRow(ctx, q, args).Scan(&t.State, &t.Version) + err := conn.QueryRow(ctx, q, args).Scan(&t.State, &t.TimeEstimate, &t.UpdatedAt, &t.Version) return err } diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index bb9729807..e3041104d 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -287,6 +287,7 @@ func (r *mutationResolver) UpdateTask(ctx context.Context, input types.UpdateTas Name: input.Name, Description: input.Description, State: input.State, + TimeEstimate: input.TimeEstimate, }) if err != nil { return nil, fmt.Errorf("cannot update task: %w", err)