fix(editor): NodeEditorDialog — correct bracket-form path wrapping + focus trap on open
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user