fix(editor): NodeEditorDialog — correct bracket-form path wrapping + focus trap on open

This commit is contained in:
Claude
2026-08-30 11:43:51 +00:00
parent 2649e09da9
commit abc71e481e
2 changed files with 72 additions and 1 deletions

View File

@@ -192,6 +192,37 @@ describe("NodeEditorDialog", () => {
expect(props.onClose).toHaveBeenCalled();
});
it("focuses the first form field on open", async () => {
renderDialog();
// jsdom autofocuses via .focus(); label textarea should be active.
const label = screen.getByLabelText(/node label/i);
expect(document.activeElement).toBe(label);
});
it("bracket-form top-level keys wrap without a stray dot separator", async () => {
server.use(
http.get("/api/workflows/:id/executions", () =>
HttpResponse.json([
{ id: "e1", workflowId: "wf1", trigger: "manual", status: "completed",
startedAt: "", endedAt: "", error: null } satisfies ExecutionSummary,
])),
http.get("/api/executions/e1", () =>
HttpResponse.json({
id: "e1", workflowId: "wf1", trigger: "manual", status: "completed",
startedAt: "", endedAt: "", error: null,
nodes: [
{ nodeId: "n1", status: "completed", startedAt: "", endedAt: "", error: null,
inputs: [[{ json: { "weird.key": "v" } }]], outputs: [] },
],
} satisfies ExecutionDetail)),
);
renderControlledDialog();
const body = await screen.findByLabelText(/^body/i) as HTMLTextAreaElement;
body.focus();
await userEvent.click(await screen.findByText("weird.key"));
expect(body.value).toBe(`{input["weird.key"]}`);
});
it("clicking a leaf in the Input tree inserts {input.<path>} at the focused parameter", async () => {
// Seed an execution so the input tree has a leaf.
server.use(

View File

@@ -39,6 +39,41 @@ export function NodeEditorDialog({
return () => document.removeEventListener("keydown", onKey);
}, [onClose]);
// Initial focus on mount — the first field inside the dialog body.
useEffect(() => {
const first = dialogRef.current?.querySelector<HTMLElement>(
"textarea, input, select"
);
first?.focus();
}, []);
// Focus trap: Tab / Shift+Tab cycle within the dialog. jsdom doesn't
// simulate real browser tab-order, so this behavior is intentionally
// exercised only in-browser — no unit test covers the wrap-around.
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key !== "Tab" || !dialogRef.current) return;
const focusables = Array.from(
dialogRef.current.querySelectorAll<HTMLElement>(
'textarea, input, select, button, [tabindex]:not([tabindex="-1"])'
),
).filter((el) => !el.hasAttribute("disabled"));
if (focusables.length === 0) return;
const first = focusables[0];
const last = focusables[focusables.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey && active === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && active === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, []);
const executions = useExecutions(workflowId);
const [pinnedExecutionId, setPinnedExecutionId] = useState<string | null>(null);
const eid = pinnedExecutionId ?? executions.data?.[0]?.id;
@@ -78,7 +113,12 @@ export function NodeEditorDialog({
focusedFieldRef.current = h;
}, []);
const insertPath = useCallback((path: string) => {
focusedFieldRef.current?.insertAtCaret(`{input.${path}}`);
// JsonTree emits identifier-rooted paths (`body.email`, `tags[1]`) or
// bracket-rooted paths (`["weird.key"]`, `[0].a`). Backend TemplateEngine
// grammar requires no separator before a leading `[` (i.e., `{input[0]}`,
// not `{input.[0]}`). Insert `.` only for identifier roots.
const sep = path.startsWith("[") ? "" : ".";
focusedFieldRef.current?.insertAtCaret(`{input${sep}${path}}`);
}, []);
const filteredConnections = connections.filter((c) => c.providerId === nt?.connectionProviderId);