Allow to edit time estimate

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-24 11:09:50 +01:00
parent 59ce9a28fa
commit 5887144054
4 changed files with 208 additions and 8 deletions

View File

@@ -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 (
<PageTemplate
title={data.control.name ?? ""}
@@ -1824,10 +1943,80 @@ function ControlViewContent({
? "Completed"
: "In Progress"}
</div>
{selectedTask.timeEstimate && (
<div className="text-sm text-gray-500 flex items-center">
{!isEditingDuration ? (
<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>{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>

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<70ae17b69b366175da4d00bfae78ae7f>>
* @generated SignedSource<<f0b91a291608cee99d4b22fb2d19e847>>
* @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;