Add text import export for toolbox modules

This commit is contained in:
Shinuwa 2026-08-09 15:26:10 +02:00
parent a9b5ca423a
commit 117b7b79e1
27 changed files with 1430 additions and 181 deletions

View file

@ -1,10 +1,9 @@
// Rôle : fournit l'outil checklist avec quantités, catégories et imports texte.
import { useCallback, useState } from "react";
// Rôle : fournit l'outil checklist avec quantités, catégories et échange texte.
import { 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";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
@ -12,9 +11,6 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
const [label, setLabel] = useState("");
const [sectionTitle, setSectionTitle] = useState("");
const [qty, setQty] = useState(1);
const [textImport, setTextImport] = useState("");
const [importOpen, setImportOpen] = useState(false);
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);
@ -84,17 +80,6 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
setQty(1);
}
function importItems(event) {
event.preventDefault();
const importedSections = parseChecklistImport(textImport, context);
if (!importedSections.length) return;
const nextSections = importedSections.reduce((sections, section) => appendItems(sections, section.title, section.items), data.sections);
save(nextSections);
setTextImport("");
setImportOpen(false);
context.notify?.("Import checklist terminé.");
}
function setCompletedSectionsMode(mode) {
save(data.sections.map((section) => ({ ...section, hideWhenComplete: undefined })), {
hideCompletedSections: mode === "reduced",
@ -132,25 +117,9 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
<button className="primary">{textContent.addButton || "Ajouter"}</button>
</form>
<div className="text-import-actions">
<button type="button" onClick={() => setImportOpen(true)}>
<Icon name="import" />
<span>{textContent.importOpenButton || "Importer du texte"}</span>
</button>
</div>
<TextExchangeActions type="checklist" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "checklist")} />
</div>
)}
{importOpen && (
<TextImportModal
title={textContent.importModalTitle || "Importer une checklist"}
value={textImport}
onChange={setTextImport}
placeholder={textContent.importPlaceholder || "Importer plusieurs items...\nPotion:10\nMéga potion:5"}
submitLabel={textContent.importButton || "Importer le texte"}
onSubmit={importItems}
onClose={closeImportModal}
/>
)}
{namedSections.length > 0 && (
<div className="checklist-toolbar" role="radiogroup" aria-label={textContent.completedSectionsModeTitle || "Affichage des catégories terminées"}>
{[
@ -211,32 +180,6 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
);
}
function parseChecklistImport(text, context) {
const sections = [];
let currentSection = { id: context.uid("section"), title: "", items: [] };
String(text || "").split(/\r?\n/).forEach((line) => {
const cleanLine = line.trim();
if (!cleanLine) return;
if (cleanLine.startsWith("#")) {
if (currentSection.items.length || currentSection.title) sections.push(currentSection);
currentSection = { id: context.uid("section"), title: cleanLine.replace(/^#+/, "").trim(), items: [] };
return;
}
parseColonImportLines(cleanLine).forEach((entry) => {
currentSection.items.push({
id: context.uid("item"),
label: entry.label,
qtyTarget: Math.max(1, Number.parseInt(entry.value, 10) || 1),
qtyCurrent: 0
});
});
});
if (currentSection.items.length || currentSection.title) sections.push(currentSection);
return sections.filter((section) => section.items.length);
}
function isSectionComplete(section, context) {
return section.items.length > 0 && section.items.every((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget);
}