From 432782aeb1cae69d514f0681dbae13c9f45483ec Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 14:37:19 +0000 Subject: [PATCH] =?UTF-8?q?refactor(editor):=20PinnedDataSection=20?= =?UTF-8?q?=E2=80=94=20useId=20select,=20side-aware=20placeholder,=20exhau?= =?UTF-8?q?stive-deps=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/editor/PinnedDataSection.tsx | 35 +++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/frontend/src/editor/PinnedDataSection.tsx b/frontend/src/editor/PinnedDataSection.tsx index b69e781..aad6f24 100644 --- a/frontend/src/editor/PinnedDataSection.tsx +++ b/frontend/src/editor/PinnedDataSection.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useId, useState } from "react"; import { useExecution, useExecutions } from "@/api/hooks"; import type { NodeExecutionDto } from "@/api/types"; @@ -17,17 +17,21 @@ export function PinnedDataSection({ const [text, setText] = useState(() => stringifyOrEmpty(value)); const [error, setError] = useState(null); const [loadEid, setLoadEid] = useState(""); + const selectId = useId(); // If the parent's value changes while we're not editing (e.g. after a // load-from-execution or a Clear), keep the textarea in sync. We only // re-sync when the current text is invalid (let parse-error stand) OR the // current text parses to a different value than the parent's. + // Depending on `text` here would create a self-triggering loop; we + // rely on `safeParse`'s tri-state guard instead. + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { const parsedCurrent = safeParse(text); if (parsedCurrent === undefined) return; // parse-error state — don't overwrite if (canonicalize(parsedCurrent) === canonicalize(value)) return; setText(stringifyOrEmpty(value)); - }, [value]); // deliberately not depending on `text` + }, [value]); function commit(next: string) { setText(next); @@ -58,6 +62,15 @@ export function PinnedDataSection({ // Once the selected execution's detail arrives, splice its inputs[0] / // outputs[0] for our node into the textarea. + // Deps omit `nodeId`, `side`, and `commit` deliberately: + // - `nodeId` and `side` are effectively constant per mount (dialog owns + // one node at a time). + // - `commit` closes over `onChange`/`setText`/`setError` — all stable + // setters, and we WANT the current onChange, so re-capturing per + // render is fine. + // - Including any of them would cause the effect to re-fire on + // unrelated re-renders and clobber user edits. + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { if (!loadEid || !loadedExec.data) return; const row = loadedExec.data.nodes.find((n: NodeExecutionDto) => n.nodeId === nodeId); @@ -75,11 +88,11 @@ export function PinnedDataSection({
{title}
-