Preserve memo drafts and stabilize goal tree selection
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s

This commit is contained in:
Shinuwa 2026-09-11 13:58:00 +02:00
parent b9cc77a4d1
commit 4f644ca116
8 changed files with 158 additions and 49 deletions

View file

@ -0,0 +1,29 @@
// Rôle : conserve le brouillon d'un mémo et ajuste la hauteur de son champ.
import { useLayoutEffect } from "react";
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
export function MemoTextEditor({ text, textContent, onCommit }) {
const { draft, setDraft, inputRef, commit } = useInlineEdit({ value: text, onCommit, commitUnchanged: true });
useLayoutEffect(() => {
const element = inputRef.current;
if (!element) return;
element.style.height = "auto";
element.style.height = `${element.scrollHeight + 2}px`;
});
return (
<textarea
className="nds-textarea"
data-size="sm"
data-variant="ghost"
ref={inputRef}
value={draft}
rows={3}
placeholder={textContent.placeholder || "Mémo court..."}
onChange={(event) => setDraft(event.target.value)}
onBlur={() => commit()}
aria-label={textContent.editLabel || "Modifier le mémo"}
/>
);
}

View file

@ -2,7 +2,8 @@
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
import { Icon } from "../../../components/Icon.jsx";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useMemo } from "react";
import { MemoTextEditor } from "./MemoTextEditor.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { TEXT_COLORS } from "./notepadUtils.js";
@ -11,8 +12,6 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
const data = useMemo(() => context.normalizeMemosData(storedData), [context, storedData]);
const textContent = context.moduleText?.memos || {};
const tickerItems = data.tickerEnabled ? data.items : [];
const textareaRefs = useRef(new Map());
const [drafts, setDrafts] = useState({});
const {
itemReorder,
getItemProps,
@ -29,20 +28,6 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
hierarchy: { enabled: true, stickyParents: true }
});
useEffect(() => {
setDrafts(Object.fromEntries(data.items.map((item) => [item.id, item.text])));
}, [data.items]);
useLayoutEffect(() => {
data.items.forEach((item) => resizeMemoTextarea(textareaRefs.current.get(item.id)));
}, [data.items, drafts]);
function resizeMemoTextarea(element) {
if (!element) return;
element.style.height = "auto";
element.style.height = `${element.scrollHeight + 2}px`;
}
function save(nextData) {
context.setModuleData(toolboxId, moduleId, nextData, "memos");
}
@ -74,8 +59,8 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
save({ ...data, items: data.items.filter((item) => item.id !== memoId) });
}
function commitMemoText(memoId) {
const nextText = String(drafts[memoId] || "").trim();
function commitMemoText(memoId, draft) {
const nextText = String(draft || "").trim();
if (!nextText) {
deleteMemo(memoId);
return;
@ -146,27 +131,10 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
>
<Icon name="drag" />
</button>
<textarea
className="nds-textarea"
data-size="sm"
data-variant="ghost"
ref={(element) => {
if (element) {
textareaRefs.current.set(memo.id, element);
resizeMemoTextarea(element);
} else {
textareaRefs.current.delete(memo.id);
}
}}
value={drafts[memo.id] ?? memo.text}
rows={3}
placeholder={textContent.placeholder || "Mémo court..."}
onChange={(event) => {
resizeMemoTextarea(event.currentTarget);
setDrafts((current) => ({ ...current, [memo.id]: event.target.value }));
}}
onBlur={() => commitMemoText(memo.id)}
aria-label={textContent.editLabel || "Modifier le mémo"}
<MemoTextEditor
text={memo.text}
textContent={textContent}
onCommit={(draft) => commitMemoText(memo.id, draft)}
/>
<footer>
<CompactDropdown

View file

@ -34,6 +34,9 @@ export function useGoalTreeController({ toolboxId, moduleId, context, editing, s
const [activeFormTab, setActiveFormTab] = useState("base");
const selectedNode = evaluatedNodes.find((node) => node.id === selectedNodeId) || evaluatedNodes[0] || null;
const selectedNodeIsStep = selectedNode?.kind === "step";
const nextSelectedNodeId = selectedNode?.id || "";
if (selectedNodeId !== nextSelectedNodeId) setSelectedNodeId(nextSelectedNodeId);
if (activeFormTab === "content" && selectedNodeIsStep) setActiveFormTab("base");
const selectedNodeChildren = selectedNode ? getGoalTreeChildren(evaluatedNodes, selectedNode.id) : [];
const selectedProgressMode = selectedNode ? getEffectiveGoalTreeProgressMode(selectedNode, selectedNodeChildren) : "auto";
const formTabs = [
@ -60,15 +63,6 @@ export function useGoalTreeController({ toolboxId, moduleId, context, editing, s
if (resetData !== data) save(resetData);
}, [data, save]);
useEffect(() => {
if (!selectedNodeId && data.nodes[0]) setSelectedNodeId(data.nodes[0].id);
if (selectedNodeId && !data.nodes.some((node) => node.id === selectedNodeId)) setSelectedNodeId(data.nodes[0]?.id || "");
}, [data.nodes, selectedNodeId]);
useEffect(() => {
if (activeFormTab === "content" && selectedNodeIsStep) setActiveFormTab("base");
}, [activeFormTab, selectedNodeIsStep]);
function addNode(parentId = "") {
const parent = data.nodes.find((node) => node.id === parentId);
const node = {