Refactor large toolbox modules
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s

This commit is contained in:
Shinuwa 2026-08-25 08:20:56 +02:00
parent c2e37ad3f2
commit 6dd64f87c3
19 changed files with 2042 additions and 1659 deletions

View file

@ -1,22 +1,14 @@
// Rôle : fournit l'outil task planner avec tâches récurrentes, parents, pré requis et échange texte.
import { useEffect, useMemo, useRef, useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { TaskPlannerBranch } from "./TaskPlannerItems.jsx";
import { TaskPlannerSettings } from "./TaskPlannerSettings.jsx";
import { TaskPlannerTopbar } from "./TaskPlannerTopbar.jsx";
import {
TASK_TYPES,
WEEK_DAYS,
applyDueResets,
getBoundaryTaskId,
getRootTaskIdsForCategory,
getTaskCategory,
getTaskCategoryDataset,
getTaskParentMap,
getTaskTypeLabel,
moveTask
getTaskParentMap
} from "./taskPlannerUtils.js";
import { useTaskPlannerReorder } from "./useTaskPlannerReorder.js";
export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
@ -29,117 +21,11 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textE
const [prerequisiteNoticeTaskIds, setPrerequisiteNoticeTaskIds] = useState(() => new Set());
const prerequisiteNoticeTimeouts = useRef(new Map());
const parentMap = useMemo(() => getTaskParentMap(data.tasks, data.relations), [data.tasks, data.relations]);
const {
itemReorder,
groupReorder,
getItemProps,
getGroupProps,
getGroupBoundaryProps,
isItemDragging,
isItemDropTarget,
isGroupDragging,
isGroupDropTarget,
isBoundaryDropTarget,
getDropPlacement,
shouldShowGroupBoundaries
} = useGroupedReorder({
namespace: "task-planner",
items: data.tasks,
getItemId: (task) => task.id,
getParentId: (task) => parentMap.get(task.id) || "",
getItemGroup: (task) => getTaskCategoryDataset(task, data.tasks, parentMap),
getEffectiveGroup: (task) => getTaskCategoryDataset(task, data.tasks, parentMap),
reorderFeatures: {
item: {
groupChange: true,
boundaryDrop: true,
rootOnly: true
},
group: {
reorder: true,
boundaryDrop: true,
itemDrop: true,
rootOnly: true
}
},
onItemMove: (operation) => {
const fromTaskId = operation.sourceId;
const fromTask = data.tasks.find((task) => task.id === fromTaskId);
if (!fromTask) return;
function setRootTaskCategory(tasks, category) {
return tasks.map((task) => {
if (task.id !== fromTaskId) return task;
const nextTask = { ...task };
if (category) nextTask.category = category;
else delete nextTask.category;
return nextTask;
});
}
function moveToCategory(category, placement = operation.placement) {
const nextTasks = setRootTaskCategory(data.tasks, category);
const targetIds = category
? getRootTaskIdsForCategory(data.tasks, parentMap, category).filter((taskId) => taskId !== fromTaskId)
: data.tasks.filter((task) => !parentMap.get(task.id) && !getTaskCategory(task) && task.id !== fromTaskId).map((task) => task.id);
const boundaryTaskId = getBoundaryTaskId(data.tasks, targetIds, placement);
save({ ...data, tasks: boundaryTaskId ? moveTask(nextTasks, fromTaskId, boundaryTaskId, placement) : nextTasks });
}
if (operation.targetType === "boundary") {
const targetIds = getRootTaskIdsForCategory(data.tasks, parentMap, operation.targetGroup);
const boundaryTaskId = getBoundaryTaskId(data.tasks, targetIds, operation.placement);
if (!boundaryTaskId) return;
const sourceCategory = getTaskCategoryDataset(fromTask, data.tasks, parentMap);
const tasks = sourceCategory ? setRootTaskCategory(data.tasks, "") : data.tasks;
save({ ...data, tasks: moveTask(tasks, fromTaskId, boundaryTaskId, operation.placement) });
return;
}
if (operation.targetType === "group") {
const targetCategory = operation.targetGroup;
const sourceCategory = getTaskCategoryDataset(fromTask, data.tasks, parentMap);
if (!sourceCategory) {
const targetIds = getRootTaskIdsForCategory(data.tasks, parentMap, targetCategory);
const boundaryTaskId = getBoundaryTaskId(data.tasks, targetIds, operation.placement);
if (boundaryTaskId) save({ ...data, tasks: moveTask(data.tasks, fromTaskId, boundaryTaskId, operation.placement) });
return;
}
moveToCategory(targetCategory);
return;
}
const targetTask = data.tasks.find((task) => task.id === operation.targetId);
const targetCategory = targetTask ? getTaskCategoryDataset(targetTask, data.tasks, parentMap) : "";
const sourceCategory = getTaskCategoryDataset(fromTask, data.tasks, parentMap);
const shouldChangeRootCategory = !parentMap.get(fromTaskId) && targetTask && !parentMap.get(operation.targetId) && targetCategory !== sourceCategory;
const tasks = shouldChangeRootCategory ? setRootTaskCategory(data.tasks, targetCategory) : data.tasks;
save({ ...data, tasks: moveTask(tasks, fromTaskId, operation.targetId, operation.placement) });
},
onGroupMove: (operation) => {
save(applyGroupedReorderOperation(data, {
operation,
itemsKey: "tasks",
groupOrderKey: "categoryOrder",
getItemGroup: (task) => parentMap.get(task.id) ? "" : getTaskCategory(task),
setItemGroup: (task) => task
}));
},
hierarchy: { enabled: true, stickyParents: true }
});
const reorder = {
itemReorder,
groupReorder,
getItemProps,
getGroupProps,
getGroupBoundaryProps,
isItemDragging,
isItemDropTarget,
isGroupDragging,
isGroupDropTarget,
isBoundaryDropTarget,
getDropPlacement,
shouldShowGroupBoundaries
};
function save(nextData) {
context.setModuleData(toolboxId, moduleId, nextData, "taskPlanner");
}
const reorder = useTaskPlannerReorder({ data, parentMap, save });
useEffect(() => {
const intervalId = window.setInterval(() => setNowMs(Date.now()), 60000);
@ -280,75 +166,28 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textE
return (
<div className="task-planner-module">
<div className="task-planner-topbar">
<div className="task-planner-global-controls">
<button
className={`tool-split-scroll-toggle task-planner-hide-completed-toggle ${data.hideCompleted ? "active" : ""}`}
type="button"
onClick={() => save({ ...data, hideCompleted: !data.hideCompleted })}
aria-pressed={data.hideCompleted}
aria-label={textContent.hideCompletedTitle || "Masquer les tâches effectuées"}
title={textContent.hideCompletedTitle || "Masquer les tâches effectuées"}
>
<Icon name={data.hideCompleted ? "eye-closed" : "eye-open"} />
<i aria-hidden="true" />
</button>
<button
className={`task-planner-settings-toggle ${settingsOpen ? "active" : ""}`}
type="button"
onClick={() => setSettingsOpen((value) => !value)}
aria-expanded={settingsOpen}
aria-label={textContent.settingsTitle || "Réglages du planificateur"}
title={textContent.settingsTitle || "Réglages du planificateur"}
>
<Icon name="settings" />
<span>{textContent.settingsButtonLabel || "Paramètres des réinitialisations"}</span>
</button>
</div>
{editing && (
<div className="task-planner-add-stack">
<form className="inline-form task-planner-add-form" onSubmit={addTask}>
<input {...taskDraft.getFieldProps("title", { placeholder: textContent.titlePlaceholder || "Nouvelle tâche" })} />
<select {...taskDraft.getFieldProps("type", { "aria-label": textContent.typeLabel || "Type" })}>
{TASK_TYPES.map((taskType) => <option key={taskType} value={taskType}>{getTaskTypeLabel(taskType, textContent)}</option>)}
</select>
<button className="primary">{textContent.addButton || "Ajouter"}</button>
</form>
</div>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="taskPlanner" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
</div>
)}
</div>
<TaskPlannerTopbar
data={data}
context={context}
editing={editing}
settingsOpen={settingsOpen}
taskDraft={taskDraft}
textContent={textContent}
textExchangeOpen={textExchangeOpen}
onAddTask={addTask}
onHideCompletedToggle={() => save({ ...data, hideCompleted: !data.hideCompleted })}
onSettingsToggle={() => setSettingsOpen((value) => !value)}
onImport={(nextData) => save(nextData)}
/>
{settingsOpen && (
<section className="task-planner-settings" aria-label={textContent.settingsTitle || "Réglages du planificateur"}>
<label>
<span>{textContent.globalWeeklyResetDayLabel || "Reset hebdo"}</span>
<select value={data.weeklyResetDay} onChange={(event) => save({ ...data, weeklyResetDay: Number(event.target.value) })}>
{WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
</select>
</label>
<label>
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
<input type="time" value={data.resetTime} onChange={(event) => save({ ...data, resetTime: event.target.value || "00:00" })} />
</label>
<div className="task-planner-settings-actions">
<button
className="task-planner-clear-completed-button danger"
type="button"
onClick={clearCompletedUniqueTasks}
disabled={!data.tasks.some((task) => task.type === "unique" && task.checked)}
title={textContent.clearCompletedUniqueTitle || "Nettoyer les tâches ponctuelles terminées"}
aria-label={textContent.clearCompletedUniqueTitle || "Nettoyer les tâches ponctuelles terminées"}
>
<Icon name="trash" />
<span>{textContent.clearCompletedUniqueButton || "Nettoyer les tâches ponctuelles terminées"}</span>
</button>
</div>
</section>
<TaskPlannerSettings
data={data}
textContent={textContent}
onWeeklyResetDayChange={(weeklyResetDay) => save({ ...data, weeklyResetDay })}
onResetTimeChange={(resetTime) => save({ ...data, resetTime })}
onClearCompletedUniqueTasks={clearCompletedUniqueTasks}
/>
)}
<ul className="task-planner-list">