@@ -176,6 +176,7 @@ const updateTaskStateMutation = graphql`
|
|||||||
task {
|
task {
|
||||||
id
|
id
|
||||||
state
|
state
|
||||||
|
timeEstimate
|
||||||
version
|
version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1178,6 +1179,124 @@ function ControlViewContent({
|
|||||||
setSearchParams(searchParams);
|
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 (
|
return (
|
||||||
<PageTemplate
|
<PageTemplate
|
||||||
title={data.control.name ?? ""}
|
title={data.control.name ?? ""}
|
||||||
@@ -1824,10 +1943,80 @@ function ControlViewContent({
|
|||||||
? "Completed"
|
? "Completed"
|
||||||
: "In Progress"}
|
: "In Progress"}
|
||||||
</div>
|
</div>
|
||||||
{selectedTask.timeEstimate && (
|
{!isEditingDuration ? (
|
||||||
<div className="text-sm text-gray-500 flex items-center">
|
<div
|
||||||
|
className="text-sm text-gray-500 flex items-center hover:bg-gray-100 px-2 py-1 rounded-md cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
startEditingDuration(selectedTask.timeEstimate)
|
||||||
|
}
|
||||||
|
>
|
||||||
<span className="inline-block w-4 h-4 mr-1">⏱️</span>
|
<span className="inline-block w-4 h-4 mr-1">⏱️</span>
|
||||||
<span>{formatDuration(selectedTask.timeEstimate)}</span>
|
<span>
|
||||||
|
{selectedTask.timeEstimate
|
||||||
|
? formatDuration(selectedTask.timeEstimate)
|
||||||
|
: "Add time estimate"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={editTimeEstimateDays}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditTimeEstimateDays(e.target.value)
|
||||||
|
}
|
||||||
|
className="w-12 p-1 text-xs border rounded"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500">d</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="23"
|
||||||
|
value={editTimeEstimateHours}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditTimeEstimateHours(e.target.value)
|
||||||
|
}
|
||||||
|
className="w-12 p-1 text-xs border rounded"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500">h</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
value={editTimeEstimateMinutes}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditTimeEstimateMinutes(e.target.value)
|
||||||
|
}
|
||||||
|
className="w-12 p-1 text-xs border rounded"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500">m</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="p-1 text-sm text-blue-600 hover:text-blue-800"
|
||||||
|
onClick={() =>
|
||||||
|
handleSaveDuration(
|
||||||
|
selectedTask.id,
|
||||||
|
selectedTask.version
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="p-1 text-sm text-gray-500 hover:text-gray-700"
|
||||||
|
onClick={() => setIsEditingDuration(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<70ae17b69b366175da4d00bfae78ae7f>>
|
* @generated SignedSource<<f0b91a291608cee99d4b22fb2d19e847>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -26,6 +26,7 @@ export type ControlViewUpdateTaskStateMutation$data = {
|
|||||||
readonly task: {
|
readonly task: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly state: TaskState;
|
readonly state: TaskState;
|
||||||
|
readonly timeEstimate: any | null | undefined;
|
||||||
readonly version: number;
|
readonly version: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -80,6 +81,13 @@ v1 = [
|
|||||||
"name": "state",
|
"name": "state",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "timeEstimate",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -112,16 +120,16 @@ return {
|
|||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "1b9a74365007e7c46668815c9e37cd0a",
|
"cacheID": "f8f00bd63de4f5eac131954a8039b40e",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "ControlViewUpdateTaskStateMutation",
|
"name": "ControlViewUpdateTaskStateMutation",
|
||||||
"operationKind": "mutation",
|
"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;
|
export default node;
|
||||||
|
|||||||
@@ -233,6 +233,8 @@ WHERE
|
|||||||
AND version = @expected_version
|
AND version = @expected_version
|
||||||
RETURNING
|
RETURNING
|
||||||
state,
|
state,
|
||||||
|
time_estimate,
|
||||||
|
updated_at,
|
||||||
version;
|
version;
|
||||||
`
|
`
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
@@ -248,7 +250,7 @@ RETURNING
|
|||||||
}
|
}
|
||||||
maps.Copy(args, scope.SQLArguments())
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ func (r *mutationResolver) UpdateTask(ctx context.Context, input types.UpdateTas
|
|||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
Description: input.Description,
|
Description: input.Description,
|
||||||
State: input.State,
|
State: input.State,
|
||||||
|
TimeEstimate: input.TimeEstimate,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot update task: %w", err)
|
return nil, fmt.Errorf("cannot update task: %w", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user