fix: move categories in task planenr
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
3f72f152c7
commit
3dc5ed8733
1 changed files with 136 additions and 32 deletions
|
|
@ -76,6 +76,24 @@ function moveTask(tasks, fromTaskId, toTaskId, placement = "before") {
|
|||
return nextTasks;
|
||||
}
|
||||
|
||||
function moveTaskGroup(tasks, fromTaskIds, toTaskId, placement = "before") {
|
||||
const movingIds = new Set(fromTaskIds);
|
||||
if (!movingIds.size || movingIds.has(toTaskId)) return tasks;
|
||||
const targetIndex = tasks.findIndex((task) => task.id === toTaskId);
|
||||
if (targetIndex < 0) return tasks;
|
||||
const movingTasks = tasks.filter((task) => movingIds.has(task.id));
|
||||
const remainingTasks = tasks.filter((task) => !movingIds.has(task.id));
|
||||
const nextTargetIndex = remainingTasks.findIndex((task) => task.id === toTaskId);
|
||||
remainingTasks.splice(placement === "after" ? nextTargetIndex + 1 : nextTargetIndex, 0, ...movingTasks);
|
||||
return remainingTasks;
|
||||
}
|
||||
|
||||
function getBoundaryTaskId(tasks, taskIds, placement) {
|
||||
const ids = new Set(taskIds);
|
||||
const orderedTasks = tasks.filter((task) => ids.has(task.id));
|
||||
return placement === "after" ? orderedTasks.at(-1)?.id || "" : orderedTasks[0]?.id || "";
|
||||
}
|
||||
|
||||
function moveCategory(categoryOrder, fromCategory, toCategory, placement = "before") {
|
||||
const index = categoryOrder.indexOf(fromCategory);
|
||||
const targetIndex = categoryOrder.indexOf(toCategory);
|
||||
|
|
@ -164,6 +182,7 @@ function getTaskCategoryGroups(tasks, parentMap, parentId, sortMode, categoryOrd
|
|||
const siblings = getTasksForParent(tasks, parentMap, parentId, sortMode);
|
||||
if (parentId) {
|
||||
return {
|
||||
entries: siblings.map((task) => ({ type: "task", task })),
|
||||
uncategorized: siblings,
|
||||
categories: []
|
||||
};
|
||||
|
|
@ -177,28 +196,51 @@ function getTaskCategoryGroups(tasks, parentMap, parentId, sortMode, categoryOrd
|
|||
...categoryOrder.filter((category) => categories.includes(category)),
|
||||
...categories.filter((category) => !categoryOrder.includes(category))
|
||||
];
|
||||
return {
|
||||
uncategorized: siblings.filter((task) => !getTaskCategoryForParent(task, tasks, parentMap, parentId)),
|
||||
categories: orderedCategories.map((category) => ({
|
||||
const uncategorized = siblings.filter((task) => !getTaskCategoryForParent(task, tasks, parentMap, parentId));
|
||||
const categoryGroups = orderedCategories.map((category) => ({
|
||||
category,
|
||||
tasks: siblings.filter((task) => getTaskCategoryForParent(task, tasks, parentMap, parentId) === category)
|
||||
})).filter((group) => group.tasks.length)
|
||||
})).filter((group) => group.tasks.length);
|
||||
if (sortMode !== "manual") return { entries: [...uncategorized.map((task) => ({ type: "task", task })), ...categoryGroups.map((group) => ({ type: "category", group }))], uncategorized, categories: categoryGroups };
|
||||
|
||||
const renderedCategories = new Set();
|
||||
const entries = siblings.map((task) => {
|
||||
const category = getTaskCategoryForParent(task, tasks, parentMap, parentId);
|
||||
if (!category) return { type: "task", task };
|
||||
if (renderedCategories.has(category)) return null;
|
||||
renderedCategories.add(category);
|
||||
return { type: "category", group: categoryGroups.find((group) => group.category === category) };
|
||||
}).filter(Boolean);
|
||||
return {
|
||||
entries,
|
||||
uncategorized,
|
||||
categories: categoryGroups
|
||||
};
|
||||
}
|
||||
|
||||
function getCategorySectionId(parentId, category) {
|
||||
return `${parentId || ""}\n${category}`;
|
||||
return `category\n${parentId || ""}\n${category}`;
|
||||
}
|
||||
|
||||
function parseCategorySectionId(id) {
|
||||
const separatorIndex = id.indexOf("\n");
|
||||
if (separatorIndex < 0) return { parentId: "", category: id };
|
||||
const parts = String(id || "").split("\n");
|
||||
if (parts[0] !== "category") return { parentId: "", category: "" };
|
||||
return {
|
||||
parentId: id.slice(0, separatorIndex),
|
||||
category: id.slice(separatorIndex + 1)
|
||||
parentId: parts[1] || "",
|
||||
category: parts.slice(2).join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
function isCategorySectionId(id) {
|
||||
return String(id || "").startsWith("category\n");
|
||||
}
|
||||
|
||||
function getRootTaskIdsForCategory(tasks, parentMap, category) {
|
||||
return tasks
|
||||
.filter((task) => !parentMap.get(task.id) && getTaskCategory(task) === category)
|
||||
.map((task) => task.id);
|
||||
}
|
||||
|
||||
export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
|
||||
const textContent = context.moduleText?.taskPlanner || {};
|
||||
|
|
@ -216,31 +258,63 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
dropTarget,
|
||||
startDrag
|
||||
} = usePointerReorder({
|
||||
targetSelector: ".task-planner-item",
|
||||
getTargetId: (target) => target.dataset.taskId,
|
||||
targetSelector: ".task-planner-item, .task-planner-category-section",
|
||||
getTargetId: (target) => target.dataset.taskId || getCategorySectionId(target.dataset.parentId || "", target.dataset.category || ""),
|
||||
canDropOn: (target, draggingTaskId) => {
|
||||
const draggingTask = data.tasks.find((task) => task.id === draggingTaskId);
|
||||
if (!draggingTask || data.sortMode !== "manual") return false;
|
||||
if (target.classList.contains("task-planner-category-section")) {
|
||||
return !parentMap.get(draggingTaskId) && !getTaskCategory(draggingTask) && !target.dataset.parentId && Boolean(target.dataset.category);
|
||||
}
|
||||
return data.sortMode === "manual"
|
||||
&& target.dataset.parentId === (parentMap.get(draggingTaskId) || "")
|
||||
&& target.dataset.category === getTaskCategoryDataset(draggingTask, data.tasks, parentMap);
|
||||
&& (
|
||||
target.dataset.category === getTaskCategoryDataset(draggingTask, data.tasks, parentMap)
|
||||
|| (!parentMap.get(draggingTaskId) && !getTaskCategory(draggingTask) && Boolean(target.dataset.category))
|
||||
);
|
||||
},
|
||||
onMove: (fromTaskId, toTaskId, placement) => save({ ...data, tasks: moveTask(data.tasks, fromTaskId, toTaskId, placement) })
|
||||
onMove: (fromTaskId, toId, placement) => {
|
||||
if (isCategorySectionId(toId)) {
|
||||
const targetCategory = parseCategorySectionId(toId).category;
|
||||
const targetIds = getRootTaskIdsForCategory(data.tasks, parentMap, targetCategory);
|
||||
const boundaryTaskId = getBoundaryTaskId(data.tasks, targetIds, placement);
|
||||
if (boundaryTaskId) save({ ...data, tasks: moveTask(data.tasks, fromTaskId, boundaryTaskId, placement) });
|
||||
return;
|
||||
}
|
||||
save({ ...data, tasks: moveTask(data.tasks, fromTaskId, toId, placement) });
|
||||
}
|
||||
});
|
||||
const {
|
||||
draggingId: draggingCategoryId,
|
||||
dropTarget: categoryDropTarget,
|
||||
startDrag: startCategoryDrag
|
||||
} = usePointerReorder({
|
||||
targetSelector: ".task-planner-category-section",
|
||||
getTargetId: (target) => getCategorySectionId(target.dataset.parentId || "", target.dataset.category || ""),
|
||||
targetSelector: ".task-planner-category-section, .task-planner-item",
|
||||
getTargetId: (target) => target.dataset.taskId || getCategorySectionId(target.dataset.parentId || "", target.dataset.category || ""),
|
||||
canDropOn: (target, draggingIdValue) => {
|
||||
const draggingCategory = parseCategorySectionId(draggingIdValue);
|
||||
return target.dataset.parentId === draggingCategory.parentId && Boolean(target.dataset.category);
|
||||
if (!draggingCategory.category || target.dataset.parentId !== draggingCategory.parentId) return false;
|
||||
if (target.classList.contains("task-planner-category-section")) return Boolean(target.dataset.category);
|
||||
return !target.dataset.category;
|
||||
},
|
||||
onMove: (fromId, toId, placement) => {
|
||||
const fromCategory = parseCategorySectionId(fromId).category;
|
||||
const movingIds = getRootTaskIdsForCategory(data.tasks, parentMap, fromCategory);
|
||||
if (!movingIds.length) return;
|
||||
if (isCategorySectionId(toId)) {
|
||||
const toCategory = parseCategorySectionId(toId).category;
|
||||
save({ ...data, categoryOrder: moveCategory(data.categoryOrder, fromCategory, toCategory, placement) });
|
||||
const targetIds = getRootTaskIdsForCategory(data.tasks, parentMap, toCategory);
|
||||
const boundaryTaskId = getBoundaryTaskId(data.tasks, targetIds, placement);
|
||||
if (boundaryTaskId) {
|
||||
save({
|
||||
...data,
|
||||
tasks: moveTaskGroup(data.tasks, movingIds, boundaryTaskId, placement),
|
||||
categoryOrder: moveCategory(data.categoryOrder, fromCategory, toCategory, placement)
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
save({ ...data, tasks: moveTaskGroup(data.tasks, movingIds, toId, placement) });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -501,6 +575,7 @@ function TaskPlannerBranch({
|
|||
parentId={parentId}
|
||||
draggingId={draggingId}
|
||||
dropTarget={dropTarget}
|
||||
categoryDropTarget={categoryDropTarget}
|
||||
textContent={textContent}
|
||||
descriptionOpen={openDescriptions.has(task.id)}
|
||||
settingsOpen={openTaskSettings.has(task.id)}
|
||||
|
|
@ -548,9 +623,7 @@ function TaskPlannerBranch({
|
|||
);
|
||||
}
|
||||
|
||||
return [
|
||||
...groupedTasks.uncategorized.filter((task) => !visited.has(task.id)).map(renderTask),
|
||||
...groupedTasks.categories.map((group) => {
|
||||
function renderCategory(group) {
|
||||
const groupTasks = group.tasks.filter((task) => !visited.has(task.id));
|
||||
if (!groupTasks.length) return null;
|
||||
const groupCount = countTaskTree(groupTasks.map((task) => task.id), tasks, parentMap);
|
||||
|
|
@ -558,8 +631,8 @@ function TaskPlannerBranch({
|
|||
const sectionClassName = [
|
||||
"task-planner-category-section",
|
||||
draggingCategoryId === categoryId ? "is-dragging" : "",
|
||||
categoryDropTarget.id === categoryId ? "is-drop-target" : "",
|
||||
categoryDropTarget.id === categoryId && categoryDropTarget.placement === "after" ? "drop-after" : ""
|
||||
categoryDropTarget.id === categoryId || dropTarget.id === categoryId ? "is-drop-target" : "",
|
||||
(categoryDropTarget.id === categoryId && categoryDropTarget.placement === "after") || (dropTarget.id === categoryId && dropTarget.placement === "after") ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
return (
|
||||
<li className={sectionClassName} key={`${parentId || "root"}:${group.category}`} data-parent-id={parentId} data-category={group.category}>
|
||||
|
|
@ -583,8 +656,12 @@ function TaskPlannerBranch({
|
|||
</ul>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
return groupedTasks.entries.map((entry) => {
|
||||
if (entry.type === "task") return visited.has(entry.task.id) ? null : renderTask(entry.task);
|
||||
return renderCategory(entry.group);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function TaskPlannerItem({
|
||||
|
|
@ -595,6 +672,7 @@ function TaskPlannerItem({
|
|||
parentId,
|
||||
draggingId,
|
||||
dropTarget,
|
||||
categoryDropTarget,
|
||||
textContent,
|
||||
descriptionOpen,
|
||||
settingsOpen,
|
||||
|
|
@ -611,6 +689,8 @@ function TaskPlannerItem({
|
|||
onSetTaskCategory,
|
||||
children
|
||||
}) {
|
||||
const [titleDraft, setTitleDraft] = useState(task.title);
|
||||
const [descriptionDraft, setDescriptionDraft] = useState(task.description);
|
||||
const parentTask = parentId ? getTaskById(tasks, parentId) : null;
|
||||
const inheritedCategory = parentTask ? getEffectiveTaskCategory(parentTask, tasks, parentMap) : "";
|
||||
const categoryValue = parentTask ? inheritedCategory : task.category || "";
|
||||
|
|
@ -625,8 +705,8 @@ function TaskPlannerItem({
|
|||
task.checked ? "is-complete" : "",
|
||||
missingPrerequisites.length ? "has-missing-prerequisite" : "",
|
||||
draggingId === task.id ? "is-dragging" : "",
|
||||
dropTarget.id === task.id ? "is-drop-target" : "",
|
||||
dropTarget.id === task.id && dropTarget.placement === "after" ? "drop-after" : ""
|
||||
dropTarget.id === task.id || categoryDropTarget.id === task.id ? "is-drop-target" : "",
|
||||
(dropTarget.id === task.id && dropTarget.placement === "after") || (categoryDropTarget.id === task.id && categoryDropTarget.placement === "after") ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
function updateChecked(event) {
|
||||
|
|
@ -643,6 +723,26 @@ function TaskPlannerItem({
|
|||
setCategoryDraft(categoryValue);
|
||||
}, [categoryValue, task.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setTitleDraft(task.title);
|
||||
}, [task.id, task.title]);
|
||||
|
||||
useEffect(() => {
|
||||
setDescriptionDraft(task.description);
|
||||
}, [task.description, task.id]);
|
||||
|
||||
function commitTitleDraft() {
|
||||
const title = titleDraft.trim() || task.title;
|
||||
if (title !== task.title) onUpdateTask(task.id, (current) => ({ ...current, title }));
|
||||
else if (titleDraft !== title) setTitleDraft(title);
|
||||
}
|
||||
|
||||
function commitDescriptionDraft() {
|
||||
const description = descriptionDraft.trim();
|
||||
if (description !== task.description) onUpdateTask(task.id, (current) => ({ ...current, description }));
|
||||
else if (descriptionDraft !== description) setDescriptionDraft(description);
|
||||
}
|
||||
|
||||
function commitCategoryDraft() {
|
||||
if (categoryEditable && categoryDraft.trim() !== (task.category || "")) onSetTaskCategory(task.id, categoryDraft);
|
||||
}
|
||||
|
|
@ -668,9 +768,12 @@ function TaskPlannerItem({
|
|||
/>
|
||||
<input
|
||||
className="tool-split-entry-label task-planner-title-input"
|
||||
value={task.title}
|
||||
onChange={(event) => onUpdateTask(task.id, (current) => ({ ...current, title: event.target.value }))}
|
||||
onBlur={(event) => onUpdateTask(task.id, (current) => ({ ...current, title: event.target.value.trim() || current.title }))}
|
||||
value={titleDraft}
|
||||
onChange={(event) => setTitleDraft(event.target.value)}
|
||||
onBlur={commitTitleDraft}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") event.currentTarget.blur();
|
||||
}}
|
||||
aria-label={textContent.titleLabel || "Titre"}
|
||||
/>
|
||||
<select
|
||||
|
|
@ -738,8 +841,9 @@ function TaskPlannerItem({
|
|||
<span>{textContent.descriptionLabel || "Description"}</span>
|
||||
<textarea
|
||||
className="task-planner-description"
|
||||
value={task.description}
|
||||
onChange={(event) => onUpdateTask(task.id, (current) => ({ ...current, description: event.target.value }))}
|
||||
value={descriptionDraft}
|
||||
onChange={(event) => setDescriptionDraft(event.target.value)}
|
||||
onBlur={commitDescriptionDraft}
|
||||
placeholder={textContent.descriptionPlaceholder || "Description"}
|
||||
aria-label={`${textContent.descriptionLabel || "Description"} ${task.title}`}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue