allow drag in and out of categorie
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s

This commit is contained in:
Shinuwa 2026-07-29 08:54:18 +02:00
parent 3726b2330b
commit ee579af2ec
5 changed files with 91 additions and 16 deletions

View file

@ -319,7 +319,7 @@ Notes :
- une tâche ne conserve qu'un seul parent.
- `prerequisite: true` marque aussi ce parent comme pré requis.
- Les pré requis sont non bloquants : ils affichent un avertissement si le parent requis n'est pas effectué.
- En tri manuel, une tâche catégorisée ne peut être réordonnée qu'avec des tâches du même parent et de la même catégorie.
- Le drag & drop conserve l'ordre manuel, et peut aussi ajouter une tâche racine à une catégorie existante ou la sortir vers les tâches sans catégorie.
Stockage compact :

View file

@ -218,6 +218,7 @@ export function validateSiteContent(site) {
"toolboxes.modules.taskPlanner.categoryLabel",
"toolboxes.modules.taskPlanner.categoryPlaceholder",
"toolboxes.modules.taskPlanner.categoryReorderTitle",
"toolboxes.modules.taskPlanner.uncategorizedDropTitle",
"toolboxes.modules.taskPlanner.showCategoryTitle",
"toolboxes.modules.taskPlanner.hideCategoryTitle",
"toolboxes.modules.taskPlanner.taskWeeklyResetDayLabel",

View file

@ -352,6 +352,7 @@
"categoryLabel": "Catégorie",
"categoryPlaceholder": "Nom de catégorie",
"categoryReorderTitle": "Déplacer la catégorie",
"uncategorizedDropTitle": "Sans catégorie",
"showCategoryTitle": "Afficher cette catégorie",
"hideCategoryTitle": "Réduire cette catégorie",
"taskWeeklyResetDayLabel": "Jour hebdo",

View file

@ -5,6 +5,7 @@ import { InlineNotice } from "../../../components/AppOverlays.jsx";
import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
const TASK_TYPES = ["daily", "weekly", "unique"];
const UNCATEGORIZED_DROP_ID = "task-planner:uncategorized";
const WEEK_DAYS = [
{ value: 1, label: "Lundi" },
{ value: 2, label: "Mardi" },
@ -252,6 +253,10 @@ function isCategorySectionId(id) {
return String(id || "").startsWith("category\n");
}
function isUncategorizedDropId(id) {
return id === UNCATEGORIZED_DROP_ID;
}
function getRootTaskIdsForCategory(tasks, parentMap, category) {
return tasks
.filter((task) => !parentMap.get(task.id) && getTaskCategory(task) === category)
@ -275,29 +280,58 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
dropTarget,
startDrag
} = usePointerReorder({
targetSelector: ".task-planner-item, .task-planner-category-section",
getTargetId: (target) => target.dataset.taskId || getCategorySectionId(target.dataset.parentId || "", target.dataset.category || ""),
targetSelector: ".task-planner-item, .task-planner-category-section, .task-planner-uncategorized-drop-zone",
getTargetId: (target) => target.dataset.dropId || target.dataset.taskId || getCategorySectionId(target.dataset.parentId || "", target.dataset.category || ""),
canDropOn: (target, draggingTaskId) => {
const draggingTask = data.tasks.find((task) => task.id === draggingTaskId);
if (!draggingTask) return false;
if (target.classList.contains("task-planner-category-section")) {
return !parentMap.get(draggingTaskId) && !getTaskCategory(draggingTask) && !target.dataset.parentId && Boolean(target.dataset.category);
const draggingParentId = parentMap.get(draggingTaskId) || "";
const draggingCategory = getTaskCategoryDataset(draggingTask, data.tasks, parentMap);
if (target.classList.contains("task-planner-uncategorized-drop-zone")) {
return !draggingParentId && Boolean(draggingCategory);
}
return target.dataset.parentId === (parentMap.get(draggingTaskId) || "")
&& (
target.dataset.category === getTaskCategoryDataset(draggingTask, data.tasks, parentMap)
|| (!parentMap.get(draggingTaskId) && !getTaskCategory(draggingTask) && Boolean(target.dataset.category))
);
if (target.classList.contains("task-planner-category-section")) {
return !draggingParentId && !target.dataset.parentId && Boolean(target.dataset.category) && target.dataset.category !== draggingCategory;
}
if (target.dataset.parentId !== draggingParentId) return false;
if (target.dataset.category === draggingCategory) return true;
return !draggingParentId;
},
onMove: (fromTaskId, toId, placement) => {
if (isCategorySectionId(toId)) {
const targetCategory = parseCategorySectionId(toId).category;
const targetIds = getRootTaskIdsForCategory(data.tasks, parentMap, targetCategory);
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) {
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);
if (boundaryTaskId) save({ ...data, tasks: moveTask(data.tasks, fromTaskId, boundaryTaskId, placement) });
save({ ...data, tasks: boundaryTaskId ? moveTask(nextTasks, fromTaskId, boundaryTaskId, placement) : nextTasks });
}
if (isUncategorizedDropId(toId)) {
moveToCategory("");
return;
}
save({ ...data, tasks: moveTask(data.tasks, fromTaskId, toId, placement) });
if (isCategorySectionId(toId)) {
const targetCategory = parseCategorySectionId(toId).category;
moveToCategory(targetCategory);
return;
}
const targetTask = data.tasks.find((task) => task.id === toId);
const targetCategory = targetTask ? getTaskCategoryDataset(targetTask, data.tasks, parentMap) : "";
const sourceCategory = getTaskCategoryDataset(fromTask, data.tasks, parentMap);
const shouldChangeRootCategory = !parentMap.get(fromTaskId) && targetTask && !parentMap.get(toId) && targetCategory !== sourceCategory;
const tasks = shouldChangeRootCategory ? setRootTaskCategory(data.tasks, targetCategory) : data.tasks;
save({ ...data, tasks: moveTask(tasks, fromTaskId, toId, placement) });
}
});
const {
@ -608,6 +642,8 @@ function TaskPlannerBranch({
const children = [...groupedTasks.uncategorized, ...groupedTasks.categories.flatMap((group) => group.tasks)].filter((task) => !visited.has(task.id));
if (!children.length) return null;
const nextVisited = new Set([...visited, ...children.map((task) => task.id)]);
const draggingTask = draggingId ? getTaskById(tasks, draggingId) : null;
const showUncategorizedDropZone = !parentId && draggingTask && !parentMap.get(draggingTask.id) && Boolean(getTaskCategoryDataset(draggingTask, tasks, parentMap));
function hasVisibleDescendants(taskId, seen = new Set()) {
if (seen.has(taskId)) return false;
@ -739,10 +775,22 @@ function TaskPlannerBranch({
);
}
return groupedTasks.entries.map((entry) => {
const entries = groupedTasks.entries.map((entry) => {
if (entry.type === "task") return visited.has(entry.task.id) ? null : renderTask(entry.task);
return renderCategory(entry.group);
}).filter(Boolean);
if (!showUncategorizedDropZone) return entries;
const dropZoneClassName = [
"task-planner-uncategorized-drop-zone",
dropTarget.id === UNCATEGORIZED_DROP_ID ? "is-drop-target" : "",
dropTarget.id === UNCATEGORIZED_DROP_ID && dropTarget.placement === "after" ? "drop-after" : ""
].filter(Boolean).join(" ");
return [
<li className={dropZoneClassName} key={UNCATEGORIZED_DROP_ID} data-drop-id={UNCATEGORIZED_DROP_ID}>
{textContent.uncategorizedDropTitle || "Sans catégorie"}
</li>,
...entries
];
}
function TaskPlannerItem({

View file

@ -1369,6 +1369,31 @@ textarea:focus {
text-align: center;
}
.task-planner-uncategorized-drop-zone {
padding: 9px 12px;
border: 1px dashed rgba(165, 180, 252, 0.22);
border-radius: var(--radius-md);
background: rgba(15, 23, 42, 0.26);
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 900;
text-align: center;
text-transform: uppercase;
}
.task-planner-uncategorized-drop-zone.is-drop-target {
border-color: rgba(246, 196, 83, 0.72);
background:
linear-gradient(135deg, rgba(246, 196, 83, 0.08), transparent 56%),
rgba(15, 23, 42, 0.34);
color: var(--color-accent-gold);
box-shadow: inset 0 3px 0 rgba(246, 196, 83, 0.8);
}
.task-planner-uncategorized-drop-zone.is-drop-target.drop-after {
box-shadow: inset 0 -3px 0 rgba(246, 196, 83, 0.8);
}
.task-planner-category-section {
display: grid;
gap: 8px;