Refactor calculator formula tagging workflow
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
This commit is contained in:
parent
ae503c7260
commit
abfa23fb3e
6 changed files with 118 additions and 191 deletions
|
|
@ -257,19 +257,18 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.calculator.labelLabel",
|
"toolboxes.modules.calculator.labelLabel",
|
||||||
"toolboxes.modules.calculator.labelPlaceholder",
|
"toolboxes.modules.calculator.labelPlaceholder",
|
||||||
"toolboxes.modules.calculator.saveButton",
|
"toolboxes.modules.calculator.saveButton",
|
||||||
|
"toolboxes.modules.calculator.updateButton",
|
||||||
|
"toolboxes.modules.calculator.newButton",
|
||||||
"toolboxes.modules.calculator.rootButton",
|
"toolboxes.modules.calculator.rootButton",
|
||||||
"toolboxes.modules.calculator.resetTitle",
|
"toolboxes.modules.calculator.resetTitle",
|
||||||
"toolboxes.modules.calculator.copyTitle",
|
"toolboxes.modules.calculator.copyTitle",
|
||||||
"toolboxes.modules.calculator.copiedTitle",
|
"toolboxes.modules.calculator.copiedTitle",
|
||||||
"toolboxes.modules.calculator.scrollableTitle",
|
"toolboxes.modules.calculator.scrollableTitle",
|
||||||
"toolboxes.modules.calculator.emptyResults",
|
"toolboxes.modules.calculator.emptyResults",
|
||||||
"toolboxes.modules.calculator.formulaTitle",
|
|
||||||
"toolboxes.modules.calculator.childFormulaHelp",
|
|
||||||
"toolboxes.modules.calculator.rootFormulaHelp",
|
|
||||||
"toolboxes.modules.calculator.baseTokenLabel",
|
"toolboxes.modules.calculator.baseTokenLabel",
|
||||||
"toolboxes.modules.calculator.insertBaseTitle",
|
"toolboxes.modules.calculator.insertBaseTitle",
|
||||||
"toolboxes.modules.calculator.useEntryTitle",
|
"toolboxes.modules.calculator.editTitle",
|
||||||
"toolboxes.modules.calculator.renameTitle",
|
"toolboxes.modules.calculator.addChildTitle",
|
||||||
"toolboxes.modules.calculator.deleteTitle",
|
"toolboxes.modules.calculator.deleteTitle",
|
||||||
"toolboxes.modules.goalTree.editorTitle",
|
"toolboxes.modules.goalTree.editorTitle",
|
||||||
"toolboxes.modules.goalTree.treeTitle",
|
"toolboxes.modules.goalTree.treeTitle",
|
||||||
|
|
|
||||||
|
|
@ -245,18 +245,23 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(calculatorModule, /calculateExpression/);
|
assert.match(calculatorModule, /calculateExpression/);
|
||||||
assert.match(calculatorModule, /activeParentId/);
|
assert.match(calculatorModule, /activeParentId/);
|
||||||
assert.match(calculatorModule, /parentId/);
|
assert.match(calculatorModule, /parentId/);
|
||||||
assert.match(calculatorModule, /useInlineEdit/);
|
assert.match(calculatorModule, /MixTagsInput/);
|
||||||
assert.match(calculatorModule, /useGroupedReorder/);
|
assert.match(calculatorModule, /useGroupedReorder/);
|
||||||
assert.match(calculatorModule, /calculator-drag-handle/);
|
assert.match(calculatorModule, /calculator-drag-handle/);
|
||||||
assert.match(calculatorModule, /scrollResults/);
|
assert.match(calculatorModule, /scrollResults/);
|
||||||
assert.match(calculatorModule, /tool-split-scroll-toggle/);
|
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, /copyChecklistImport/);
|
||||||
assert.match(calculatorModule, /resetCalculator/);
|
assert.match(calculatorModule, /resetCalculator/);
|
||||||
assert.match(calculatorModule, /returnToRoot/);
|
assert.match(calculatorModule, /returnToRoot/);
|
||||||
assert.match(calculatorModule, /setExpression\(activeParent \? formatResult\(activeParent\.value\) : ""\)/);
|
assert.match(calculatorModule, /createChildEntry/);
|
||||||
assert.match(calculatorModule, /EditableCalculatorFormula/);
|
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, /entry\.label \|\| formatResult\(entry\.value\)/);
|
||||||
assert.match(calculatorModule, /ResizeObserver/);
|
assert.match(calculatorModule, /ResizeObserver/);
|
||||||
assert.match(calculatorModule, /--calculator-scroll-height/);
|
assert.match(calculatorModule, /--calculator-scroll-height/);
|
||||||
|
|
|
||||||
|
|
@ -1854,19 +1854,18 @@
|
||||||
"labelLabel": "Libellé",
|
"labelLabel": "Libellé",
|
||||||
"labelPlaceholder": "ex: Lingots de fer",
|
"labelPlaceholder": "ex: Lingots de fer",
|
||||||
"saveButton": "Enregistrer",
|
"saveButton": "Enregistrer",
|
||||||
|
"updateButton": "Mettre à jour",
|
||||||
|
"newButton": "Nouveau calcul",
|
||||||
"rootButton": "Revenir à la racine",
|
"rootButton": "Revenir à la racine",
|
||||||
"resetTitle": "Réinitialiser",
|
"resetTitle": "Réinitialiser",
|
||||||
"copyTitle": "Copier pour checklist",
|
"copyTitle": "Copier pour checklist",
|
||||||
"copiedTitle": "Copié",
|
"copiedTitle": "Copié",
|
||||||
"scrollableTitle": "Liste scrollable",
|
"scrollableTitle": "Liste scrollable",
|
||||||
"emptyResults": "Aucun résultat enregistré.",
|
"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",
|
"baseTokenLabel": "Valeur du parent",
|
||||||
"insertBaseTitle": "Ajouter la valeur du parent",
|
"insertBaseTitle": "Ajouter la valeur du parent",
|
||||||
"useEntryTitle": "Utiliser comme base",
|
"editTitle": "Modifier",
|
||||||
"renameTitle": "Renommer",
|
"addChildTitle": "Ajouter un calcul enfant",
|
||||||
"deleteTitle": "Supprimer"
|
"deleteTitle": "Supprimer"
|
||||||
},
|
},
|
||||||
"goalTree": {
|
"goalTree": {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
// Rôle : fournit l'outil calculateur avec résultats enregistrés en arborescence et échange texte.
|
// 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 { Icon } from "../../../components/Icon.jsx";
|
||||||
import { MixTagsInput } from "../../../components/MixTagsInput.jsx";
|
import { MixTagsInput } from "../../../components/MixTagsInput.jsx";
|
||||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
|
||||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
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 }) {
|
export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
||||||
const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] }));
|
const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] }));
|
||||||
const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.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 [expression, setExpression] = useState("");
|
||||||
const [label, setLabel] = useState("");
|
const [label, setLabel] = useState("");
|
||||||
const [activeParentId, setActiveParentId] = useState("");
|
const [activeParentId, setActiveParentId] = useState("");
|
||||||
|
const [editingEntryId, setEditingEntryId] = useState("");
|
||||||
const [calculatorHeight, setCalculatorHeight] = useState(0);
|
const [calculatorHeight, setCalculatorHeight] = useState(0);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const calculatorCardRef = useRef(null);
|
const calculatorCardRef = useRef(null);
|
||||||
const activeParent = calculatedEntries.find((entry) => entry.id === activeParentId);
|
const expressionInputRef = useRef(null);
|
||||||
const result = useMemo(() => calculateExpression(expression, activeParent?.value), [activeParent?.value, expression]);
|
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 scopeParentId = (parentId = "") => `${moduleId}:${parentId}`;
|
||||||
const reorder = useGroupedReorder({
|
const reorder = useGroupedReorder({
|
||||||
namespace: "calculator",
|
namespace: "calculator",
|
||||||
|
|
@ -156,6 +178,12 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!pendingExpressionFocusRef.current || !editingEntryId) return;
|
||||||
|
pendingExpressionFocusRef.current = false;
|
||||||
|
window.requestAnimationFrame(() => focusExpressionInputAtEnd(expressionInputRef.current));
|
||||||
|
}, [editingEntryId]);
|
||||||
|
|
||||||
function save(entries) {
|
function save(entries) {
|
||||||
context.setModuleData(toolboxId, moduleId, { ...data, entries: evaluateCalculatorEntries(entries) });
|
context.setModuleData(toolboxId, moduleId, { ...data, entries: evaluateCalculatorEntries(entries) });
|
||||||
}
|
}
|
||||||
|
|
@ -168,24 +196,58 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
const cleanLabel = label.trim();
|
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([
|
save([
|
||||||
...data.entries,
|
...data.entries,
|
||||||
{
|
{
|
||||||
id: context.uid("calc"),
|
id: context.uid("calc"),
|
||||||
parentId: activeParentId,
|
parentId: formulaParentId,
|
||||||
label: cleanLabel,
|
label: cleanLabel,
|
||||||
value: result,
|
value: result,
|
||||||
formula: createStoredFormula(expression, activeParent?.value)
|
formula: storedFormula
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
setExpression(activeParent ? formatResult(activeParent.value) : "");
|
setExpression(formulaParent ? BASE_TOKEN : "");
|
||||||
setLabel("");
|
setLabel("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEntry(entry) {
|
function editEntry(entry) {
|
||||||
setActiveParentId(entry.id);
|
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);
|
setExpression(BASE_TOKEN);
|
||||||
|
setLabel("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteEntry(entryId) {
|
function deleteEntry(entryId) {
|
||||||
|
|
@ -194,34 +256,26 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
setActiveParentId("");
|
setActiveParentId("");
|
||||||
setExpression("");
|
setExpression("");
|
||||||
}
|
}
|
||||||
|
if (deletedIds.has(editingEntryId)) {
|
||||||
|
setEditingEntryId("");
|
||||||
|
setLabel("");
|
||||||
|
setExpression("");
|
||||||
|
}
|
||||||
save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id)));
|
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() {
|
function resetCalculator() {
|
||||||
setExpression("");
|
setExpression("");
|
||||||
setLabel("");
|
setLabel("");
|
||||||
setActiveParentId("");
|
setActiveParentId("");
|
||||||
|
setEditingEntryId("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function returnToRoot() {
|
function returnToRoot() {
|
||||||
setActiveParentId("");
|
setActiveParentId("");
|
||||||
|
setEditingEntryId("");
|
||||||
setExpression("");
|
setExpression("");
|
||||||
|
setLabel("");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyChecklistImport() {
|
async function copyChecklistImport() {
|
||||||
|
|
@ -245,10 +299,11 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
<div className="calculator-card" ref={calculatorCardRef}>
|
<div className="calculator-card" ref={calculatorCardRef}>
|
||||||
<div className="calculator-field">
|
<div className="calculator-field">
|
||||||
<span>{textContent.expressionLabel || "Calcul"}</span>
|
<span>{textContent.expressionLabel || "Calcul"}</span>
|
||||||
{activeParent ? (
|
{formulaParent ? (
|
||||||
<MixTagsInput
|
<MixTagsInput
|
||||||
aria-label={textContent.expressionLabel || "Calcul"}
|
aria-label={textContent.expressionLabel || "Calcul"}
|
||||||
className="calculator-expression-input"
|
className="calculator-expression-input"
|
||||||
|
inputRef={expressionInputRef}
|
||||||
onChange={setExpression}
|
onChange={setExpression}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key !== "Enter") return;
|
if (event.key !== "Enter") return;
|
||||||
|
|
@ -273,6 +328,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
<input
|
<input
|
||||||
aria-label={textContent.expressionLabel || "Calcul"}
|
aria-label={textContent.expressionLabel || "Calcul"}
|
||||||
className="calculator-expression-input"
|
className="calculator-expression-input"
|
||||||
|
ref={expressionInputRef}
|
||||||
onChange={(event) => setExpression(event.target.value)}
|
onChange={(event) => setExpression(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key !== "Enter") return;
|
if (event.key !== "Enter") return;
|
||||||
|
|
@ -295,7 +351,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
<span>{textContent.labelLabel || "Libellé"}</span>
|
<span>{textContent.labelLabel || "Libellé"}</span>
|
||||||
<input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
|
<input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
|
||||||
</label>
|
</label>
|
||||||
<button className="primary" disabled={result == null}>{textContent.saveButton || "Enregistrer"}</button>
|
<button className="primary" disabled={result == null}>{formMode === "edit" ? textContent.updateButton || "Mettre à jour" : textContent.saveButton || "Enregistrer"}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div
|
<div
|
||||||
|
|
@ -303,9 +359,9 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
style={data.scrollResults && calculatorHeight ? { "--calculator-scroll-height": `${calculatorHeight}px` } : undefined}
|
style={data.scrollResults && calculatorHeight ? { "--calculator-scroll-height": `${calculatorHeight}px` } : undefined}
|
||||||
>
|
>
|
||||||
<div className="tool-split-actions">
|
<div className="tool-split-actions">
|
||||||
{activeParent && (
|
{(formulaParent || editingEntry) && (
|
||||||
<button className="tool-split-root-button" type="button" onClick={returnToRoot}>
|
<button className="tool-split-root-button" type="button" onClick={returnToRoot}>
|
||||||
{textContent.rootButton || "Revenir à la racine"}
|
{editingEntry ? textContent.newButton || "Nouveau calcul" : textContent.rootButton || "Revenir à la racine"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button className="tool-split-action-button danger" type="button" onClick={resetCalculator} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
|
<button className="tool-split-action-button danger" type="button" onClick={resetCalculator} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
|
||||||
|
|
@ -328,7 +384,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
</div>
|
</div>
|
||||||
<div className={`tool-split-tree calculator-tree ${data.scrollResults ? "is-scrollable legacy-scrollbar" : ""}`}>
|
<div className={`tool-split-tree calculator-tree ${data.scrollResults ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||||
{calculatedEntries.length ? (
|
{calculatedEntries.length ? (
|
||||||
<CalculatorEntries entries={calculatedEntries} parentId="" getScopedParentId={scopeParentId} activeParentId={activeParentId} textContent={textContent} reorder={reorder} onUse={useEntry} onRename={renameEntry} onUpdateFormula={updateEntryFormula} onDelete={deleteEntry} />
|
<CalculatorEntries entries={calculatedEntries} parentId="" getScopedParentId={scopeParentId} activeEntryId={editingEntryId || activeParentId} textContent={textContent} reorder={reorder} onEdit={editEntry} onAddChild={createChildEntry} onDelete={deleteEntry} />
|
||||||
) : (
|
) : (
|
||||||
<p className="muted">{textContent.emptyResults || "Aucun résultat enregistré."}</p>
|
<p className="muted">{textContent.emptyResults || "Aucun résultat enregistré."}</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -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 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;
|
if (!children.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="tool-split-entry-list calculator-entry-list">
|
<ul className="tool-split-entry-list calculator-entry-list">
|
||||||
{children.map((entry) => {
|
{children.map((entry) => {
|
||||||
const className = [
|
const className = [
|
||||||
entry.id === activeParentId ? "active" : "",
|
entry.id === activeEntryId ? "active" : "",
|
||||||
entry.id === editingId ? "is-editing" : "",
|
|
||||||
entry.parentId ? "can-edit-formula" : "",
|
|
||||||
reorder.isItemDragging(entry.id) ? "is-dragging" : "",
|
reorder.isItemDragging(entry.id) ? "is-dragging" : "",
|
||||||
reorder.isItemDropTarget(entry.id) ? "is-drop-target" : "",
|
reorder.isItemDropTarget(entry.id) ? "is-drop-target" : "",
|
||||||
reorder.getDropPlacement("item", entry.id, parentId) === "after" ? "drop-after" : ""
|
reorder.getDropPlacement("item", entry.id, parentId) === "after" ? "drop-after" : ""
|
||||||
|
|
@ -380,13 +420,7 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
|
||||||
>
|
>
|
||||||
<Icon name="drag" />
|
<Icon name="drag" />
|
||||||
</button>
|
</button>
|
||||||
{entry.id === editingId ? (
|
<button className="tool-split-entry-summary" type="button" onClick={() => onEdit(entry)} title={textContent.editTitle || "Modifier"}>
|
||||||
<span className="calculator-entry-edit-fields" data-calculator-edit-fields={entry.id}>
|
|
||||||
<EditableCalculatorFormula entry={entry} entries={entries} textContent={textContent} onUpdateFormula={onUpdateFormula} />
|
|
||||||
<EditableCalculatorLabel entry={entry} textContent={textContent} onRename={onRename} />
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<button className="tool-split-entry-summary" type="button" onClick={() => onUse(entry)} title={textContent.useEntryTitle || "Utiliser comme base"}>
|
|
||||||
<span>
|
<span>
|
||||||
<strong className={getEntryResultLengthClass(entry.value)} aria-label={formatDisplayResult(entry.value)}>
|
<strong className={getEntryResultLengthClass(entry.value)} aria-label={formatDisplayResult(entry.value)}>
|
||||||
<FormattedCalculatorNumber value={entry.value} />
|
<FormattedCalculatorNumber value={entry.value} />
|
||||||
|
|
@ -394,107 +428,22 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
|
||||||
{entry.label && <em>{entry.label}</em>}
|
{entry.label && <em>{entry.label}</em>}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
className={entry.id === editingId ? "active" : ""}
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditingId(entry.id === editingId ? "" : entry.id)}
|
onClick={() => onAddChild(entry)}
|
||||||
aria-pressed={entry.id === editingId}
|
aria-label={`${textContent.addChildTitle || "Ajouter un calcul enfant"} ${entry.label || formatResult(entry.value)}`}
|
||||||
aria-label={`${textContent.renameTitle || "Renommer"} ${entry.label}`}
|
title={textContent.addChildTitle || "Ajouter un calcul enfant"}
|
||||||
data-calculator-edit-toggle
|
|
||||||
title={textContent.renameTitle || "Renommer"}
|
|
||||||
>
|
>
|
||||||
<Icon name="edit" />
|
<Icon name="add" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
|
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
|
||||||
<Icon name="trash" />
|
<Icon name="trash" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<CalculatorEntries entries={entries} parentId={entry.id} getScopedParentId={getScopedParentId} activeParentId={activeParentId} textContent={textContent} reorder={reorder} onUse={onUse} onRename={onRename} onUpdateFormula={onUpdateFormula} onDelete={onDelete} />
|
<CalculatorEntries entries={entries} parentId={entry.id} getScopedParentId={getScopedParentId} activeEntryId={activeEntryId} textContent={textContent} reorder={reorder} onEdit={onEdit} onAddChild={onAddChild} onDelete={onDelete} />
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
|
||||||
<input
|
|
||||||
{...inlineEdit.getInputProps({
|
|
||||||
className: "tool-split-entry-label",
|
|
||||||
"aria-label": `${textContent.renameTitle || "Renommer"} ${entry.label}`
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<input
|
|
||||||
{...inlineEdit.getInputProps({
|
|
||||||
className: "tool-split-entry-label calculator-entry-formula",
|
|
||||||
"aria-label": `${textContent.formulaTitle || "Formule enregistrée"} ${entry.label || formatResult(entry.value)}`,
|
|
||||||
title: textContent.rootFormulaHelp || "Valeur ou formule racine."
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MixTagsInput
|
|
||||||
aria-label={`${textContent.formulaTitle || "Formule enregistrée"} ${entry.label || formatResult(entry.value)}`}
|
|
||||||
className="tool-split-entry-label calculator-entry-formula"
|
|
||||||
onBlur={() => 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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -27,27 +27,6 @@
|
||||||
grid-template-columns: 30px minmax(0, 1fr) 34px 34px;
|
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 {
|
.calculator-drag-handle {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
|
|
|
||||||
|
|
@ -658,10 +658,6 @@
|
||||||
min-height: 36px;
|
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-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-value,
|
||||||
.tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-label {
|
.tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-label {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue