From 541df01684c53964d51b6f86315291333587cb5a Mon Sep 17 00:00:00 2001 From: mptyl Date: Thu, 2 Jul 2026 18:24:02 +0200 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20'Other=20=E2=80=94=20specify'?= =?UTF-8?q?=20opens=20a=20text=20field=20and=20emits=20control:freetext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/widgets/ArtifactGateWidget.tsx | 2 +- frontend/src/widgets/MultiselectWidget.tsx | 2 +- .../src/widgets/ReservedControls.test.tsx | 23 ++++++++++ frontend/src/widgets/ReservedControls.tsx | 43 +++++++++++++++++-- frontend/src/widgets/SelectWidget.tsx | 2 +- 5 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 frontend/src/widgets/ReservedControls.test.tsx diff --git a/frontend/src/widgets/ArtifactGateWidget.tsx b/frontend/src/widgets/ArtifactGateWidget.tsx index 7770fd84..641ac6b0 100644 --- a/frontend/src/widgets/ArtifactGateWidget.tsx +++ b/frontend/src/widgets/ArtifactGateWidget.tsx @@ -53,7 +53,7 @@ export function ArtifactGateWidget({ descriptor, onRespond }: WidgetProps) { onRespond({ id: descriptor.id, control: c })} + onControl={(c, t) => onRespond({ id: descriptor.id, control: c, ...(t !== undefined ? { text: t } : {}) })} /> ); diff --git a/frontend/src/widgets/MultiselectWidget.tsx b/frontend/src/widgets/MultiselectWidget.tsx index 66b608c1..afecd30f 100644 --- a/frontend/src/widgets/MultiselectWidget.tsx +++ b/frontend/src/widgets/MultiselectWidget.tsx @@ -57,7 +57,7 @@ export function MultiselectWidget({ descriptor, onRespond }: WidgetProps) { onRespond({ id: descriptor.id, control: c })} + onControl={(c, t) => onRespond({ id: descriptor.id, control: c, ...(t !== undefined ? { text: t } : {}) })} /> ); diff --git a/frontend/src/widgets/ReservedControls.test.tsx b/frontend/src/widgets/ReservedControls.test.tsx new file mode 100644 index 00000000..e607e7ec --- /dev/null +++ b/frontend/src/widgets/ReservedControls.test.tsx @@ -0,0 +1,23 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { ReservedControls } from "./ReservedControls"; + +test("back and exit fire immediately with no text", async () => { + const onControl = vi.fn(); + render(); + await userEvent.click(screen.getByRole("button", { name: /go back/i })); + expect(onControl).toHaveBeenCalledWith("back"); + await userEvent.click(screen.getByRole("button", { name: /^exit$/i })); + expect(onControl).toHaveBeenCalledWith("exit"); +}); + +test("other reveals a textarea and emits freetext with the typed text", async () => { + const onControl = vi.fn(); + render(); + await userEvent.click(screen.getByRole("button", { name: /other — specify/i })); + // clicking Other does NOT emit a control yet — it reveals the input + expect(onControl).not.toHaveBeenCalled(); + await userEvent.type(screen.getByRole("textbox"), "usa la tabella X"); + await userEvent.click(screen.getByRole("button", { name: /send/i })); + expect(onControl).toHaveBeenCalledWith("freetext", "usa la tabella X"); +}); diff --git a/frontend/src/widgets/ReservedControls.tsx b/frontend/src/widgets/ReservedControls.tsx index 7311733a..7fd37e7f 100644 --- a/frontend/src/widgets/ReservedControls.tsx +++ b/frontend/src/widgets/ReservedControls.tsx @@ -1,7 +1,42 @@ +import { useState } from "react"; + const LABELS: Record = { back: "Go back", exit: "Exit", other: "Other — specify" }; -export function ReservedControls({ reserved, onControl }: { reserved?: string[]; onControl: (c: string) => void }) { + +export function ReservedControls({ + reserved, + onControl, +}: { + reserved?: string[]; + onControl: (c: string, text?: string) => void; +}) { + const [otherOpen, setOtherOpen] = useState(false); + const [text, setText] = useState(""); if (!reserved?.length) return null; - return
{reserved.map((c) => ( - - ))}
; + return ( +
+
+ {reserved.map((c) => ( + + ))} +
+ {otherOpen && ( +
+