From abfa23fb3eac089391918eaf83e594eed79ea558 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Mon, 24 Aug 2026 14:14:57 +0200 Subject: [PATCH] Refactor calculator formula tagging workflow --- tests/helpers/data-validation.mjs | 9 +- tests/static-toolboxes.test.mjs | 15 +- website/public/data/site.json | 9 +- .../toolboxes/modules/CalculatorModule.jsx | 251 +++++++----------- .../styles/toolboxes/_calculator-results.scss | 21 -- .../styles/toolboxes/_shared-controls.scss | 4 - 6 files changed, 118 insertions(+), 191 deletions(-) diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 062901f..3ca3a73 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -257,19 +257,18 @@ export function validateSiteContent(site) { "toolboxes.modules.calculator.labelLabel", "toolboxes.modules.calculator.labelPlaceholder", "toolboxes.modules.calculator.saveButton", + "toolboxes.modules.calculator.updateButton", + "toolboxes.modules.calculator.newButton", "toolboxes.modules.calculator.rootButton", "toolboxes.modules.calculator.resetTitle", "toolboxes.modules.calculator.copyTitle", "toolboxes.modules.calculator.copiedTitle", "toolboxes.modules.calculator.scrollableTitle", "toolboxes.modules.calculator.emptyResults", - "toolboxes.modules.calculator.formulaTitle", - "toolboxes.modules.calculator.childFormulaHelp", - "toolboxes.modules.calculator.rootFormulaHelp", "toolboxes.modules.calculator.baseTokenLabel", "toolboxes.modules.calculator.insertBaseTitle", - "toolboxes.modules.calculator.useEntryTitle", - "toolboxes.modules.calculator.renameTitle", + "toolboxes.modules.calculator.editTitle", + "toolboxes.modules.calculator.addChildTitle", "toolboxes.modules.calculator.deleteTitle", "toolboxes.modules.goalTree.editorTitle", "toolboxes.modules.goalTree.treeTitle", diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index c16ec47..a2aaae7 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -245,18 +245,23 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(calculatorModule, /calculateExpression/); assert.match(calculatorModule, /activeParentId/); assert.match(calculatorModule, /parentId/); - assert.match(calculatorModule, /useInlineEdit/); + assert.match(calculatorModule, /MixTagsInput/); assert.match(calculatorModule, /useGroupedReorder/); assert.match(calculatorModule, /calculator-drag-handle/); assert.match(calculatorModule, /scrollResults/); assert.match(calculatorModule, /tool-split-scroll-toggle/); - assert.match(calculatorModule, /EditableCalculatorLabel/); - assert.match(calculatorModule, /tool-split-entry-label/); assert.match(calculatorModule, /copyChecklistImport/); assert.match(calculatorModule, /resetCalculator/); assert.match(calculatorModule, /returnToRoot/); - assert.match(calculatorModule, /setExpression\(activeParent \? formatResult\(activeParent\.value\) : ""\)/); - assert.match(calculatorModule, /EditableCalculatorFormula/); + assert.match(calculatorModule, /createChildEntry/); + assert.match(calculatorModule, /const childId = context\.uid\("calc"\)/); + assert.match(calculatorModule, /parentId: entry\.id/); + assert.match(calculatorModule, /formula: BASE_TOKEN/); + assert.match(calculatorModule, /pendingExpressionFocusRef/); + assert.match(calculatorModule, /setExpression\(BASE_TOKEN\)/); + assert.match(calculatorModule, /editingEntryId/); + assert.match(calculatorModule, /updateButton/); + assert.match(calculatorModule, /onAddChild/); assert.match(calculatorModule, /entry\.label \|\| formatResult\(entry\.value\)/); assert.match(calculatorModule, /ResizeObserver/); assert.match(calculatorModule, /--calculator-scroll-height/); diff --git a/website/public/data/site.json b/website/public/data/site.json index b57c235..322bc11 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -1854,19 +1854,18 @@ "labelLabel": "Libellé", "labelPlaceholder": "ex: Lingots de fer", "saveButton": "Enregistrer", + "updateButton": "Mettre à jour", + "newButton": "Nouveau calcul", "rootButton": "Revenir à la racine", "resetTitle": "Réinitialiser", "copyTitle": "Copier pour checklist", "copiedTitle": "Copié", "scrollableTitle": "Liste scrollable", "emptyResults": "Aucun résultat enregistré.", - "formulaTitle": "Modifier la formule", - "childFormulaHelp": "Utilisez base pour référencer le parent.", - "rootFormulaHelp": "Valeur ou formule racine.", "baseTokenLabel": "Valeur du parent", "insertBaseTitle": "Ajouter la valeur du parent", - "useEntryTitle": "Utiliser comme base", - "renameTitle": "Renommer", + "editTitle": "Modifier", + "addChildTitle": "Ajouter un calcul enfant", "deleteTitle": "Supprimer" }, "goalTree": { diff --git a/website/src/features/toolboxes/modules/CalculatorModule.jsx b/website/src/features/toolboxes/modules/CalculatorModule.jsx index d78dbbb..f910c16 100644 --- a/website/src/features/toolboxes/modules/CalculatorModule.jsx +++ b/website/src/features/toolboxes/modules/CalculatorModule.jsx @@ -1,8 +1,7 @@ // Rôle : fournit l'outil calculateur avec résultats enregistrés en arborescence et échange texte. -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { Icon } from "../../../components/Icon.jsx"; import { MixTagsInput } from "../../../components/MixTagsInput.jsx"; -import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; @@ -117,6 +116,23 @@ function FormattedCalculatorNumber({ value }) { }); } +function focusExpressionInputAtEnd(element) { + if (!element) return; + element.focus(); + if (typeof element.setSelectionRange === "function") { + const length = element.value?.length || 0; + element.setSelectionRange(length, length); + return; + } + if (!element.isContentEditable) return; + const range = document.createRange(); + range.selectNodeContents(element); + range.collapse(false); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); +} + export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpen }) { const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] })); const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.entries]); @@ -125,11 +141,17 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe const [expression, setExpression] = useState(""); const [label, setLabel] = useState(""); const [activeParentId, setActiveParentId] = useState(""); + const [editingEntryId, setEditingEntryId] = useState(""); const [calculatorHeight, setCalculatorHeight] = useState(0); const [copied, setCopied] = useState(false); const calculatorCardRef = useRef(null); - const activeParent = calculatedEntries.find((entry) => entry.id === activeParentId); - const result = useMemo(() => calculateExpression(expression, activeParent?.value), [activeParent?.value, expression]); + const expressionInputRef = useRef(null); + const pendingExpressionFocusRef = useRef(false); + const editingEntry = calculatedEntries.find((entry) => entry.id === editingEntryId); + const formulaParentId = editingEntry ? editingEntry.parentId || "" : activeParentId; + const formulaParent = calculatedEntries.find((entry) => entry.id === formulaParentId); + const formMode = editingEntry ? "edit" : formulaParent ? "child" : "root"; + const result = useMemo(() => calculateExpression(expression, formulaParent?.value), [formulaParent?.value, expression]); const scopeParentId = (parentId = "") => `${moduleId}:${parentId}`; const reorder = useGroupedReorder({ namespace: "calculator", @@ -156,6 +178,12 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe return () => observer.disconnect(); }, []); + useLayoutEffect(() => { + if (!pendingExpressionFocusRef.current || !editingEntryId) return; + pendingExpressionFocusRef.current = false; + window.requestAnimationFrame(() => focusExpressionInputAtEnd(expressionInputRef.current)); + }, [editingEntryId]); + function save(entries) { context.setModuleData(toolboxId, moduleId, { ...data, entries: evaluateCalculatorEntries(entries) }); } @@ -168,24 +196,58 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe event.preventDefault(); if (result == null) return; const cleanLabel = label.trim(); + const storedFormula = createStoredFormula(expression, formulaParent?.value); + + if (editingEntry) { + save(calculatedEntries.map((entry) => entry.id === editingEntry.id ? { + ...entry, + label: cleanLabel, + value: result, + formula: storedFormula + } : entry)); + setLabel(cleanLabel); + return; + } save([ ...data.entries, { id: context.uid("calc"), - parentId: activeParentId, + parentId: formulaParentId, label: cleanLabel, value: result, - formula: createStoredFormula(expression, activeParent?.value) + formula: storedFormula } ]); - setExpression(activeParent ? formatResult(activeParent.value) : ""); + setExpression(formulaParent ? BASE_TOKEN : ""); setLabel(""); } - function useEntry(entry) { - setActiveParentId(entry.id); + function editEntry(entry) { + setEditingEntryId(entry.id); + setActiveParentId(""); + setExpression(getEditableFormula(entry)); + setLabel(entry.label || ""); + } + + function createChildEntry(entry) { + const childId = context.uid("calc"); + save([ + ...data.entries, + { + id: childId, + parentId: entry.id, + label: "", + value: Number.isFinite(entry.value) ? entry.value : 0, + formula: BASE_TOKEN + } + ]); + setEditingEntryId(""); + pendingExpressionFocusRef.current = true; + setEditingEntryId(childId); + setActiveParentId(""); setExpression(BASE_TOKEN); + setLabel(""); } function deleteEntry(entryId) { @@ -194,34 +256,26 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe setActiveParentId(""); setExpression(""); } + if (deletedIds.has(editingEntryId)) { + setEditingEntryId(""); + setLabel(""); + setExpression(""); + } save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id))); } - function renameEntry(entryId, label) { - const cleanLabel = label.trim(); - save(calculatedEntries.map((entry) => entry.id === entryId ? { ...entry, label: cleanLabel } : entry)); - } - - function updateEntryFormula(entryId, formula) { - const nextEntries = calculatedEntries.map((entry) => { - if (entry.id !== entryId) return entry; - const parent = calculatedEntries.find((candidate) => candidate.id === entry.parentId); - const nextFormula = createStoredFormula(formula, parent?.value); - const nextValue = calculateExpression(nextFormula, parent?.value) ?? entry.value; - return { ...entry, formula: nextFormula, value: nextValue }; - }); - save(nextEntries); - } - function resetCalculator() { setExpression(""); setLabel(""); setActiveParentId(""); + setEditingEntryId(""); } function returnToRoot() { setActiveParentId(""); + setEditingEntryId(""); setExpression(""); + setLabel(""); } async function copyChecklistImport() { @@ -245,10 +299,11 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
{textContent.expressionLabel || "Calcul"} - {activeParent ? ( + {formulaParent ? ( { if (event.key !== "Enter") return; @@ -273,6 +328,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe setExpression(event.target.value)} onKeyDown={(event) => { if (event.key !== "Enter") return; @@ -295,7 +351,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe {textContent.labelLabel || "Libellé"} setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} /> - +
- {activeParent && ( + {(formulaParent || editingEntry) && ( )}
{calculatedEntries.length ? ( - + ) : (

{textContent.emptyResults || "Aucun résultat enregistré."}

)} @@ -338,31 +394,15 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe ); } -function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentId, textContent, reorder, onUse, onRename, onUpdateFormula, onDelete }) { +function CalculatorEntries({ entries, parentId, getScopedParentId, activeEntryId, textContent, reorder, onEdit, onAddChild, onDelete }) { const children = getChildren(entries, parentId); - const [editingId, setEditingId] = useState(""); - useEffect(() => { - if (!editingId) return undefined; - - function closeOnOutsidePointer(event) { - if (event.target?.closest?.(`[data-calculator-edit-fields="${editingId}"]`)) return; - if (event.target?.closest?.("[data-calculator-edit-toggle]")) return; - setEditingId(""); - } - - document.addEventListener("pointerdown", closeOnOutsidePointer); - return () => document.removeEventListener("pointerdown", closeOnOutsidePointer); - }, [editingId]); - if (!children.length) return null; return (
    {children.map((entry) => { const className = [ - entry.id === activeParentId ? "active" : "", - entry.id === editingId ? "is-editing" : "", - entry.parentId ? "can-edit-formula" : "", + entry.id === activeEntryId ? "active" : "", reorder.isItemDragging(entry.id) ? "is-dragging" : "", reorder.isItemDropTarget(entry.id) ? "is-drop-target" : "", reorder.getDropPlacement("item", entry.id, parentId) === "after" ? "drop-after" : "" @@ -380,121 +420,30 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI > - {entry.id === editingId ? ( - - - + - )} +
- + ); })} ); } - -function EditableCalculatorLabel({ entry, textContent, onRename, onDone, autoFocus = true }) { - const inlineEdit = useInlineEdit({ - value: entry.label, - onCommit: (label) => onRename(entry.id, label), - onDone, - autoFocus, - focusKey: entry.id - }); - - return ( - - ); -} - -function EditableCalculatorFormula({ entry, entries, textContent, onUpdateFormula, autoFocus = false }) { - const parent = entries.find((candidate) => candidate.id === entry.parentId); - const parentValue = parent?.value; - const hasParent = Boolean(entry.parentId); - const inlineEdit = useInlineEdit({ - value: getEditableFormula(entry), - onCommit: (formula) => { - const storedFormula = createStoredFormula(formula, parentValue); - if (calculateExpression(storedFormula, parentValue) != null) onUpdateFormula(entry.id, storedFormula); - }, - autoFocus, - focusKey: `${entry.id}:formula`, - transform: (draft) => String(draft || "").replaceAll(",", ".").trim() - }); - - if (!hasParent) { - return ( - - ); - } - - return ( - inlineEdit.commit()} - onChange={inlineEdit.setDraft} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.preventDefault(); - event.currentTarget.blur(); - } - if (event.key === "Escape") { - event.preventDefault(); - inlineEdit.cancel(event); - } - }} - formulaSpacing - insertToken={{ - value: "base", - label: textContent.baseTokenLabel || "Valeur du parent", - icon: "parent", - title: textContent.insertBaseTitle || "Ajouter la valeur du parent" - }} - placeholder={textContent.formulaPlaceholder || "base * 3"} - size="small" - title={entry.parentId ? textContent.childFormulaHelp || "Utilisez base pour référencer le parent." : textContent.rootFormulaHelp || "Valeur ou formule racine."} - tokens={[{ - value: "base", - label: textContent.baseTokenLabel || "Valeur du parent" - }]} - value={inlineEdit.draft} - /> - ); -} diff --git a/website/src/styles/toolboxes/_calculator-results.scss b/website/src/styles/toolboxes/_calculator-results.scss index cce7431..6cd2a93 100644 --- a/website/src/styles/toolboxes/_calculator-results.scss +++ b/website/src/styles/toolboxes/_calculator-results.scss @@ -27,27 +27,6 @@ grid-template-columns: 30px minmax(0, 1fr) 34px 34px; } -.tool-split-entry-list li.is-editing > .calculator-entry.has-drag-handle { - grid-template-columns: 30px minmax(0, 1fr) 34px 34px; - align-items: start; -} - -.tool-split-entry-list li.is-editing > .calculator-entry.has-drag-handle .calculator-drag-handle { - grid-row: 1 / span 2; -} - -.calculator-entry-formula { - font-variant-numeric: tabular-nums; -} - -.calculator-entry-edit-fields { - display: grid; - min-width: 0; - gap: 6px; - grid-column: 2; - grid-row: 1 / span 2; -} - .calculator-drag-handle { width: 30px; min-width: 30px; diff --git a/website/src/styles/toolboxes/_shared-controls.scss b/website/src/styles/toolboxes/_shared-controls.scss index fd3c22b..2c9c70d 100644 --- a/website/src/styles/toolboxes/_shared-controls.scss +++ b/website/src/styles/toolboxes/_shared-controls.scss @@ -658,10 +658,6 @@ min-height: 36px; } -.tool-split-entry > button.active:not(.tool-split-entry-summary, .tool-split-entry-value, .danger) { - @include mixins.temporary-toggle-active; -} - .tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-summary, .tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-value, .tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-label {