task planner categories optimisation
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
3dc5ed8733
commit
1d3bdd60f1
7 changed files with 217 additions and 37 deletions
|
|
@ -178,6 +178,31 @@ function countTaskTree(taskIds, tasks, parentMap) {
|
|||
return count;
|
||||
}
|
||||
|
||||
function getTaskTreeIds(taskIds, tasks, parentMap) {
|
||||
const ids = [];
|
||||
const visited = new Set();
|
||||
function visit(taskId) {
|
||||
if (visited.has(taskId)) return;
|
||||
visited.add(taskId);
|
||||
ids.push(taskId);
|
||||
tasks
|
||||
.filter((task) => (parentMap.get(task.id) || "") === taskId)
|
||||
.forEach((child) => visit(child.id));
|
||||
}
|
||||
taskIds.forEach(visit);
|
||||
return ids;
|
||||
}
|
||||
|
||||
function countCompletedTaskTree(taskIds, tasks, parentMap) {
|
||||
const taskIdsSet = new Set(getTaskTreeIds(taskIds, tasks, parentMap));
|
||||
return tasks.filter((task) => taskIdsSet.has(task.id) && task.checked).length;
|
||||
}
|
||||
|
||||
function isTaskTreeComplete(taskIds, tasks, parentMap) {
|
||||
const treeIds = getTaskTreeIds(taskIds, tasks, parentMap);
|
||||
return treeIds.length > 0 && treeIds.every((taskId) => getTaskById(tasks, taskId)?.checked);
|
||||
}
|
||||
|
||||
function getTaskCategoryGroups(tasks, parentMap, parentId, sortMode, categoryOrder) {
|
||||
const siblings = getTasksForParent(tasks, parentMap, parentId, sortMode);
|
||||
if (parentId) {
|
||||
|
|
@ -420,6 +445,21 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
save({ ...data, tasks, categoryOrder });
|
||||
}
|
||||
|
||||
function toggleCategoryCollapsed(category) {
|
||||
const collapsedCategories = data.collapsedCategories.includes(category)
|
||||
? data.collapsedCategories.filter((item) => item !== category)
|
||||
: [...data.collapsedCategories, category];
|
||||
save({ ...data, collapsedCategories });
|
||||
}
|
||||
|
||||
function clearCompletedUniqueTasks() {
|
||||
const removedTaskIds = new Set(data.tasks.filter((task) => task.type === "unique" && task.checked).map((task) => task.id));
|
||||
if (!removedTaskIds.size) return;
|
||||
const tasks = data.tasks.filter((task) => !removedTaskIds.has(task.id));
|
||||
const relations = data.relations.filter((relation) => !removedTaskIds.has(relation.fromTaskId) && !removedTaskIds.has(relation.toTaskId));
|
||||
save({ ...data, tasks, relations });
|
||||
}
|
||||
|
||||
function toggleSet(setter, taskId) {
|
||||
setter((current) => {
|
||||
const next = new Set(current);
|
||||
|
|
@ -459,6 +499,17 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
<Icon name="sort-time" />
|
||||
<i aria-hidden="true" />
|
||||
</button>
|
||||
<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"
|
||||
|
|
@ -494,6 +545,19 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
<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>
|
||||
)}
|
||||
|
||||
|
|
@ -523,6 +587,7 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
onRemoveLinkTarget={removeLinkTarget}
|
||||
onSetPrerequisite={setPrerequisite}
|
||||
onSetTaskCategory={setTaskCategory}
|
||||
onToggleCategoryCollapsed={toggleCategoryCollapsed}
|
||||
/>
|
||||
) : (
|
||||
<li className="task-planner-empty">{textContent.emptyTasks || "Aucune tâche planifiée."}</li>
|
||||
|
|
@ -556,6 +621,7 @@ function TaskPlannerBranch({
|
|||
onRemoveLinkTarget,
|
||||
onSetPrerequisite,
|
||||
onSetTaskCategory,
|
||||
onToggleCategoryCollapsed,
|
||||
visited = new Set()
|
||||
}) {
|
||||
const groupedTasks = getTaskCategoryGroups(tasks, parentMap, parentId, data.sortMode, data.categoryOrder);
|
||||
|
|
@ -563,8 +629,51 @@ function TaskPlannerBranch({
|
|||
if (!children.length) return null;
|
||||
const nextVisited = new Set([...visited, ...children.map((task) => task.id)]);
|
||||
|
||||
function hasVisibleDescendants(taskId, seen = new Set()) {
|
||||
if (seen.has(taskId)) return false;
|
||||
seen.add(taskId);
|
||||
return getTasksForParent(tasks, parentMap, taskId, data.sortMode).some((child) => !child.checked || hasVisibleDescendants(child.id, seen));
|
||||
}
|
||||
|
||||
function renderTask(task) {
|
||||
const nestedChildren = getTasksForParent(tasks, parentMap, task.id, data.sortMode).filter((child) => !nextVisited.has(child.id));
|
||||
const nestedBranch = nestedChildren.length ? (
|
||||
<TaskPlannerBranch
|
||||
parentId={task.id}
|
||||
parentMap={parentMap}
|
||||
tasks={tasks}
|
||||
data={data}
|
||||
draggingId={draggingId}
|
||||
dropTarget={dropTarget}
|
||||
draggingCategoryId={draggingCategoryId}
|
||||
categoryDropTarget={categoryDropTarget}
|
||||
textContent={textContent}
|
||||
openDescriptions={openDescriptions}
|
||||
openTaskSettings={openTaskSettings}
|
||||
prerequisiteNoticeTaskIds={prerequisiteNoticeTaskIds}
|
||||
onDragStart={onDragStart}
|
||||
onCategoryDragStart={onCategoryDragStart}
|
||||
onToggleDescription={onToggleDescription}
|
||||
onToggleSettings={onToggleSettings}
|
||||
onPrerequisiteNotice={onPrerequisiteNotice}
|
||||
onUpdateTask={onUpdateTask}
|
||||
onDeleteTask={onDeleteTask}
|
||||
onAddLinkTarget={onAddLinkTarget}
|
||||
onRemoveLinkTarget={onRemoveLinkTarget}
|
||||
onSetPrerequisite={onSetPrerequisite}
|
||||
onSetTaskCategory={onSetTaskCategory}
|
||||
onToggleCategoryCollapsed={onToggleCategoryCollapsed}
|
||||
visited={nextVisited}
|
||||
/>
|
||||
) : null;
|
||||
if (data.hideCompleted && task.checked) {
|
||||
if (!hasVisibleDescendants(task.id)) return null;
|
||||
return (
|
||||
<li className="task-planner-hidden-parent-children" key={`${task.id}:visible-children`}>
|
||||
<ul className="task-planner-list task-planner-child-list">{nestedBranch}</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TaskPlannerItem
|
||||
key={task.id}
|
||||
|
|
@ -591,34 +700,7 @@ function TaskPlannerBranch({
|
|||
onSetPrerequisite={onSetPrerequisite}
|
||||
onSetTaskCategory={onSetTaskCategory}
|
||||
>
|
||||
{nestedChildren.length ? (
|
||||
<TaskPlannerBranch
|
||||
parentId={task.id}
|
||||
parentMap={parentMap}
|
||||
tasks={tasks}
|
||||
data={data}
|
||||
draggingId={draggingId}
|
||||
dropTarget={dropTarget}
|
||||
draggingCategoryId={draggingCategoryId}
|
||||
categoryDropTarget={categoryDropTarget}
|
||||
textContent={textContent}
|
||||
openDescriptions={openDescriptions}
|
||||
openTaskSettings={openTaskSettings}
|
||||
prerequisiteNoticeTaskIds={prerequisiteNoticeTaskIds}
|
||||
onDragStart={onDragStart}
|
||||
onCategoryDragStart={onCategoryDragStart}
|
||||
onToggleDescription={onToggleDescription}
|
||||
onToggleSettings={onToggleSettings}
|
||||
onPrerequisiteNotice={onPrerequisiteNotice}
|
||||
onUpdateTask={onUpdateTask}
|
||||
onDeleteTask={onDeleteTask}
|
||||
onAddLinkTarget={onAddLinkTarget}
|
||||
onRemoveLinkTarget={onRemoveLinkTarget}
|
||||
onSetPrerequisite={onSetPrerequisite}
|
||||
onSetTaskCategory={onSetTaskCategory}
|
||||
visited={nextVisited}
|
||||
/>
|
||||
) : null}
|
||||
{nestedBranch}
|
||||
</TaskPlannerItem>
|
||||
);
|
||||
}
|
||||
|
|
@ -626,10 +708,15 @@ function TaskPlannerBranch({
|
|||
function renderCategory(group) {
|
||||
const groupTasks = group.tasks.filter((task) => !visited.has(task.id));
|
||||
if (!groupTasks.length) return null;
|
||||
if (data.hideCompleted && isTaskTreeComplete(groupTasks.map((task) => task.id), tasks, parentMap)) return null;
|
||||
const groupCount = countTaskTree(groupTasks.map((task) => task.id), tasks, parentMap);
|
||||
const groupCompletedCount = countCompletedTaskTree(groupTasks.map((task) => task.id), tasks, parentMap);
|
||||
const categoryId = getCategorySectionId(parentId, group.category);
|
||||
const isCollapsed = data.collapsedCategories.includes(group.category);
|
||||
const sectionClassName = [
|
||||
"task-planner-category-section",
|
||||
groupCompletedCount === groupCount ? "is-complete" : "",
|
||||
isCollapsed ? "is-collapsed" : "",
|
||||
draggingCategoryId === categoryId ? "is-dragging" : "",
|
||||
categoryDropTarget.id === categoryId || dropTarget.id === categoryId ? "is-drop-target" : "",
|
||||
(categoryDropTarget.id === categoryId && categoryDropTarget.placement === "after") || (dropTarget.id === categoryId && dropTarget.placement === "after") ? "drop-after" : ""
|
||||
|
|
@ -637,7 +724,7 @@ function TaskPlannerBranch({
|
|||
return (
|
||||
<li className={sectionClassName} key={`${parentId || "root"}:${group.category}`} data-parent-id={parentId} data-category={group.category}>
|
||||
<div className="checklist-section-header task-planner-category-header">
|
||||
<div>
|
||||
<div className="task-planner-category-title">
|
||||
<button
|
||||
className="task-planner-category-drag-handle"
|
||||
type="button"
|
||||
|
|
@ -649,11 +736,25 @@ function TaskPlannerBranch({
|
|||
</button>
|
||||
<h3>{group.category}</h3>
|
||||
</div>
|
||||
<span>{groupCount}</span>
|
||||
<div className="task-planner-category-actions">
|
||||
<span>{groupCompletedCount} / {groupCount}</span>
|
||||
<button
|
||||
className="checklist-section-collapse-button"
|
||||
type="button"
|
||||
onClick={() => onToggleCategoryCollapsed(group.category)}
|
||||
aria-expanded={!isCollapsed}
|
||||
title={isCollapsed ? textContent.showCategoryTitle || "Afficher cette catégorie" : textContent.hideCategoryTitle || "Réduire cette catégorie"}
|
||||
aria-label={isCollapsed ? textContent.showCategoryTitle || "Afficher cette catégorie" : textContent.hideCategoryTitle || "Réduire cette catégorie"}
|
||||
>
|
||||
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="task-planner-list">
|
||||
{groupTasks.map(renderTask)}
|
||||
</ul>
|
||||
{!isCollapsed && (
|
||||
<ul className="task-planner-list">
|
||||
{groupTasks.map(renderTask).filter(Boolean)}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -384,12 +384,17 @@ export function normalizeTaskPlannerData(data) {
|
|||
return normalized;
|
||||
});
|
||||
const taskIds = new Set(tasks.map((task) => task.id));
|
||||
const categoryOrder = normalizeTaskPlannerCategoryOrder(data?.categoryOrder, tasks);
|
||||
return {
|
||||
sortMode: TASK_SORT_MODES.has(data?.sortMode) ? data.sortMode : "manual",
|
||||
weeklyResetDay: normalizeWeekDay(data?.weeklyResetDay),
|
||||
resetTime: isClockTimeString(data?.resetTime) ? data.resetTime : DEFAULT_TASK_PLANNER_RESET_TIME,
|
||||
lastResetAt: normalizeTimestamp(data?.lastResetAt),
|
||||
categoryOrder: normalizeTaskPlannerCategoryOrder(data?.categoryOrder, tasks),
|
||||
hideCompleted: data?.hideCompleted === true,
|
||||
collapsedCategories: (Array.isArray(data?.collapsedCategories) ? data.collapsedCategories : [])
|
||||
.map((category) => String(category || "").trim())
|
||||
.filter((category, index, categories) => category && categoryOrder.includes(category) && categories.indexOf(category) === index),
|
||||
categoryOrder,
|
||||
tasks,
|
||||
relations: relations.filter((relation) => taskIds.has(relation.fromTaskId) && taskIds.has(relation.toTaskId))
|
||||
};
|
||||
|
|
@ -535,6 +540,8 @@ export function compactModuleDataForStorage(type, value) {
|
|||
if (normalized.weeklyResetDay !== DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY) compact.weeklyResetDay = normalized.weeklyResetDay;
|
||||
if (normalized.resetTime !== DEFAULT_TASK_PLANNER_RESET_TIME) compact.resetTime = normalized.resetTime;
|
||||
if (normalized.lastResetAt) compact.lastResetAt = normalized.lastResetAt;
|
||||
if (normalized.hideCompleted) compact.hideCompleted = true;
|
||||
if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories;
|
||||
if (normalized.tasks.length) {
|
||||
compact.tasks = normalized.tasks.map((task) => {
|
||||
const compactTask = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue