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
|
|
@ -269,7 +269,9 @@ Type : `taskPlanner`
|
||||||
"weeklyResetDay": 1,
|
"weeklyResetDay": 1,
|
||||||
"resetTime": "00:00",
|
"resetTime": "00:00",
|
||||||
"lastResetAt": 1760000000000,
|
"lastResetAt": 1760000000000,
|
||||||
|
"hideCompleted": true,
|
||||||
"categoryOrder": ["Raid", "Farm"],
|
"categoryOrder": ["Raid", "Farm"],
|
||||||
|
"collapsedCategories": ["Farm"],
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"id": "task1",
|
"id": "task1",
|
||||||
|
|
@ -309,10 +311,12 @@ Notes :
|
||||||
- `sortMode` vaut `manual` ou `type`. Le tri par type affiche quotidiennes, hebdo, puis uniques.
|
- `sortMode` vaut `manual` ou `type`. Le tri par type affiche quotidiennes, hebdo, puis uniques.
|
||||||
- `weeklyResetDay` suit les jours JavaScript : `0` dimanche, `1` lundi, ..., `6` samedi.
|
- `weeklyResetDay` suit les jours JavaScript : `0` dimanche, `1` lundi, ..., `6` samedi.
|
||||||
- `resetTime` est global à l'outil et vaut `HH:MM`.
|
- `resetTime` est global à l'outil et vaut `HH:MM`.
|
||||||
|
- `hideCompleted` masque les tâches effectuées à l'affichage, ainsi que les catégories dont toutes les tâches sont effectuées.
|
||||||
- `weeklyResetDay` sur une tâche hebdo surcharge le jour global.
|
- `weeklyResetDay` sur une tâche hebdo surcharge le jour global.
|
||||||
- `category` sur une tâche racine est facultatif. Les tâches enfants héritent de la catégorie effective de leur parent et ne stockent pas de catégorie propre.
|
- `category` sur une tâche racine est facultatif. Les tâches enfants héritent de la catégorie effective de leur parent et ne stockent pas de catégorie propre.
|
||||||
- Les tâches catégorisées sont regroupées par catégorie au niveau racine.
|
- Les tâches catégorisées sont regroupées par catégorie au niveau racine.
|
||||||
- `categoryOrder` stocke l'ordre d'affichage des catégories utilisées. Les catégories absentes des tâches sont retirées à la normalisation.
|
- `categoryOrder` stocke l'ordre d'affichage des catégories utilisées. Les catégories absentes des tâches sont retirées à la normalisation.
|
||||||
|
- `collapsedCategories` stocke les noms des catégories repliées.
|
||||||
- `relations` stocke les parents directionnels. `fromTaskId` désigne la tâche enfant, `toTaskId` son parent.
|
- `relations` stocke les parents directionnels. `fromTaskId` désigne la tâche enfant, `toTaskId` son parent.
|
||||||
- une tâche ne conserve qu'un seul parent.
|
- une tâche ne conserve qu'un seul parent.
|
||||||
- `prerequisite: true` marque aussi ce parent comme pré requis.
|
- `prerequisite: true` marque aussi ce parent comme pré requis.
|
||||||
|
|
@ -325,9 +329,11 @@ Stockage compact :
|
||||||
- `weeklyResetDay` global est omis si la valeur vaut `1`.
|
- `weeklyResetDay` global est omis si la valeur vaut `1`.
|
||||||
- `resetTime` est omis si la valeur vaut `00:00`.
|
- `resetTime` est omis si la valeur vaut `00:00`.
|
||||||
- `lastResetAt` est omis si la valeur vaut `0`.
|
- `lastResetAt` est omis si la valeur vaut `0`.
|
||||||
|
- `hideCompleted` est omis si `false`.
|
||||||
- `description` est omis si vide.
|
- `description` est omis si vide.
|
||||||
- `category` est omis si vide ou si la tâche a un parent.
|
- `category` est omis si vide ou si la tâche a un parent.
|
||||||
- `categoryOrder` est omis si aucune catégorie n'est utilisée.
|
- `categoryOrder` est omis si aucune catégorie n'est utilisée.
|
||||||
|
- `collapsedCategories` est omis si aucune catégorie n'est repliée.
|
||||||
- `checked` et `checkedAt` sont omis si la tâche n'est pas effectuée.
|
- `checked` et `checkedAt` sont omis si la tâche n'est pas effectuée.
|
||||||
- `weeklyResetDay` unitaire est omis si non défini.
|
- `weeklyResetDay` unitaire est omis si non défini.
|
||||||
- `relations` est omis si vide.
|
- `relations` est omis si vide.
|
||||||
|
|
|
||||||
|
|
@ -192,8 +192,11 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.taskPlanner.settingsTitle",
|
"toolboxes.modules.taskPlanner.settingsTitle",
|
||||||
"toolboxes.modules.taskPlanner.settingsButtonLabel",
|
"toolboxes.modules.taskPlanner.settingsButtonLabel",
|
||||||
"toolboxes.modules.taskPlanner.sortModeLabel",
|
"toolboxes.modules.taskPlanner.sortModeLabel",
|
||||||
|
"toolboxes.modules.taskPlanner.hideCompletedTitle",
|
||||||
"toolboxes.modules.taskPlanner.globalWeeklyResetDayLabel",
|
"toolboxes.modules.taskPlanner.globalWeeklyResetDayLabel",
|
||||||
"toolboxes.modules.taskPlanner.resetTimeLabel",
|
"toolboxes.modules.taskPlanner.resetTimeLabel",
|
||||||
|
"toolboxes.modules.taskPlanner.clearCompletedUniqueTitle",
|
||||||
|
"toolboxes.modules.taskPlanner.clearCompletedUniqueButton",
|
||||||
"toolboxes.modules.taskPlanner.titlePlaceholder",
|
"toolboxes.modules.taskPlanner.titlePlaceholder",
|
||||||
"toolboxes.modules.taskPlanner.titleLabel",
|
"toolboxes.modules.taskPlanner.titleLabel",
|
||||||
"toolboxes.modules.taskPlanner.descriptionPlaceholder",
|
"toolboxes.modules.taskPlanner.descriptionPlaceholder",
|
||||||
|
|
@ -217,6 +220,8 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.taskPlanner.categoryLabel",
|
"toolboxes.modules.taskPlanner.categoryLabel",
|
||||||
"toolboxes.modules.taskPlanner.categoryPlaceholder",
|
"toolboxes.modules.taskPlanner.categoryPlaceholder",
|
||||||
"toolboxes.modules.taskPlanner.categoryReorderTitle",
|
"toolboxes.modules.taskPlanner.categoryReorderTitle",
|
||||||
|
"toolboxes.modules.taskPlanner.showCategoryTitle",
|
||||||
|
"toolboxes.modules.taskPlanner.hideCategoryTitle",
|
||||||
"toolboxes.modules.taskPlanner.taskWeeklyResetDayLabel",
|
"toolboxes.modules.taskPlanner.taskWeeklyResetDayLabel",
|
||||||
"toolboxes.modules.taskPlanner.inheritWeeklyResetDay",
|
"toolboxes.modules.taskPlanner.inheritWeeklyResetDay",
|
||||||
"toolboxes.modules.taskPlanner.linksLabel",
|
"toolboxes.modules.taskPlanner.linksLabel",
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ test("task planner data normalizes invalid settings and relations", () => {
|
||||||
sortMode: "unknown",
|
sortMode: "unknown",
|
||||||
weeklyResetDay: 9,
|
weeklyResetDay: 9,
|
||||||
resetTime: "27:80",
|
resetTime: "27:80",
|
||||||
|
hideCompleted: true,
|
||||||
categoryOrder: ["Raid", "Missing", "Farm", "Raid"],
|
categoryOrder: ["Raid", "Missing", "Farm", "Raid"],
|
||||||
|
collapsedCategories: ["Raid", "", "Raid"],
|
||||||
tasks: [
|
tasks: [
|
||||||
{ id: "task1", title: "Daily", type: "daily", checked: true, checkedAt: 1000, category: "Farm" },
|
{ id: "task1", title: "Daily", type: "daily", checked: true, checkedAt: 1000, category: "Farm" },
|
||||||
{ id: "task2", title: "Weekly", type: "weekly", weeklyResetDay: 4, category: "Raid" },
|
{ id: "task2", title: "Weekly", type: "weekly", weeklyResetDay: 4, category: "Raid" },
|
||||||
|
|
@ -50,7 +52,9 @@ test("task planner data normalizes invalid settings and relations", () => {
|
||||||
assert.equal(normalized.sortMode, "manual");
|
assert.equal(normalized.sortMode, "manual");
|
||||||
assert.equal(normalized.weeklyResetDay, 1);
|
assert.equal(normalized.weeklyResetDay, 1);
|
||||||
assert.equal(normalized.resetTime, "00:00");
|
assert.equal(normalized.resetTime, "00:00");
|
||||||
|
assert.equal(normalized.hideCompleted, true);
|
||||||
assert.deepEqual(normalized.categoryOrder, ["Raid"]);
|
assert.deepEqual(normalized.categoryOrder, ["Raid"]);
|
||||||
|
assert.deepEqual(normalized.collapsedCategories, ["Raid"]);
|
||||||
assert.equal(normalized.tasks[0].category, undefined);
|
assert.equal(normalized.tasks[0].category, undefined);
|
||||||
assert.equal(normalized.tasks[1].weeklyResetDay, 4);
|
assert.equal(normalized.tasks[1].weeklyResetDay, 4);
|
||||||
assert.equal(normalized.tasks[1].category, "Raid");
|
assert.equal(normalized.tasks[1].category, "Raid");
|
||||||
|
|
@ -65,6 +69,8 @@ test("task planner storage compacts defaults and export remaps task relations",
|
||||||
sortMode: "type",
|
sortMode: "type",
|
||||||
weeklyResetDay: 1,
|
weeklyResetDay: 1,
|
||||||
resetTime: "00:00",
|
resetTime: "00:00",
|
||||||
|
hideCompleted: true,
|
||||||
|
collapsedCategories: ["Raid"],
|
||||||
tasks: [
|
tasks: [
|
||||||
{ id: "task1", title: "Daily", description: "", type: "daily", checked: false },
|
{ id: "task1", title: "Daily", description: "", type: "daily", checked: false },
|
||||||
{ id: "task2", title: "Weekly", description: "Run", type: "weekly", checked: true, checkedAt: 2000, weeklyResetDay: 5, category: "Raid" }
|
{ id: "task2", title: "Weekly", description: "Run", type: "weekly", checked: true, checkedAt: 2000, weeklyResetDay: 5, category: "Raid" }
|
||||||
|
|
@ -78,6 +84,8 @@ test("task planner storage compacts defaults and export remaps task relations",
|
||||||
assert.equal(compact.sortMode, "type");
|
assert.equal(compact.sortMode, "type");
|
||||||
assert.equal(compact.weeklyResetDay, undefined);
|
assert.equal(compact.weeklyResetDay, undefined);
|
||||||
assert.equal(compact.resetTime, undefined);
|
assert.equal(compact.resetTime, undefined);
|
||||||
|
assert.equal(compact.hideCompleted, true);
|
||||||
|
assert.deepEqual(compact.collapsedCategories, ["Raid"]);
|
||||||
assert.deepEqual(compact.categoryOrder, ["Raid"]);
|
assert.deepEqual(compact.categoryOrder, ["Raid"]);
|
||||||
assert.deepEqual(compact.tasks[0], { id: "task1", title: "Daily", type: "daily" });
|
assert.deepEqual(compact.tasks[0], { id: "task1", title: "Daily", type: "daily" });
|
||||||
assert.equal(compact.tasks[1].category, "Raid");
|
assert.equal(compact.tasks[1].category, "Raid");
|
||||||
|
|
|
||||||
|
|
@ -326,8 +326,11 @@
|
||||||
"settingsTitle": "Réglages du planificateur",
|
"settingsTitle": "Réglages du planificateur",
|
||||||
"settingsButtonLabel": "Paramètres des réinitialisations",
|
"settingsButtonLabel": "Paramètres des réinitialisations",
|
||||||
"sortModeLabel": "Tri automatique par type",
|
"sortModeLabel": "Tri automatique par type",
|
||||||
|
"hideCompletedTitle": "Masquer les tâches effectuées",
|
||||||
"globalWeeklyResetDayLabel": "Reset hebdo",
|
"globalWeeklyResetDayLabel": "Reset hebdo",
|
||||||
"resetTimeLabel": "Heure de reset",
|
"resetTimeLabel": "Heure de reset",
|
||||||
|
"clearCompletedUniqueTitle": "Nettoyer les tâches ponctuelles effectuées",
|
||||||
|
"clearCompletedUniqueButton": "Nettoyer les tâches ponctuelles terminées",
|
||||||
"titlePlaceholder": "Nouvelle tâche",
|
"titlePlaceholder": "Nouvelle tâche",
|
||||||
"titleLabel": "Titre",
|
"titleLabel": "Titre",
|
||||||
"descriptionPlaceholder": "Description",
|
"descriptionPlaceholder": "Description",
|
||||||
|
|
@ -351,6 +354,8 @@
|
||||||
"categoryLabel": "Catégorie",
|
"categoryLabel": "Catégorie",
|
||||||
"categoryPlaceholder": "Nom de catégorie",
|
"categoryPlaceholder": "Nom de catégorie",
|
||||||
"categoryReorderTitle": "Déplacer la catégorie",
|
"categoryReorderTitle": "Déplacer la catégorie",
|
||||||
|
"showCategoryTitle": "Afficher cette catégorie",
|
||||||
|
"hideCategoryTitle": "Réduire cette catégorie",
|
||||||
"taskWeeklyResetDayLabel": "Jour hebdo",
|
"taskWeeklyResetDayLabel": "Jour hebdo",
|
||||||
"inheritWeeklyResetDay": "Global",
|
"inheritWeeklyResetDay": "Global",
|
||||||
"linksLabel": "Parent",
|
"linksLabel": "Parent",
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,31 @@ function countTaskTree(taskIds, tasks, parentMap) {
|
||||||
return count;
|
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) {
|
function getTaskCategoryGroups(tasks, parentMap, parentId, sortMode, categoryOrder) {
|
||||||
const siblings = getTasksForParent(tasks, parentMap, parentId, sortMode);
|
const siblings = getTasksForParent(tasks, parentMap, parentId, sortMode);
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
|
|
@ -420,6 +445,21 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||||
save({ ...data, tasks, categoryOrder });
|
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) {
|
function toggleSet(setter, taskId) {
|
||||||
setter((current) => {
|
setter((current) => {
|
||||||
const next = new Set(current);
|
const next = new Set(current);
|
||||||
|
|
@ -459,6 +499,17 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||||
<Icon name="sort-time" />
|
<Icon name="sort-time" />
|
||||||
<i aria-hidden="true" />
|
<i aria-hidden="true" />
|
||||||
</button>
|
</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
|
<button
|
||||||
className={`task-planner-settings-toggle ${settingsOpen ? "active" : ""}`}
|
className={`task-planner-settings-toggle ${settingsOpen ? "active" : ""}`}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -494,6 +545,19 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||||
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
|
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
|
||||||
<input type="time" value={data.resetTime} onChange={(event) => save({ ...data, resetTime: event.target.value || "00:00" })} />
|
<input type="time" value={data.resetTime} onChange={(event) => save({ ...data, resetTime: event.target.value || "00:00" })} />
|
||||||
</label>
|
</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>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -523,6 +587,7 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||||
onRemoveLinkTarget={removeLinkTarget}
|
onRemoveLinkTarget={removeLinkTarget}
|
||||||
onSetPrerequisite={setPrerequisite}
|
onSetPrerequisite={setPrerequisite}
|
||||||
onSetTaskCategory={setTaskCategory}
|
onSetTaskCategory={setTaskCategory}
|
||||||
|
onToggleCategoryCollapsed={toggleCategoryCollapsed}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<li className="task-planner-empty">{textContent.emptyTasks || "Aucune tâche planifiée."}</li>
|
<li className="task-planner-empty">{textContent.emptyTasks || "Aucune tâche planifiée."}</li>
|
||||||
|
|
@ -556,6 +621,7 @@ function TaskPlannerBranch({
|
||||||
onRemoveLinkTarget,
|
onRemoveLinkTarget,
|
||||||
onSetPrerequisite,
|
onSetPrerequisite,
|
||||||
onSetTaskCategory,
|
onSetTaskCategory,
|
||||||
|
onToggleCategoryCollapsed,
|
||||||
visited = new Set()
|
visited = new Set()
|
||||||
}) {
|
}) {
|
||||||
const groupedTasks = getTaskCategoryGroups(tasks, parentMap, parentId, data.sortMode, data.categoryOrder);
|
const groupedTasks = getTaskCategoryGroups(tasks, parentMap, parentId, data.sortMode, data.categoryOrder);
|
||||||
|
|
@ -563,8 +629,51 @@ function TaskPlannerBranch({
|
||||||
if (!children.length) return null;
|
if (!children.length) return null;
|
||||||
const nextVisited = new Set([...visited, ...children.map((task) => task.id)]);
|
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) {
|
function renderTask(task) {
|
||||||
const nestedChildren = getTasksForParent(tasks, parentMap, task.id, data.sortMode).filter((child) => !nextVisited.has(child.id));
|
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 (
|
return (
|
||||||
<TaskPlannerItem
|
<TaskPlannerItem
|
||||||
key={task.id}
|
key={task.id}
|
||||||
|
|
@ -591,34 +700,7 @@ function TaskPlannerBranch({
|
||||||
onSetPrerequisite={onSetPrerequisite}
|
onSetPrerequisite={onSetPrerequisite}
|
||||||
onSetTaskCategory={onSetTaskCategory}
|
onSetTaskCategory={onSetTaskCategory}
|
||||||
>
|
>
|
||||||
{nestedChildren.length ? (
|
{nestedBranch}
|
||||||
<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}
|
|
||||||
</TaskPlannerItem>
|
</TaskPlannerItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -626,10 +708,15 @@ function TaskPlannerBranch({
|
||||||
function renderCategory(group) {
|
function renderCategory(group) {
|
||||||
const groupTasks = group.tasks.filter((task) => !visited.has(task.id));
|
const groupTasks = group.tasks.filter((task) => !visited.has(task.id));
|
||||||
if (!groupTasks.length) return null;
|
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 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 categoryId = getCategorySectionId(parentId, group.category);
|
||||||
|
const isCollapsed = data.collapsedCategories.includes(group.category);
|
||||||
const sectionClassName = [
|
const sectionClassName = [
|
||||||
"task-planner-category-section",
|
"task-planner-category-section",
|
||||||
|
groupCompletedCount === groupCount ? "is-complete" : "",
|
||||||
|
isCollapsed ? "is-collapsed" : "",
|
||||||
draggingCategoryId === categoryId ? "is-dragging" : "",
|
draggingCategoryId === categoryId ? "is-dragging" : "",
|
||||||
categoryDropTarget.id === categoryId || dropTarget.id === categoryId ? "is-drop-target" : "",
|
categoryDropTarget.id === categoryId || dropTarget.id === categoryId ? "is-drop-target" : "",
|
||||||
(categoryDropTarget.id === categoryId && categoryDropTarget.placement === "after") || (dropTarget.id === categoryId && dropTarget.placement === "after") ? "drop-after" : ""
|
(categoryDropTarget.id === categoryId && categoryDropTarget.placement === "after") || (dropTarget.id === categoryId && dropTarget.placement === "after") ? "drop-after" : ""
|
||||||
|
|
@ -637,7 +724,7 @@ function TaskPlannerBranch({
|
||||||
return (
|
return (
|
||||||
<li className={sectionClassName} key={`${parentId || "root"}:${group.category}`} data-parent-id={parentId} data-category={group.category}>
|
<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 className="checklist-section-header task-planner-category-header">
|
||||||
<div>
|
<div className="task-planner-category-title">
|
||||||
<button
|
<button
|
||||||
className="task-planner-category-drag-handle"
|
className="task-planner-category-drag-handle"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -649,11 +736,25 @@ function TaskPlannerBranch({
|
||||||
</button>
|
</button>
|
||||||
<h3>{group.category}</h3>
|
<h3>{group.category}</h3>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<ul className="task-planner-list">
|
{!isCollapsed && (
|
||||||
{groupTasks.map(renderTask)}
|
<ul className="task-planner-list">
|
||||||
</ul>
|
{groupTasks.map(renderTask).filter(Boolean)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -384,12 +384,17 @@ export function normalizeTaskPlannerData(data) {
|
||||||
return normalized;
|
return normalized;
|
||||||
});
|
});
|
||||||
const taskIds = new Set(tasks.map((task) => task.id));
|
const taskIds = new Set(tasks.map((task) => task.id));
|
||||||
|
const categoryOrder = normalizeTaskPlannerCategoryOrder(data?.categoryOrder, tasks);
|
||||||
return {
|
return {
|
||||||
sortMode: TASK_SORT_MODES.has(data?.sortMode) ? data.sortMode : "manual",
|
sortMode: TASK_SORT_MODES.has(data?.sortMode) ? data.sortMode : "manual",
|
||||||
weeklyResetDay: normalizeWeekDay(data?.weeklyResetDay),
|
weeklyResetDay: normalizeWeekDay(data?.weeklyResetDay),
|
||||||
resetTime: isClockTimeString(data?.resetTime) ? data.resetTime : DEFAULT_TASK_PLANNER_RESET_TIME,
|
resetTime: isClockTimeString(data?.resetTime) ? data.resetTime : DEFAULT_TASK_PLANNER_RESET_TIME,
|
||||||
lastResetAt: normalizeTimestamp(data?.lastResetAt),
|
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,
|
tasks,
|
||||||
relations: relations.filter((relation) => taskIds.has(relation.fromTaskId) && taskIds.has(relation.toTaskId))
|
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.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.resetTime !== DEFAULT_TASK_PLANNER_RESET_TIME) compact.resetTime = normalized.resetTime;
|
||||||
if (normalized.lastResetAt) compact.lastResetAt = normalized.lastResetAt;
|
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) {
|
if (normalized.tasks.length) {
|
||||||
compact.tasks = normalized.tasks.map((task) => {
|
compact.tasks = normalized.tasks.map((task) => {
|
||||||
const compactTask = {
|
const compactTask = {
|
||||||
|
|
|
||||||
|
|
@ -1235,10 +1235,26 @@ textarea:focus {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-planner-clear-completed-button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 38px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-planner-clear-completed-button .ui-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.task-planner-settings {
|
.task-planner-settings {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(130px, 1fr) minmax(110px, 0.72fr);
|
grid-template-columns: minmax(130px, 1fr) minmax(110px, 0.72fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
align-items: end;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
|
@ -1247,6 +1263,12 @@ textarea:focus {
|
||||||
rgba(7, 10, 24, 0.34);
|
rgba(7, 10, 24, 0.34);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-planner-settings-actions {
|
||||||
|
display: flex;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.task-planner-settings label,
|
.task-planner-settings label,
|
||||||
.task-planner-weekly-override {
|
.task-planner-weekly-override {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -1318,7 +1340,18 @@ textarea:focus {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-planner-child-list > .task-planner-item::before {
|
.task-planner-hidden-parent-children {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-planner-hidden-parent-children > .task-planner-child-list {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-planner-child-list > .task-planner-item::before,
|
||||||
|
.task-planner-child-list > .task-planner-hidden-parent-children::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 25px;
|
top: 25px;
|
||||||
|
|
@ -1351,7 +1384,7 @@ textarea:focus {
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-planner-category-header > div {
|
.task-planner-category-title {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -1359,6 +1392,13 @@ textarea:focus {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-planner-category-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.task-planner-category-header h3 {
|
.task-planner-category-header h3 {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -1369,7 +1409,7 @@ textarea:focus {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-planner-category-header > span {
|
.task-planner-category-actions > span {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: 3px 9px;
|
padding: 3px 9px;
|
||||||
border: 1px solid rgba(246, 196, 83, 0.22);
|
border: 1px solid rgba(246, 196, 83, 0.22);
|
||||||
|
|
@ -1380,6 +1420,11 @@ textarea:focus {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-planner-category-section.is-complete .task-planner-category-header h3 {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
.task-planner-category-section.is-dragging {
|
.task-planner-category-section.is-dragging {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
opacity: 0.58;
|
opacity: 0.58;
|
||||||
|
|
@ -2426,6 +2471,7 @@ textarea:focus {
|
||||||
.tool-split-entry > button.danger,
|
.tool-split-entry > button.danger,
|
||||||
.counter-actions button.danger,
|
.counter-actions button.danger,
|
||||||
.link-item button.danger,
|
.link-item button.danger,
|
||||||
|
.task-planner-clear-completed-button.danger,
|
||||||
.task-planner-delete-button.danger {
|
.task-planner-delete-button.danger {
|
||||||
border-color: rgba(251, 113, 133, 0.2);
|
border-color: rgba(251, 113, 133, 0.2);
|
||||||
background: rgba(21, 26, 48, 0.88);
|
background: rgba(21, 26, 48, 0.88);
|
||||||
|
|
@ -2436,6 +2482,7 @@ textarea:focus {
|
||||||
.tool-split-entry > button.danger:hover,
|
.tool-split-entry > button.danger:hover,
|
||||||
.counter-actions button.danger:hover,
|
.counter-actions button.danger:hover,
|
||||||
.link-item button.danger:hover,
|
.link-item button.danger:hover,
|
||||||
|
.task-planner-clear-completed-button.danger:hover:not(:disabled),
|
||||||
.task-planner-delete-button.danger:hover {
|
.task-planner-delete-button.danger:hover {
|
||||||
border-color: rgba(251, 113, 133, 0.56);
|
border-color: rgba(251, 113, 133, 0.56);
|
||||||
background:
|
background:
|
||||||
|
|
@ -2451,6 +2498,7 @@ textarea:focus {
|
||||||
.tool-split-entry > button.danger:hover .ui-icon,
|
.tool-split-entry > button.danger:hover .ui-icon,
|
||||||
.counter-actions button.danger:hover .ui-icon,
|
.counter-actions button.danger:hover .ui-icon,
|
||||||
.link-item button.danger:hover .ui-icon,
|
.link-item button.danger:hover .ui-icon,
|
||||||
|
.task-planner-clear-completed-button.danger:hover .ui-icon,
|
||||||
.task-planner-delete-button.danger:hover .ui-icon {
|
.task-planner-delete-button.danger:hover .ui-icon {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
|
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue