Add internal mix tag input for calculator and goal tree formulas
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
5c8148c905
commit
ae503c7260
13 changed files with 929 additions and 23 deletions
|
|
@ -1,6 +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 { 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";
|
||||
|
|
@ -183,8 +184,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
}
|
||||
|
||||
function useEntry(entry) {
|
||||
setExpression(formatResult(entry.value));
|
||||
setActiveParentId(entry.id);
|
||||
setExpression(BASE_TOKEN);
|
||||
}
|
||||
|
||||
function deleteEntry(entryId) {
|
||||
|
|
@ -242,10 +243,48 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
)}
|
||||
<form className="calculator-form" onSubmit={saveResult}>
|
||||
<div className="calculator-card" ref={calculatorCardRef}>
|
||||
<label>
|
||||
<div className="calculator-field">
|
||||
<span>{textContent.expressionLabel || "Calcul"}</span>
|
||||
<input value={expression} onChange={(event) => setExpression(event.target.value)} placeholder={textContent.expressionPlaceholder || "10*10"} inputMode="decimal" />
|
||||
</label>
|
||||
{activeParent ? (
|
||||
<MixTagsInput
|
||||
aria-label={textContent.expressionLabel || "Calcul"}
|
||||
className="calculator-expression-input"
|
||||
onChange={setExpression}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.closest("form")?.requestSubmit();
|
||||
}}
|
||||
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"}
|
||||
tokens={[{
|
||||
value: "base",
|
||||
label: textContent.baseTokenLabel || "Valeur du parent"
|
||||
}]}
|
||||
value={expression}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
aria-label={textContent.expressionLabel || "Calcul"}
|
||||
className="calculator-expression-input"
|
||||
onChange={(event) => setExpression(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.closest("form")?.requestSubmit();
|
||||
}}
|
||||
inputMode="decimal"
|
||||
placeholder={textContent.expressionPlaceholder || "10*10"}
|
||||
value={expression}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={`calculator-result ${result == null ? "" : getResultLengthClass(result)}`} aria-live="polite">
|
||||
<span>{textContent.resultLabel || "Résultat"}</span>
|
||||
<strong aria-label={result == null ? "-" : formatDisplayResult(result)}>
|
||||
|
|
@ -307,6 +346,7 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
|
|||
|
||||
function closeOnOutsidePointer(event) {
|
||||
if (event.target?.closest?.(`[data-calculator-edit-fields="${editingId}"]`)) return;
|
||||
if (event.target?.closest?.("[data-calculator-edit-toggle]")) return;
|
||||
setEditingId("");
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +362,7 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
|
|||
const className = [
|
||||
entry.id === activeParentId ? "active" : "",
|
||||
entry.id === editingId ? "is-editing" : "",
|
||||
"can-edit-formula",
|
||||
entry.parentId ? "can-edit-formula" : "",
|
||||
reorder.isItemDragging(entry.id) ? "is-dragging" : "",
|
||||
reorder.isItemDropTarget(entry.id) ? "is-drop-target" : "",
|
||||
reorder.getDropPlacement("item", entry.id, parentId) === "after" ? "drop-after" : ""
|
||||
|
|
@ -355,7 +395,15 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
|
|||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={() => setEditingId(entry.id === editingId ? "" : entry.id)} aria-label={`${textContent.renameTitle || "Renommer"} ${entry.label}`} title={textContent.renameTitle || "Renommer"}>
|
||||
<button
|
||||
className={entry.id === editingId ? "active" : ""}
|
||||
type="button"
|
||||
onClick={() => setEditingId(entry.id === editingId ? "" : entry.id)}
|
||||
aria-pressed={entry.id === editingId}
|
||||
aria-label={`${textContent.renameTitle || "Renommer"} ${entry.label}`}
|
||||
data-calculator-edit-toggle
|
||||
title={textContent.renameTitle || "Renommer"}
|
||||
>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
|
||||
|
|
@ -392,6 +440,7 @@ function EditableCalculatorLabel({ entry, textContent, onRename, onDone, autoFoc
|
|||
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) => {
|
||||
|
|
@ -403,13 +452,49 @@ function EditableCalculatorFormula({ entry, entries, textContent, onUpdateFormul
|
|||
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 (
|
||||
<input
|
||||
{...inlineEdit.getInputProps({
|
||||
className: "tool-split-entry-label calculator-entry-formula",
|
||||
"aria-label": `${textContent.formulaTitle || "Formule enregistrée"} ${entry.label || formatResult(entry.value)}`,
|
||||
title: entry.parentId ? textContent.childFormulaHelp || "Utilisez base pour référencer le parent." : textContent.rootFormulaHelp || "Valeur ou formule racine."
|
||||
})}
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|||
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
||||
import { DiagramConnectors } from "../../../components/DiagramConnectors.jsx";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { MixTagsInput } from "../../../components/MixTagsInput.jsx";
|
||||
import { Tabs } from "../../../components/Tabs.jsx";
|
||||
import { Tooltip } from "../../../components/Tooltip.jsx";
|
||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||
|
|
@ -42,6 +43,7 @@ const KIND_ICONS = {
|
|||
};
|
||||
|
||||
const GOAL_TREE_VIEW_SCALE = 0.7;
|
||||
const BASE_TOKEN = "base";
|
||||
|
||||
function getGoalTreeKindIcon(kind) {
|
||||
return KIND_ICONS[kind] || "diagram";
|
||||
|
|
@ -528,10 +530,13 @@ export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditi
|
|||
<div className="goal-tree-form-section-title">{textContent.quantitySectionTitle || "Quantité"}</div>
|
||||
<label>
|
||||
<span>{textContent.targetQuantityLabel || "Cible"}</span>
|
||||
<input
|
||||
<GoalTreeFormulaInput
|
||||
aria-label={textContent.targetQuantityLabel || "Cible"}
|
||||
hasParent={Boolean(selectedNode.parentId)}
|
||||
onChange={(value) => setStepTarget(selectedNode.id, value)}
|
||||
value={selectedNode.formula || selectedNode.qtyTarget}
|
||||
placeholder={textContent.formulaPlaceholder || "base * 3"}
|
||||
onChange={(event) => setStepTarget(selectedNode.id, event.target.value)}
|
||||
textContent={textContent}
|
||||
title={textContent.formulaHelp || "Utilisez base pour référencer la cible du parent."}
|
||||
/>
|
||||
</label>
|
||||
|
|
@ -852,6 +857,46 @@ function GoalTreeTitleInput({ value, onChange }) {
|
|||
);
|
||||
}
|
||||
|
||||
function GoalTreeFormulaInput({ value, onChange, hasParent, textContent, placeholder, title, className = "", "aria-label": ariaLabel }) {
|
||||
const normalizedValue = String(value ?? "");
|
||||
if (!hasParent) {
|
||||
return (
|
||||
<input
|
||||
aria-label={ariaLabel}
|
||||
className={className}
|
||||
inputMode="decimal"
|
||||
onChange={(event) => onChange(event.target.value.replaceAll(",", "."))}
|
||||
placeholder={placeholder}
|
||||
title={title}
|
||||
value={normalizedValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MixTagsInput
|
||||
aria-label={ariaLabel}
|
||||
className={className}
|
||||
formulaSpacing
|
||||
inputMode="decimal"
|
||||
insertToken={{
|
||||
value: BASE_TOKEN,
|
||||
label: textContent.baseTokenLabel || "Valeur du parent",
|
||||
icon: "parent",
|
||||
title: textContent.insertBaseTitle || "Ajouter la valeur du parent"
|
||||
}}
|
||||
onChange={(nextValue) => onChange(nextValue.replaceAll(",", "."))}
|
||||
placeholder={placeholder}
|
||||
title={title}
|
||||
tokens={[{
|
||||
value: BASE_TOKEN,
|
||||
label: textContent.baseTokenLabel || "Valeur du parent"
|
||||
}]}
|
||||
value={normalizedValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function GoalTreeContentEditor({ node, textContent, onAdd, onUpdate, onDelete }) {
|
||||
return (
|
||||
<div className="goal-tree-content-editor">
|
||||
|
|
@ -859,17 +904,21 @@ function GoalTreeContentEditor({ node, textContent, onAdd, onUpdate, onDelete })
|
|||
{(node.items || []).map((item) => (
|
||||
<div className={`goal-tree-content-editor-row is-${node.kind}`} key={item.id}>
|
||||
<input
|
||||
className="goal-tree-content-label-input"
|
||||
value={item.label}
|
||||
placeholder={textContent.itemLabelPlaceholder || "Élément"}
|
||||
onChange={(event) => onUpdate(item.id, (current) => ({ ...current, label: event.target.value }))}
|
||||
aria-label={textContent.itemLabelLabel || "Libellé"}
|
||||
/>
|
||||
{node.kind === "checklist" && (
|
||||
<input
|
||||
<GoalTreeFormulaInput
|
||||
aria-label={textContent.itemTotalLabel || "Total"}
|
||||
className="goal-tree-content-formula-input"
|
||||
hasParent={Boolean(node.parentId)}
|
||||
onChange={(value) => onUpdate(item.id, (current) => ({ ...current, formula: value.replaceAll(",", ".") }))}
|
||||
value={item.formula || "1"}
|
||||
placeholder={textContent.itemTotalPlaceholder || "base"}
|
||||
onChange={(event) => onUpdate(item.id, (current) => ({ ...current, formula: event.target.value.replaceAll(",", ".") }))}
|
||||
aria-label={textContent.itemTotalLabel || "Total"}
|
||||
textContent={textContent}
|
||||
title={textContent.itemTotalHelp || "Saisissez un nombre ou une formule avec base pour référencer la cible du parent."}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue