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