Refactor goal tree editor form
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
a796edcb02
commit
d9bbf40fad
3 changed files with 255 additions and 169 deletions
|
|
@ -165,7 +165,9 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
const taskPlannerBundle = `${taskPlannerItems}\n${taskPlannerItemPanels}`;
|
const taskPlannerBundle = `${taskPlannerItems}\n${taskPlannerItemPanels}`;
|
||||||
const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8");
|
const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8");
|
||||||
const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8");
|
const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8");
|
||||||
|
const goalTreeEditor = await readFile("website/src/features/toolboxes/modules/GoalTreeEditor.jsx", "utf8");
|
||||||
const goalTreeParts = await readFile("website/src/features/toolboxes/modules/GoalTreeParts.jsx", "utf8");
|
const goalTreeParts = await readFile("website/src/features/toolboxes/modules/GoalTreeParts.jsx", "utf8");
|
||||||
|
const goalTreeBundle = `${goalTreeModule}\n${goalTreeEditor}\n${goalTreeParts}`;
|
||||||
const goalTreeDisplay = await readFile("website/src/features/toolboxes/modules/goalTreeDisplay.js", "utf8");
|
const goalTreeDisplay = await readFile("website/src/features/toolboxes/modules/goalTreeDisplay.js", "utf8");
|
||||||
const goalTreeUtils = await readFile("website/src/features/toolboxes/modules/goalTreeUtils.js", "utf8");
|
const goalTreeUtils = await readFile("website/src/features/toolboxes/modules/goalTreeUtils.js", "utf8");
|
||||||
const equipmentPlannerModule = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx", "utf8");
|
const equipmentPlannerModule = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx", "utf8");
|
||||||
|
|
@ -381,11 +383,15 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(goalTreeModule, /applyGoalTreeDueResets/);
|
assert.match(goalTreeModule, /applyGoalTreeDueResets/);
|
||||||
assert.match(goalTreeModule, /TextExchangeActions/);
|
assert.match(goalTreeModule, /TextExchangeActions/);
|
||||||
assert.match(goalTreeModule, /DiagramConnectors/);
|
assert.match(goalTreeModule, /DiagramConnectors/);
|
||||||
|
assert.match(goalTreeEditor, /export function GoalTreeEditor/);
|
||||||
|
assert.match(goalTreeEditor, /Tabs/);
|
||||||
|
assert.match(goalTreeEditor, /GoalTreeFormulaInput/);
|
||||||
|
assert.match(goalTreeEditor, /GoalTreePrerequisitePicker/);
|
||||||
assert.match(goalTreeParts, /Tooltip/);
|
assert.match(goalTreeParts, /Tooltip/);
|
||||||
assert.match(goalTreeParts, /export function GoalTreeIconPicker/);
|
assert.match(goalTreeParts, /export function GoalTreeIconPicker/);
|
||||||
assert.match(goalTreeParts, /export function GoalTreeContentEditor/);
|
assert.match(goalTreeParts, /export function GoalTreeContentEditor/);
|
||||||
assert.match(goalTreeParts, /function GoalTreeNodeContentPreview/);
|
assert.match(goalTreeParts, /function GoalTreeNodeContentPreview/);
|
||||||
assert.doesNotMatch(`${goalTreeModule}\n${goalTreeParts}`, /\.slice\(0, 3\)/);
|
assert.doesNotMatch(goalTreeBundle, /\.slice\(0, 3\)/);
|
||||||
assert.match(goalTreeParts, /GoalTreeChecklistInlineControl/);
|
assert.match(goalTreeParts, /GoalTreeChecklistInlineControl/);
|
||||||
assert.match(goalTreeParts, /GoalTreeStepLeafControl/);
|
assert.match(goalTreeParts, /GoalTreeStepLeafControl/);
|
||||||
assert.match(goalTreeParts, /export function GoalTreePrerequisitePicker/);
|
assert.match(goalTreeParts, /export function GoalTreePrerequisitePicker/);
|
||||||
|
|
|
||||||
227
website/src/features/toolboxes/modules/GoalTreeEditor.jsx
Normal file
227
website/src/features/toolboxes/modules/GoalTreeEditor.jsx
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
// Rôle : rend le formulaire d'édition des noeuds de l'arbre d'objectifs.
|
||||||
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { Tabs } from "../../../components/Tabs.jsx";
|
||||||
|
import {
|
||||||
|
GoalTreeContentEditor,
|
||||||
|
GoalTreeFormulaInput,
|
||||||
|
GoalTreeIconPicker,
|
||||||
|
GoalTreePrerequisitePicker,
|
||||||
|
GoalTreeTitleInput
|
||||||
|
} from "./GoalTreeParts.jsx";
|
||||||
|
import {
|
||||||
|
KIND_LABELS,
|
||||||
|
RESET_LABELS
|
||||||
|
} from "./goalTreeDisplay.js";
|
||||||
|
import {
|
||||||
|
GOAL_TREE_COLORS,
|
||||||
|
GOAL_TREE_COLOR_VALUES,
|
||||||
|
GOAL_TREE_KINDS,
|
||||||
|
GOAL_TREE_RESET_TYPES,
|
||||||
|
GOAL_TREE_WEEK_DAYS
|
||||||
|
} from "./goalTreeUtils.js";
|
||||||
|
|
||||||
|
export function GoalTreeEditor({
|
||||||
|
selectedNode,
|
||||||
|
selectedNodeIsStep,
|
||||||
|
selectedProgressMode,
|
||||||
|
formTabs,
|
||||||
|
activeFormTab,
|
||||||
|
evaluatedNodes,
|
||||||
|
textContent,
|
||||||
|
onTabChange,
|
||||||
|
onClose,
|
||||||
|
onDeleteNode,
|
||||||
|
onSetKind,
|
||||||
|
onUpdateNode,
|
||||||
|
onSetStepTarget,
|
||||||
|
onAddNodeItem,
|
||||||
|
onUpdateNodeItem,
|
||||||
|
onDeleteNodeItem,
|
||||||
|
onToggleLinkedParent
|
||||||
|
}) {
|
||||||
|
if (!selectedNode) {
|
||||||
|
return (
|
||||||
|
<section className="goal-tree-editor calculator-card" aria-label={textContent.editorTitle || "Édition de l'arbre d'objectifs"}>
|
||||||
|
<p className="goal-tree-empty">{textContent.emptySelection || "Sélectionnez ou créez un objectif."}</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="goal-tree-editor calculator-card" aria-label={textContent.editorTitle || "Édition de l'arbre d'objectifs"}>
|
||||||
|
<div className="goal-tree-selected-form">
|
||||||
|
<Tabs tabs={formTabs} activeTab={activeFormTab} onChange={onTabChange} className="goal-tree-form-tabs" />
|
||||||
|
|
||||||
|
{activeFormTab === "base" && (
|
||||||
|
<GoalTreeBasePanel
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
selectedNodeIsStep={selectedNodeIsStep}
|
||||||
|
textContent={textContent}
|
||||||
|
onSetKind={onSetKind}
|
||||||
|
onUpdateNode={onUpdateNode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeFormTab === "content" && !selectedNodeIsStep && (
|
||||||
|
<div className="goal-tree-form-panel">
|
||||||
|
<GoalTreeContentEditor
|
||||||
|
node={selectedNode}
|
||||||
|
textContent={textContent}
|
||||||
|
onAdd={() => onAddNodeItem(selectedNode.id)}
|
||||||
|
onUpdate={(itemId, updater) => onUpdateNodeItem(selectedNode.id, itemId, updater)}
|
||||||
|
onDelete={(itemId) => onDeleteNodeItem(selectedNode.id, itemId)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeFormTab === "content" && selectedNodeIsStep && (
|
||||||
|
<div className="goal-tree-form-panel">
|
||||||
|
<p className="goal-tree-content-hint">{textContent.stepContentUnavailable || "Les étapes n'ont pas de contenu interne."}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeFormTab === "advanced" && (
|
||||||
|
<GoalTreeAdvancedPanel
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
selectedNodeIsStep={selectedNodeIsStep}
|
||||||
|
selectedProgressMode={selectedProgressMode}
|
||||||
|
evaluatedNodes={evaluatedNodes}
|
||||||
|
textContent={textContent}
|
||||||
|
onUpdateNode={onUpdateNode}
|
||||||
|
onSetStepTarget={onSetStepTarget}
|
||||||
|
onToggleLinkedParent={onToggleLinkedParent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="goal-tree-form-actions">
|
||||||
|
<button type="button" onClick={onClose}>
|
||||||
|
<Icon name="close" />
|
||||||
|
<span>{textContent.closeButton || "Fermer"}</span>
|
||||||
|
</button>
|
||||||
|
<button className="danger" type="button" onClick={() => onDeleteNode(selectedNode.id)}>
|
||||||
|
<Icon name="trash" />
|
||||||
|
<span>{textContent.deleteButton || "Supprimer la branche"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GoalTreeBasePanel({ selectedNode, selectedNodeIsStep, textContent, onSetKind, onUpdateNode }) {
|
||||||
|
return (
|
||||||
|
<div className="goal-tree-form-panel">
|
||||||
|
<label>
|
||||||
|
<span>{textContent.kindLabel || "Type de l'objectif"}</span>
|
||||||
|
<select value={selectedNode.kind} onChange={(event) => onSetKind(selectedNode.id, event.target.value)}>
|
||||||
|
{GOAL_TREE_KINDS.map((kind) => <option key={kind} value={kind}>{textContent[`${kind}Kind`] || KIND_LABELS[kind]}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>{selectedNodeIsStep ? textContent.titleLabel || "Titre de l'objectif" : textContent.listTitleLabel || "Titre de la liste"}</span>
|
||||||
|
<GoalTreeTitleInput
|
||||||
|
key={selectedNode.id}
|
||||||
|
value={selectedNode.title}
|
||||||
|
onChange={(title) => onUpdateNode(selectedNode.id, (node) => ({ ...node, title }))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{selectedNodeIsStep && (
|
||||||
|
<>
|
||||||
|
<GoalTreeIconPicker
|
||||||
|
label={textContent.iconLabel || "Icône"}
|
||||||
|
value={selectedNode.icon || "cube"}
|
||||||
|
textContent={textContent}
|
||||||
|
onChange={(icon) => onUpdateNode(selectedNode.id, (node) => ({ ...node, icon }))}
|
||||||
|
/>
|
||||||
|
<div className="goal-tree-color-field">
|
||||||
|
<span>{textContent.iconColorLabel || "Couleur de l'icône"}</span>
|
||||||
|
<div className="goal-tree-color-swatches" role="radiogroup" aria-label={textContent.iconColorLabel || "Couleur de l'icône"}>
|
||||||
|
{GOAL_TREE_COLORS.map((color) => (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
className={`goal-tree-color-swatch ${selectedNode.color === color ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onUpdateNode(selectedNode.id, (node) => ({ ...node, color }))}
|
||||||
|
aria-pressed={selectedNode.color === color}
|
||||||
|
aria-label={textContent[`color${color}`] || color}
|
||||||
|
title={textContent[`color${color}`] || color}
|
||||||
|
style={{ "--goal-tree-swatch-color": GOAL_TREE_COLOR_VALUES[color] }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!selectedNodeIsStep && (
|
||||||
|
<p className="goal-tree-content-hint">{textContent.contentTabHint || "Le contenu se modifie dans l'onglet Contenu."}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GoalTreeAdvancedPanel({ selectedNode, selectedNodeIsStep, selectedProgressMode, evaluatedNodes, textContent, onUpdateNode, onSetStepTarget, onToggleLinkedParent }) {
|
||||||
|
return (
|
||||||
|
<div className="goal-tree-form-panel">
|
||||||
|
<label>
|
||||||
|
<span>{textContent.resetTypeLabel || "Type de reset"}</span>
|
||||||
|
<select value={selectedNode.resetType} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetType: event.target.value }))}>
|
||||||
|
{GOAL_TREE_RESET_TYPES.map((resetType) => <option key={resetType} value={resetType}>{textContent[`${resetType}Reset`] || RESET_LABELS[resetType]}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{selectedNode.resetType !== "none" && (
|
||||||
|
<label>
|
||||||
|
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
|
||||||
|
<input type="time" value={selectedNode.resetTime || "00:00"} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetTime: event.target.value || "00:00" }))} />
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{selectedNode.resetType === "weekly" && (
|
||||||
|
<label>
|
||||||
|
<span>{textContent.weeklyResetDayLabel || "Jour de reset"}</span>
|
||||||
|
<select value={selectedNode.weeklyResetDay ?? 1} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, weeklyResetDay: Number(event.target.value) }))}>
|
||||||
|
{GOAL_TREE_WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{selectedNodeIsStep && (
|
||||||
|
<>
|
||||||
|
<div className="goal-tree-form-section-title">{textContent.quantitySectionTitle || "Quantité"}</div>
|
||||||
|
<label>
|
||||||
|
<span>{textContent.targetQuantityLabel || "Cible"}</span>
|
||||||
|
<GoalTreeFormulaInput
|
||||||
|
aria-label={textContent.targetQuantityLabel || "Cible"}
|
||||||
|
hasParent={Boolean(selectedNode.parentId)}
|
||||||
|
onChange={(value) => onSetStepTarget(selectedNode.id, value)}
|
||||||
|
value={selectedNode.formula || selectedNode.qtyTarget}
|
||||||
|
placeholder={textContent.formulaPlaceholder || "base * 3"}
|
||||||
|
textContent={textContent}
|
||||||
|
title={textContent.formulaHelp || "Utilisez base pour référencer la cible du parent."}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="goal-tree-form-section-title">{textContent.progressSectionTitle || "Progression"}</div>
|
||||||
|
<button
|
||||||
|
className={`tool-split-scroll-toggle goal-tree-progress-mode-toggle ${selectedProgressMode === "auto" ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onUpdateNode(selectedNode.id, (node) => ({ ...node, progressMode: selectedProgressMode === "auto" ? "manual" : "auto" }))}
|
||||||
|
aria-pressed={selectedProgressMode === "auto"}
|
||||||
|
title={selectedProgressMode === "auto" ? textContent.autoProgressTitle || "Progression automatique" : textContent.manualProgressTitle || "Mise à jour manuelle"}
|
||||||
|
>
|
||||||
|
<Icon name={selectedProgressMode === "auto" ? "calculator" : "edit"} />
|
||||||
|
<span>{selectedProgressMode === "auto" ? textContent.autoProgressLabel || "Automatique" : textContent.manualProgressLabel || "Manuelle"}</span>
|
||||||
|
<i aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<p className="goal-tree-progress-mode-hint">
|
||||||
|
{selectedProgressMode === "auto"
|
||||||
|
? textContent.autoProgressHint || "Calcule la progression depuis les enfants."
|
||||||
|
: textContent.manualProgressHint || "Affiche une checkbox ou une quantité à mettre à jour directement."}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<GoalTreePrerequisitePicker
|
||||||
|
nodes={evaluatedNodes}
|
||||||
|
node={selectedNode}
|
||||||
|
textContent={textContent}
|
||||||
|
onToggle={(parentId) => onToggleLinkedParent(selectedNode.id, parentId)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,22 +2,13 @@
|
||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { DiagramConnectors } from "../../../components/DiagramConnectors.jsx";
|
import { DiagramConnectors } from "../../../components/DiagramConnectors.jsx";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { Tabs } from "../../../components/Tabs.jsx";
|
|
||||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
|
import { GoalTreeEditor } from "./GoalTreeEditor.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||||
import {
|
import { GoalTreeBranch } from "./GoalTreeParts.jsx";
|
||||||
GoalTreeBranch,
|
|
||||||
GoalTreeContentEditor,
|
|
||||||
GoalTreeFormulaInput,
|
|
||||||
GoalTreeIconPicker,
|
|
||||||
GoalTreePrerequisitePicker,
|
|
||||||
GoalTreeTitleInput
|
|
||||||
} from "./GoalTreeParts.jsx";
|
|
||||||
import {
|
import {
|
||||||
GOAL_TREE_VIEW_SCALE,
|
GOAL_TREE_VIEW_SCALE,
|
||||||
KIND_ICONS,
|
KIND_ICONS,
|
||||||
KIND_LABELS,
|
|
||||||
RESET_LABELS,
|
|
||||||
getDefaultGoalTreeItem,
|
getDefaultGoalTreeItem,
|
||||||
getEffectiveGoalTreeProgressMode,
|
getEffectiveGoalTreeProgressMode,
|
||||||
getGoalTreeConvergenceKeepOpenIds,
|
getGoalTreeConvergenceKeepOpenIds,
|
||||||
|
|
@ -26,11 +17,6 @@ import {
|
||||||
isSingleValueGoalTreeNode
|
isSingleValueGoalTreeNode
|
||||||
} from "./goalTreeDisplay.js";
|
} from "./goalTreeDisplay.js";
|
||||||
import {
|
import {
|
||||||
GOAL_TREE_COLORS,
|
|
||||||
GOAL_TREE_COLOR_VALUES,
|
|
||||||
GOAL_TREE_KINDS,
|
|
||||||
GOAL_TREE_RESET_TYPES,
|
|
||||||
GOAL_TREE_WEEK_DAYS,
|
|
||||||
applyGoalTreeDueResets,
|
applyGoalTreeDueResets,
|
||||||
evaluateGoalTreeNodes,
|
evaluateGoalTreeNodes,
|
||||||
getGoalTreeChildren,
|
getGoalTreeChildren,
|
||||||
|
|
@ -329,158 +315,25 @@ export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditi
|
||||||
return (
|
return (
|
||||||
<div className={`goal-tree-module ${editing ? "" : "is-view-only"}`}>
|
<div className={`goal-tree-module ${editing ? "" : "is-view-only"}`}>
|
||||||
{editing && (
|
{editing && (
|
||||||
<section className="goal-tree-editor calculator-card" aria-label={textContent.editorTitle || "Édition de l'arbre d'objectifs"}>
|
<GoalTreeEditor
|
||||||
{selectedNode ? (
|
selectedNode={selectedNode}
|
||||||
<div className="goal-tree-selected-form">
|
selectedNodeIsStep={selectedNodeIsStep}
|
||||||
<Tabs tabs={formTabs} activeTab={activeFormTab} onChange={setActiveFormTab} className="goal-tree-form-tabs" />
|
selectedProgressMode={selectedProgressMode}
|
||||||
|
formTabs={formTabs}
|
||||||
{activeFormTab === "base" && (
|
activeFormTab={activeFormTab}
|
||||||
<div className="goal-tree-form-panel">
|
evaluatedNodes={evaluatedNodes}
|
||||||
<label>
|
|
||||||
<span>{textContent.kindLabel || "Type de l'objectif"}</span>
|
|
||||||
<select value={selectedNode.kind} onChange={(event) => setKind(selectedNode.id, event.target.value)}>
|
|
||||||
{GOAL_TREE_KINDS.map((kind) => <option key={kind} value={kind}>{textContent[`${kind}Kind`] || KIND_LABELS[kind]}</option>)}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>{selectedNodeIsStep ? textContent.titleLabel || "Titre de l'objectif" : textContent.listTitleLabel || "Titre de la liste"}</span>
|
|
||||||
<GoalTreeTitleInput
|
|
||||||
key={selectedNode.id}
|
|
||||||
value={selectedNode.title}
|
|
||||||
onChange={(title) => updateNode(selectedNode.id, (node) => ({ ...node, title }))}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{selectedNodeIsStep && (
|
|
||||||
<>
|
|
||||||
<GoalTreeIconPicker
|
|
||||||
label={textContent.iconLabel || "Icône"}
|
|
||||||
value={selectedNode.icon || "cube"}
|
|
||||||
textContent={textContent}
|
textContent={textContent}
|
||||||
onChange={(icon) => updateNode(selectedNode.id, (node) => ({ ...node, icon }))}
|
onTabChange={setActiveFormTab}
|
||||||
|
onClose={() => setEditing(false)}
|
||||||
|
onDeleteNode={deleteNode}
|
||||||
|
onSetKind={setKind}
|
||||||
|
onUpdateNode={updateNode}
|
||||||
|
onSetStepTarget={setStepTarget}
|
||||||
|
onAddNodeItem={addNodeItem}
|
||||||
|
onUpdateNodeItem={updateNodeItem}
|
||||||
|
onDeleteNodeItem={deleteNodeItem}
|
||||||
|
onToggleLinkedParent={toggleLinkedParent}
|
||||||
/>
|
/>
|
||||||
<div className="goal-tree-color-field">
|
|
||||||
<span>{textContent.iconColorLabel || "Couleur de l'icône"}</span>
|
|
||||||
<div className="goal-tree-color-swatches" role="radiogroup" aria-label={textContent.iconColorLabel || "Couleur de l'icône"}>
|
|
||||||
{GOAL_TREE_COLORS.map((color) => (
|
|
||||||
<button
|
|
||||||
key={color}
|
|
||||||
className={`goal-tree-color-swatch ${selectedNode.color === color ? "active" : ""}`}
|
|
||||||
type="button"
|
|
||||||
onClick={() => updateNode(selectedNode.id, (node) => ({ ...node, color }))}
|
|
||||||
aria-pressed={selectedNode.color === color}
|
|
||||||
aria-label={textContent[`color${color}`] || color}
|
|
||||||
title={textContent[`color${color}`] || color}
|
|
||||||
style={{ "--goal-tree-swatch-color": GOAL_TREE_COLOR_VALUES[color] }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!selectedNodeIsStep && (
|
|
||||||
<p className="goal-tree-content-hint">{textContent.contentTabHint || "Le contenu se modifie dans l'onglet Contenu."}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeFormTab === "content" && !selectedNodeIsStep && (
|
|
||||||
<div className="goal-tree-form-panel">
|
|
||||||
<GoalTreeContentEditor
|
|
||||||
node={selectedNode}
|
|
||||||
textContent={textContent}
|
|
||||||
onAdd={() => addNodeItem(selectedNode.id)}
|
|
||||||
onUpdate={(itemId, updater) => updateNodeItem(selectedNode.id, itemId, updater)}
|
|
||||||
onDelete={(itemId) => deleteNodeItem(selectedNode.id, itemId)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeFormTab === "content" && selectedNodeIsStep && (
|
|
||||||
<div className="goal-tree-form-panel">
|
|
||||||
<p className="goal-tree-content-hint">{textContent.stepContentUnavailable || "Les étapes n'ont pas de contenu interne."}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeFormTab === "advanced" && (
|
|
||||||
<div className="goal-tree-form-panel">
|
|
||||||
<label>
|
|
||||||
<span>{textContent.resetTypeLabel || "Type de reset"}</span>
|
|
||||||
<select value={selectedNode.resetType} onChange={(event) => updateNode(selectedNode.id, (node) => ({ ...node, resetType: event.target.value }))}>
|
|
||||||
{GOAL_TREE_RESET_TYPES.map((resetType) => <option key={resetType} value={resetType}>{textContent[`${resetType}Reset`] || RESET_LABELS[resetType]}</option>)}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
{selectedNode.resetType !== "none" && (
|
|
||||||
<label>
|
|
||||||
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
|
|
||||||
<input type="time" value={selectedNode.resetTime || "00:00"} onChange={(event) => updateNode(selectedNode.id, (node) => ({ ...node, resetTime: event.target.value || "00:00" }))} />
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
{selectedNode.resetType === "weekly" && (
|
|
||||||
<label>
|
|
||||||
<span>{textContent.weeklyResetDayLabel || "Jour de reset"}</span>
|
|
||||||
<select value={selectedNode.weeklyResetDay ?? 1} onChange={(event) => updateNode(selectedNode.id, (node) => ({ ...node, weeklyResetDay: Number(event.target.value) }))}>
|
|
||||||
{GOAL_TREE_WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
{selectedNodeIsStep && (
|
|
||||||
<>
|
|
||||||
<div className="goal-tree-form-section-title">{textContent.quantitySectionTitle || "Quantité"}</div>
|
|
||||||
<label>
|
|
||||||
<span>{textContent.targetQuantityLabel || "Cible"}</span>
|
|
||||||
<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"}
|
|
||||||
textContent={textContent}
|
|
||||||
title={textContent.formulaHelp || "Utilisez base pour référencer la cible du parent."}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className="goal-tree-form-section-title">{textContent.progressSectionTitle || "Progression"}</div>
|
|
||||||
<button
|
|
||||||
className={`tool-split-scroll-toggle goal-tree-progress-mode-toggle ${selectedProgressMode === "auto" ? "active" : ""}`}
|
|
||||||
type="button"
|
|
||||||
onClick={() => updateNode(selectedNode.id, (node) => ({ ...node, progressMode: selectedProgressMode === "auto" ? "manual" : "auto" }))}
|
|
||||||
aria-pressed={selectedProgressMode === "auto"}
|
|
||||||
title={selectedProgressMode === "auto" ? textContent.autoProgressTitle || "Progression automatique" : textContent.manualProgressTitle || "Mise à jour manuelle"}
|
|
||||||
>
|
|
||||||
<Icon name={selectedProgressMode === "auto" ? "calculator" : "edit"} />
|
|
||||||
<span>{selectedProgressMode === "auto" ? textContent.autoProgressLabel || "Automatique" : textContent.manualProgressLabel || "Manuelle"}</span>
|
|
||||||
<i aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<p className="goal-tree-progress-mode-hint">
|
|
||||||
{selectedProgressMode === "auto"
|
|
||||||
? textContent.autoProgressHint || "Calcule la progression depuis les enfants."
|
|
||||||
: textContent.manualProgressHint || "Affiche une checkbox ou une quantité à mettre à jour directement."}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<GoalTreePrerequisitePicker
|
|
||||||
nodes={evaluatedNodes}
|
|
||||||
node={selectedNode}
|
|
||||||
textContent={textContent}
|
|
||||||
onToggle={(parentId) => toggleLinkedParent(selectedNode.id, parentId)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="goal-tree-form-actions">
|
|
||||||
<button type="button" onClick={() => setEditing(false)}>
|
|
||||||
<Icon name="close" />
|
|
||||||
<span>{textContent.closeButton || "Fermer"}</span>
|
|
||||||
</button>
|
|
||||||
<button className="danger" type="button" onClick={() => deleteNode(selectedNode.id)}>
|
|
||||||
<Icon name="trash" />
|
|
||||||
<span>{textContent.deleteButton || "Supprimer la branche"}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="goal-tree-empty">{textContent.emptySelection || "Sélectionnez ou créez un objectif."}</p>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
{textExchangeOpen && (
|
||||||
<div className="module-text-exchange-panel">
|
<div className="module-text-exchange-panel">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue