diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs
index f367b4c..95ca4cf 100644
--- a/tests/helpers/data-validation.mjs
+++ b/tests/helpers/data-validation.mjs
@@ -132,6 +132,12 @@ export function validateSiteContent(site) {
"toolboxes.modules.checklist.decrementLabel",
"toolboxes.modules.checklist.incrementLabel",
"toolboxes.modules.checklist.currentQuantityLabel",
+ "toolboxes.modules.checklist.reorderTitle",
+ "toolboxes.modules.checklist.reorderSectionTitle",
+ "toolboxes.modules.checklist.sectionEditTitle",
+ "toolboxes.modules.checklist.itemEditLabel",
+ "toolboxes.modules.checklist.categoryEditLabel",
+ "toolboxes.modules.checklist.editTitle",
"toolboxes.modules.checklist.deleteTitle",
"toolboxes.modules.images.addImages",
"toolboxes.modules.images.pastePlaceholder",
diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs
index 3ba083e..332f110 100644
--- a/tests/static-toolboxes.test.mjs
+++ b/tests/static-toolboxes.test.mjs
@@ -15,6 +15,7 @@ test("toolbox storage, cards and pages are wired", async () => {
const reorderHook = await readFile("website/src/hooks/usePointerReorder.js", "utf8");
const groupedReorderHook = await readFile("website/src/hooks/useGroupedReorder.js", "utf8");
const inlineEditHook = await readFile("website/src/hooks/useInlineEdit.js", "utf8");
+ const draftFormHook = await readFile("website/src/hooks/useDraftForm.js", "utf8");
assert.match(`${indexedToolboxesHook}\n${toolboxPages}`, /indexedDbStorage\.js/);
assert.match(source, /features\/toolboxes\/storage\/useIndexedToolboxes\.js/);
@@ -90,6 +91,10 @@ test("toolbox storage, cards and pages are wired", async () => {
assert.match(inlineEditHook, /blurOnEscape/);
assert.match(inlineEditHook, /commitOnEnter/);
assert.match(inlineEditHook, /getContentEditableProps/);
+ assert.match(draftFormHook, /export function useDraftForm/);
+ assert.match(draftFormHook, /getFieldProps/);
+ assert.match(draftFormHook, /handleSubmit/);
+ assert.match(draftFormHook, /preventDefault/);
});
test("toolbox module registry and modules expose expected behavior", async () => {
@@ -156,6 +161,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(combosModule, /useInlineEdit/);
assert.match(combosModule, /useGroupedReorder/);
assert.match(combosModule, /reorderFeatures/);
+ assert.match(combosModule, /getGroupBoundaryProps/);
+ assert.match(combosModule, /combos-category-boundary-drop-zone/);
assert.doesNotMatch(combosModule, /canMoveItem:/);
assert.doesNotMatch(combosModule, /canMoveGroup:/);
assert.match(calculatorModule, /export function CalculatorModule/);
@@ -208,6 +215,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(timerModule, /stopwatch/);
assert.match(timerModule, /countdown/);
assert.match(timerModule, /useInlineEdit/);
+ assert.match(timerModule, /useDraftForm/);
assert.match(timerModule, /normalizeTimerData/);
assert.match(taskPlannerModule, /export function TaskPlannerModule/);
assert.match(taskPlannerModule, /normalizeTaskPlannerData/);
@@ -219,6 +227,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(taskPlannerModule, /task-planner-warning/);
assert.match(taskPlannerModule, /useGroupedReorder/);
assert.match(taskPlannerModule, /useInlineEdit/);
+ assert.match(taskPlannerModule, /useDraftForm/);
assert.match(taskPlannerModule, /commitOnEnter: false/);
assert.match(taskPlannerModule, /reorderFeatures/);
assert.doesNotMatch(taskPlannerModule, /canMoveItem:/);
@@ -237,7 +246,15 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(checklistModule, /Number\.parseInt/);
assert.match(checklistModule, /checklist-qty-current/);
assert.match(checklistModule, /checklist-delete-button danger/);
- assert.doesNotMatch(checklistModule, /useGroupedReorder/);
+ assert.match(checklistModule, /useGroupedReorder/);
+ assert.match(checklistModule, /getItemProps/);
+ assert.match(checklistModule, /getGroupProps/);
+ assert.match(checklistModule, /getGroupBoundaryProps/);
+ assert.match(checklistModule, /checklist-category-boundary-drop-zone/);
+ assert.match(checklistModule, /itemReorder\.startDrag/);
+ assert.match(checklistModule, /groupReorder\.startDrag/);
+ assert.match(checklistModule, /useInlineEdit/);
+ assert.match(checklistModule, /moveChecklistItemToCategory/);
assert.match(textImportModal, /createPortal/);
assert.match(textImportModal, /lockBodyScroll/);
assert.match(imagesModule, /export function ImagesModule/);
@@ -264,11 +281,15 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(linksModule, /parseColonImportLines/);
assert.match(linksModule, /context\.normalizeUrl/);
assert.match(linksModule, /useGroupedReorder/);
+ assert.match(linksModule, /useDraftForm/);
+ assert.match(linksModule, /linkDraft\.getFieldProps/);
assert.match(linksModule, /link-drag-handle/);
assert.match(countersModule, /export function CountersModule/);
assert.match(countersModule, /editing &&/);
assert.match(countersModule, /counter-actions/);
assert.match(countersModule, /useGroupedReorder/);
+ assert.match(countersModule, /useDraftForm/);
+ assert.match(countersModule, /counterDraft\.getFieldProps/);
assert.match(countersModule, /orientation: "horizontal"/);
assert.match(countersModule, /counter-drag-handle/);
assert.match(timerModule, /useGroupedReorder/);
diff --git a/website/public/data/site.json b/website/public/data/site.json
index 4ac81ce..e386587 100644
--- a/website/public/data/site.json
+++ b/website/public/data/site.json
@@ -252,6 +252,12 @@
"decrementLabel": "Retirer une quantité",
"incrementLabel": "Ajouter une quantité",
"currentQuantityLabel": "Quantité actuelle",
+ "reorderTitle": "Déplacer",
+ "reorderSectionTitle": "Déplacer la catégorie",
+ "sectionEditTitle": "Renommer la catégorie",
+ "itemEditLabel": "Libellé",
+ "categoryEditLabel": "Catégorie",
+ "editTitle": "Modifier",
"deleteTitle": "Supprimer"
},
"images": {
diff --git a/website/src/features/toolboxes/modules/ChecklistModule.jsx b/website/src/features/toolboxes/modules/ChecklistModule.jsx
index 8fd9514..2928013 100644
--- a/website/src/features/toolboxes/modules/ChecklistModule.jsx
+++ b/website/src/features/toolboxes/modules/ChecklistModule.jsx
@@ -1,6 +1,8 @@
// Rôle : fournit l'outil checklist avec quantités, catégories et imports texte.
import { useCallback, useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
+import { useGroupedReorder, moveItem } from "../../../hooks/useGroupedReorder.js";
+import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
import { TextImportModal } from "./TextImportModal.jsx";
import { parseColonImportLines } from "./textImport.js";
@@ -15,6 +17,49 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
const closeImportModal = useCallback(() => setImportOpen(false), []);
const namedSections = data.sections.filter((section) => section.title);
const completedSectionsMode = data.hideCompletedSectionsFully ? "hidden" : data.hideCompletedSections ? "reduced" : "visible";
+ const reorderItems = getChecklistReorderItems(data.sections);
+ const {
+ itemReorder,
+ groupReorder,
+ getItemProps,
+ getGroupProps,
+ getGroupBoundaryProps,
+ isItemDragging,
+ isItemDropTarget,
+ isGroupDragging,
+ isGroupDropTarget,
+ isBoundaryDropTarget,
+ getDropPlacement,
+ shouldShowGroupBoundaries
+ } = useGroupedReorder({
+ namespace: `checklist-${moduleId}`,
+ items: reorderItems,
+ getItemId: (item) => item.id,
+ getItemGroup: (item) => item.sectionId,
+ getEffectiveGroup: (item) => item.sectionId,
+ reorderFeatures: {
+ item: { groupChange: true, boundaryDrop: true },
+ group: { reorder: true, boundaryDrop: true }
+ },
+ onItemMove: (operation) => save(moveChecklistItem(data.sections, operation, context)),
+ onGroupMove: (operation) => save(moveItem(data.sections, operation.sourceId, operation.targetId, operation.placement))
+ });
+ const reorder = {
+ itemReorder,
+ groupReorder,
+ getItemProps,
+ getGroupProps,
+ getGroupBoundaryProps,
+ isItemDragging,
+ isItemDropTarget,
+ isGroupDragging,
+ isGroupDropTarget,
+ isBoundaryDropTarget,
+ getDropPlacement,
+ shouldShowGroupBoundaries
+ };
+ const showBoundaryDropZones = reorder.shouldShowGroupBoundaries();
+ const visibleSections = data.sections.filter((section) => !(section.title && data.hideCompletedSectionsFully && isSectionComplete(section, context)));
function save(sections, settings = {}) {
context.setModuleData(toolboxId, moduleId, { ...data, ...settings, sections });
@@ -57,6 +102,26 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
});
}
+ function updateSection(sectionId, updater) {
+ save(data.sections.map((section) => section.id === sectionId ? updater(section) : section).filter(keepChecklistSection));
+ }
+
+ function renameSection(sectionId, title) {
+ save(renameChecklistSection(data.sections, sectionId, title));
+ }
+
+ function updateItem(itemId, updater) {
+ save(updateChecklistItem(data.sections, itemId, updater));
+ }
+
+ function moveItemToCategory(itemId, title) {
+ save(moveChecklistItemToCategory(data.sections, itemId, title, context));
+ }
+
+ function deleteItem(itemId) {
+ save(data.sections.map((section) => ({ ...section, items: section.items.filter((item) => item.id !== itemId) })).filter(keepChecklistSection));
+ }
+
return (
<>
{editing && (
@@ -109,9 +174,38 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
)}
- {data.sections.map((section) => (
-
- ))}
+ {visibleSections.map((section, index) => {
+ const isLastSection = index === visibleSections.length - 1;
+ return [
+ showBoundaryDropZones ? (
+
+ ) : null,
+ ,
+ showBoundaryDropZones && isLastSection ? (
+
+ ) : null
+ ];
+ })}
>
);
@@ -151,30 +245,142 @@ function getSectionHideWhenComplete(section, globalHide) {
return typeof section.hideWhenComplete === "boolean" ? section.hideWhenComplete : globalHide;
}
-function CategoryTitle({ title }) {
- return String(title || "").split(/([♂♀])/g).map((part, index) => (
- part === "♂" || part === "♀"
- ? {part}
- : part
- ));
+function keepChecklistSection(section) {
+ return section.items.length || section.title;
}
-function ChecklistSection({ section, context, sections, data, save }) {
+function getChecklistReorderItems(sections) {
+ return sections.flatMap((section) => section.items.map((item) => ({ ...item, sectionId: section.id })));
+}
+
+function findChecklistSectionByItemId(sections, itemId) {
+ return sections.find((section) => section.items.some((item) => item.id === itemId));
+}
+
+function renameChecklistSection(sections, sectionId, title) {
+ const cleanTitle = title.trim();
+ const section = sections.find((entry) => entry.id === sectionId);
+ if (!section) return sections;
+ const existingSection = sections.find((entry) => entry.id !== sectionId && entry.title.toLowerCase() === cleanTitle.toLowerCase());
+ if (!existingSection) {
+ return sections.map((entry) => entry.id === sectionId ? { ...entry, title: cleanTitle } : entry).filter(keepChecklistSection);
+ }
+ return sections.map((entry) => {
+ if (entry.id === existingSection.id) return { ...entry, items: [...entry.items, ...section.items] };
+ return entry;
+ }).filter((entry) => entry.id !== sectionId).filter(keepChecklistSection);
+}
+
+function updateChecklistItem(sections, itemId, updater) {
+ return sections.map((section) => ({
+ ...section,
+ items: section.items.map((item) => item.id === itemId ? updater(item) : item).filter((item) => item.label)
+ })).filter(keepChecklistSection);
+}
+
+function moveChecklistItemToCategory(sections, itemId, title, context) {
+ const cleanTitle = title.trim();
+ const sourceSection = findChecklistSectionByItemId(sections, itemId);
+ const item = sourceSection?.items.find((entry) => entry.id === itemId);
+ if (!sourceSection || !item || sourceSection.title.toLowerCase() === cleanTitle.toLowerCase()) return sections;
+ const targetSection = sections.find((section) => section.title.toLowerCase() === cleanTitle.toLowerCase());
+ const sectionsWithoutItem = sections.map((section) => ({
+ ...section,
+ items: section.items.filter((entry) => entry.id !== itemId)
+ })).filter(keepChecklistSection);
+ if (targetSection) {
+ return sectionsWithoutItem.map((section) => section.id === targetSection.id ? { ...section, items: [...section.items, item] } : section);
+ }
+ return [...sectionsWithoutItem, { id: context.uid("section"), title: cleanTitle, items: [item] }];
+}
+
+function moveChecklistItem(sections, operation, context) {
+ const sourceSection = findChecklistSectionByItemId(sections, operation.sourceId);
+ const sourceItem = sourceSection?.items.find((item) => item.id === operation.sourceId);
+ if (!sourceSection || !sourceItem) return sections;
+ if (operation.targetType === "boundary") return moveChecklistItemToUngroupedBoundary(sections, sourceSection, sourceItem, operation, context);
+ const targetSection = operation.targetType === "item"
+ ? findChecklistSectionByItemId(sections, operation.targetId)
+ : sections.find((section) => section.id === operation.targetGroup);
+ if (!targetSection) return sections;
+ const sectionsWithoutItem = sections.map((section) => ({
+ ...section,
+ items: section.items.filter((item) => item.id !== operation.sourceId)
+ })).filter(keepChecklistSection);
+ return sectionsWithoutItem.map((section) => {
+ if (section.id !== targetSection.id) return section;
+ if (operation.targetType === "item") {
+ const targetIndex = section.items.findIndex((item) => item.id === operation.targetId);
+ if (targetIndex < 0) return { ...section, items: [...section.items, sourceItem] };
+ const nextItems = [...section.items];
+ nextItems.splice(operation.placement === "after" ? targetIndex + 1 : targetIndex, 0, sourceItem);
+ return { ...section, items: nextItems };
+ }
+ return {
+ ...section,
+ items: operation.placement === "after" ? [...section.items, sourceItem] : [sourceItem, ...section.items]
+ };
+ });
+}
+
+function moveChecklistItemToUngroupedBoundary(sections, sourceSection, sourceItem, operation, context) {
+ const targetIndex = sections.findIndex((section) => section.id === operation.targetGroup);
+ if (targetIndex < 0) return sections;
+ const preferredUngroupedSection = sourceSection.title ? sections.find((section) => !section.title && section.id !== sourceSection.id) : sourceSection;
+ const ungroupedSectionId = preferredUngroupedSection?.id || context.uid("section");
+ const baseSections = sections
+ .map((section) => ({
+ ...section,
+ items: section.items.filter((item) => item.id !== sourceItem.id)
+ }))
+ .filter((section) => section.id !== ungroupedSectionId)
+ .filter(keepChecklistSection);
+ const targetSection = sections[targetIndex];
+ const nextTargetIndex = baseSections.findIndex((section) => section.id === targetSection.id);
+ const insertIndex = nextTargetIndex < 0
+ ? Math.min(targetIndex, baseSections.length)
+ : nextTargetIndex + (operation.placement === "after" ? 1 : 0);
+ const ungroupedSection = {
+ id: ungroupedSectionId,
+ title: "",
+ items: [...(preferredUngroupedSection?.items || []).filter((item) => item.id !== sourceItem.id), sourceItem]
+ };
+ const nextSections = [...baseSections];
+ nextSections.splice(insertIndex, 0, ungroupedSection);
+ return nextSections;
+}
+
+function ChecklistSection({ section, context, data, reorder, onRenameSection, onUpdateSection, onSaveSections, onUpdateItem, onMoveItemToCategory, onDeleteItem }) {
const showTitle = Boolean(section.title);
const sectionComplete = showTitle && isSectionComplete(section, context);
const hideWhenComplete = getSectionHideWhenComplete(section, data.hideCompletedSections);
const isAutoCollapsed = sectionComplete && hideWhenComplete;
const isCollapsed = Boolean(section.collapsed) || isAutoCollapsed;
+ const textContent = context.moduleText?.checklist || {};
+ const className = [
+ "checklist-section",
+ showTitle ? "is-grouped" : "",
+ sectionComplete ? "is-complete" : "",
+ isCollapsed ? "is-collapsed" : "",
+ reorder.isGroupDragging(section.id) ? "is-dragging" : "",
+ reorder.isGroupDropTarget(section.id) ? "is-drop-target" : "",
+ reorder.getDropPlacement("group", section.id) === "after" ? "drop-after" : ""
+ ].filter(Boolean).join(" ");
+ const titleEdit = useInlineEdit({
+ value: section.title,
+ onCommit: (title) => onRenameSection(section.id, title)
+ });
+ const [editingItemId, setEditingItemId] = useState("");
if (sectionComplete && data.hideCompletedSectionsFully) return null;
function saveItems(items) {
- save(sections.map((entry) => entry.id === section.id ? { ...entry, items } : entry).filter((entry) => entry.items.length || entry.title));
+ onUpdateSection(section.id, (entry) => ({ ...entry, items }));
}
function toggleSectionCollapsed() {
const nextCollapsed = !isCollapsed;
- const nextSections = sections.map((entry) => {
+ const nextSections = data.sections.map((entry) => {
if (entry.id === section.id) {
const nextSection = { ...entry };
if (nextCollapsed) {
@@ -195,23 +401,40 @@ function ChecklistSection({ section, context, sections, data, save }) {
});
const nextCompletedSections = nextSections.filter((entry) => entry.title && isSectionComplete(entry, context));
const allHidden = nextCompletedSections.length > 0 && nextCompletedSections.every((entry) => getSectionHideWhenComplete(entry, false));
- save(nextSections, { hideCompletedSections: allHidden });
+ onSaveSections(nextSections, { hideCompletedSections: allHidden });
}
return (
-
+
{showTitle && (
-
-
+
+
+
+
+
{section.items.filter((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget).length} / {section.items.length}
@@ -221,7 +444,20 @@ function ChecklistSection({ section, context, sections, data, save }) {
{!isCollapsed && (
{section.items.map((item) => (
-
+ setEditingItemId(editingItemId === item.id ? "" : item.id)}
+ />
))}
)}
@@ -229,16 +465,51 @@ function ChecklistSection({ section, context, sections, data, save }) {
);
}
-function ChecklistItem({ item, context, items, save }) {
+function ChecklistItem({ item, section, context, reorder, items, save, onUpdateItem, onMoveItemToCategory, onDeleteItem, editing, onToggleEditing }) {
const done = context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget;
+ const textContent = context.moduleText?.checklist || {};
+ const className = [
+ "checklist-item",
+ done ? "is-complete" : "",
+ reorder.isItemDragging(item.id) ? "is-dragging" : "",
+ reorder.isItemDropTarget(item.id) ? "is-drop-target" : "",
+ reorder.getDropPlacement("item", item.id) === "after" ? "drop-after" : ""
+ ].filter(Boolean).join(" ");
+ const labelEdit = useInlineEdit({
+ value: item.label,
+ transform: (draft) => String(draft || "").trim() || item.label,
+ onCommit: (label) => onUpdateItem(item.id, (entry) => ({ ...entry, label })),
+ autoFocus: editing,
+ focusKey: item.id
+ });
+ const categoryEdit = useInlineEdit({
+ value: section.title,
+ onCommit: (title) => onMoveItemToCategory(item.id, title)
+ });
function updateItem(updater) {
save(items.map((entry) => entry.id === item.id ? updater(entry) : entry));
}
+ function toggleChecked() {
+ updateItem((entry) => ({
+ ...entry,
+ qtyCurrent: done ? 0 : entry.qtyTarget
+ }));
+ }
+
return (
-
-
-
diff --git a/website/src/features/toolboxes/modules/CombosModule.jsx b/website/src/features/toolboxes/modules/CombosModule.jsx
index 443be8c..b8b3143 100644
--- a/website/src/features/toolboxes/modules/CombosModule.jsx
+++ b/website/src/features/toolboxes/modules/CombosModule.jsx
@@ -553,9 +553,10 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
);
}
- function renderCategory(group) {
+ function renderCategory(group, index, groupCount) {
const category = group.category;
const isCollapsed = data.collapsedCategories.includes(category);
+ const isLastCategory = index === groupCount - 1;
const categoryClassName = [
"combos-category checklist-section is-grouped",
isCollapsed ? "is-collapsed" : "",
@@ -614,10 +615,19 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
)}
,
- afterDropZone
+ showBoundaryDropZones && isLastCategory ? afterDropZone : null
];
}
+ const categoryCount = entries.filter((entry) => entry.type === "group").length;
+ let renderedCategoryIndex = -1;
+
+ function renderEntry(entry) {
+ if (entry.type !== "group") return renderCombo(entry.item);
+ renderedCategoryIndex += 1;
+ return renderCategory(entry.group, renderedCategoryIndex, categoryCount);
+ }
+
return (
{(editing || editingComboId) && (
@@ -738,7 +748,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
)}
- {entries.map((entry) => entry.type === "group" ? renderCategory(entry.group) : renderCombo(entry.item))}
+ {entries.map(renderEntry)}
);
diff --git a/website/src/features/toolboxes/modules/CountersModule.jsx b/website/src/features/toolboxes/modules/CountersModule.jsx
index 3d3a0e9..32517ff 100644
--- a/website/src/features/toolboxes/modules/CountersModule.jsx
+++ b/website/src/features/toolboxes/modules/CountersModule.jsx
@@ -1,12 +1,12 @@
// Rôle : fournit l'outil compteurs personnalisables.
-import { useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
+import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
export function CountersModule({ toolboxId, moduleId, context, editing }) {
const data = context.normalizeCountersData(context.getModuleData(toolboxId, moduleId, { counters: [] }));
const textContent = context.moduleText?.counters || {};
- const [label, setLabel] = useState("");
+ const counterDraft = useDraftForm({ label: "" });
const {
itemReorder,
getItemProps,
@@ -28,12 +28,13 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
}
function addCounter(event) {
- event.preventDefault();
- const cleanLabel = label.trim();
- if (!cleanLabel) return;
+ counterDraft.handleSubmit(event, (draft, { reset }) => {
+ const cleanLabel = draft.label.trim();
+ if (!cleanLabel) return;
- save([...data.counters, { id: context.uid("counter"), label: cleanLabel, value: 0 }]);
- setLabel("");
+ save([...data.counters, { id: context.uid("counter"), label: cleanLabel, value: 0 }]);
+ reset();
+ });
}
function updateCounter(counterId, updater) {
@@ -44,7 +45,7 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
<>
{editing && (
)}
diff --git a/website/src/features/toolboxes/modules/LinksModule.jsx b/website/src/features/toolboxes/modules/LinksModule.jsx
index 95d9d1f..fb47bcc 100644
--- a/website/src/features/toolboxes/modules/LinksModule.jsx
+++ b/website/src/features/toolboxes/modules/LinksModule.jsx
@@ -1,6 +1,7 @@
// Rôle : fournit l'outil liens avec ajout manuel et import texte.
import { useCallback, useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
+import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { TextImportModal } from "./TextImportModal.jsx";
import { parseColonImportLines } from "./textImport.js";
@@ -8,8 +9,7 @@ import { parseColonImportLines } from "./textImport.js";
export function LinksModule({ toolboxId, moduleId, context, editing }) {
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
const textContent = context.moduleText?.links || {};
- const [title, setTitle] = useState("");
- const [url, setUrl] = useState("");
+ const linkDraft = useDraftForm({ title: "", url: "" });
const [textImport, setTextImport] = useState("");
const [importOpen, setImportOpen] = useState(false);
const [copiedId, setCopiedId] = useState("");
@@ -34,20 +34,20 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
}
function addLink(event) {
- event.preventDefault();
- const cleanUrl = context.normalizeUrl(url);
- if (!cleanUrl) return;
+ linkDraft.handleSubmit(event, (draft, { reset }) => {
+ const cleanUrl = context.normalizeUrl(draft.url);
+ if (!cleanUrl) return;
- save([
- ...data.links,
- {
- id: context.uid("link"),
- title: title.trim(),
- url: cleanUrl
- }
- ]);
- setTitle("");
- setUrl("");
+ save([
+ ...data.links,
+ {
+ id: context.uid("link"),
+ title: draft.title.trim(),
+ url: cleanUrl
+ }
+ ]);
+ reset();
+ });
}
function importLinks(event) {
@@ -79,8 +79,8 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
{editing && (
diff --git a/website/src/features/toolboxes/modules/TaskPlannerModule.jsx b/website/src/features/toolboxes/modules/TaskPlannerModule.jsx
index 2ecac72..67381a2 100644
--- a/website/src/features/toolboxes/modules/TaskPlannerModule.jsx
+++ b/website/src/features/toolboxes/modules/TaskPlannerModule.jsx
@@ -2,6 +2,7 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
import { InlineNotice } from "../../../components/AppOverlays.jsx";
+import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
@@ -222,8 +223,7 @@ function getRootTaskIdsForCategory(tasks, parentMap, category) {
export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
const textContent = context.moduleText?.taskPlanner || {};
- const [title, setTitle] = useState("");
- const [type, setType] = useState("daily");
+ const taskDraft = useDraftForm({ title: "", type: "daily" });
const [nowMs, setNowMs] = useState(() => Date.now());
const [settingsOpen, setSettingsOpen] = useState(false);
const [openDescriptions, setOpenDescriptions] = useState(() => new Set());
@@ -358,25 +358,25 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
});
function addTask(event) {
- event.preventDefault();
- const cleanTitle = title.trim();
- if (!cleanTitle) return;
- save({
- ...data,
- tasks: [
- ...data.tasks,
- {
- id: context.uid("task"),
- title: cleanTitle,
- description: "",
- type,
- checked: false,
- checkedAt: 0
- }
- ]
+ taskDraft.handleSubmit(event, (draft, { reset }) => {
+ const cleanTitle = draft.title.trim();
+ if (!cleanTitle) return;
+ save({
+ ...data,
+ tasks: [
+ ...data.tasks,
+ {
+ id: context.uid("task"),
+ title: cleanTitle,
+ description: "",
+ type: draft.type,
+ checked: false,
+ checkedAt: 0
+ }
+ ]
+ });
+ reset();
});
- setTitle("");
- setType("daily");
}
function updateTask(taskId, updater) {
@@ -509,8 +509,8 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
{editing && (