From 16d9e68b230a60c3da5bd31b516da02f156313f3 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Sat, 16 May 2026 01:27:13 +0530 Subject: [PATCH 1/6] fix(builder): replace live previews with icon palette to kill autofill --- .../builder/sidebar/AddFieldsSection.vue | 37 ++++++-------- frontend/src/config/fieldTypes.ts | 48 +++++++++++++++++++ 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/builder/sidebar/AddFieldsSection.vue b/frontend/src/components/builder/sidebar/AddFieldsSection.vue index 3ae838d..fb0f986 100644 --- a/frontend/src/components/builder/sidebar/AddFieldsSection.vue +++ b/frontend/src/components/builder/sidebar/AddFieldsSection.vue @@ -2,21 +2,14 @@ import { ref, computed } from "vue"; import { formFields, FormFields } from "@/utils/form_fields"; import { Fieldtype } from "@/types/formfield"; -import { FormControl, Button } from "frappe-ui"; +import { FormControl } from "frappe-ui"; import { useEditForm } from "@/stores/editForm"; -import RenderField from "@/components/RenderField.vue"; -import type { Component } from "vue"; const search = ref(""); -const componentMap = formFields.reduce((acc: Record, field: FormFields) => { - acc[field.name] = field.component; - return acc; -}, {}); -const filteredComponents = computed(() => { - return Object.keys(componentMap).filter((component) => - component.toLowerCase().includes(search.value.toLowerCase()) - ); +const filteredFields = computed(() => { + const q = search.value.toLowerCase(); + return formFields.filter((field: FormFields) => field.name.toLowerCase().includes(q)); }); const editFormStore = useEditForm(); @@ -31,19 +24,17 @@ const editFormStore = useEditForm(); variant="outline" placeholder="Search Fields" /> -
-
+
+ + {{ field.name }} +
diff --git a/frontend/src/config/fieldTypes.ts b/frontend/src/config/fieldTypes.ts index 59a9a2c..0e93a23 100644 --- a/frontend/src/config/fieldTypes.ts +++ b/frontend/src/config/fieldTypes.ts @@ -33,6 +33,30 @@ import { TimePicker, FormControl, } from "frappe-ui"; +import { + AlignLeft, + AtSign, + Calendar, + CalendarClock, + CalendarRange, + Clock, + Hash, + Heading1, + Heading2, + Heading3, + KeyRound, + Link2, + List, + ListChecks, + Paperclip, + Phone as PhoneIcon, + Pilcrow, + SquareCheck, + Star, + Table as TableIcon, + ToggleRight, + Type, +} from "@lucide/vue"; import Attachment from "@/components/fields/Attachment.vue"; import Heading from "@/components/fields/Heading.vue"; import Multiselect from "@/components/fields/multiselect/Multiselect.vue"; @@ -63,6 +87,8 @@ export type FieldTypeDefinition = { name: Fieldtype; /** Vue component used to render this field in input mode */ component: Component; + /** Lucide icon used in the Add Fields palette */ + icon: Component; /** Default props forwarded to the component */ props: Record; /** How FieldRenderer lays out the label relative to the input */ @@ -87,6 +113,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.ATTACH, component: Attachment, + icon: Paperclip, props: { variant: "outline", filetypes: ["image/*", ".jpg", ".gif", ".pdf"], @@ -99,6 +126,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.DATA, component: FormControl, + icon: Type, props: { type: "text", variant: "outline" }, layout: "default", frappeFieldtype: "Data", @@ -108,6 +136,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.NUMBER, component: FormControl, + icon: Hash, props: { type: "number", variant: "outline" }, layout: "default", frappeFieldtype: "Int", @@ -117,6 +146,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.EMAIL, component: FormControl, + icon: AtSign, props: { type: "email", variant: "outline" }, layout: "default", frappeFieldtype: "Data", @@ -127,6 +157,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.DATE, component: DatePicker, + icon: Calendar, props: { variant: "outline", clearable: true, format: "D MMM YYYY" }, layout: "default", frappeFieldtype: "Date", @@ -136,6 +167,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.DATE_TIME, component: DateTimePicker, + icon: CalendarClock, props: { format: "DD MMM YYYY, hh:mm A", clearable: true, @@ -149,6 +181,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.DATE_RANGE, component: DateRangePicker, + icon: CalendarRange, props: { clearable: true, variant: "outline", format: "DD MMM 'YY" }, layout: "default", frappeFieldtype: "Data", @@ -158,6 +191,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.TIME_PICKER, component: TimePicker, + icon: Clock, props: { variant: "outline", use12Hour: true, clearable: true }, layout: "default", frappeFieldtype: "Time", @@ -167,6 +201,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.PASSWORD, component: Password, + icon: KeyRound, props: { variant: "outline" }, layout: "default", frappeFieldtype: "Password", @@ -176,6 +211,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.SELECT, component: Select, + icon: List, props: { variant: "outline" }, layout: "default", frappeFieldtype: "Select", @@ -185,6 +221,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.PHONE, component: Phone, + icon: PhoneIcon, props: { variant: "outline" }, layout: "default", frappeFieldtype: "Phone", @@ -194,6 +231,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.SWITCH, component: Switch, + icon: ToggleRight, props: {}, layout: "inline", frappeFieldtype: "Check", @@ -203,6 +241,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.TEXTAREA, component: Textarea, + icon: AlignLeft, props: { variant: "outline" }, layout: "default", frappeFieldtype: "Text", @@ -212,6 +251,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.TEXT_EDITOR, component: TextEditor, + icon: Pilcrow, props: { editorClass: "bg-surface-white w-full rounded-b form-description border rounded-b min-h-24", @@ -227,6 +267,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.LINK, component: Select, + icon: Link2, props: { variant: "outline" }, layout: "default", frappeFieldtype: "Link", @@ -236,6 +277,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.CHECKBOX, component: Checkbox, + icon: SquareCheck, props: {}, layout: "inline", frappeFieldtype: "Check", @@ -245,6 +287,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.RATING, component: Rating, + icon: Star, props: {}, layout: "default", frappeFieldtype: "Rating", @@ -254,6 +297,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.TABLE, component: Table, + icon: TableIcon, props: { options: { emptyState: { @@ -270,6 +314,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.MULTISELECT, component: Multiselect, + icon: ListChecks, props: {}, layout: "description-first", frappeFieldtype: "JSON", @@ -280,6 +325,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.HEADING_1, component: Heading, + icon: Heading1, props: {}, layout: "heading", frappeFieldtype: "HTML", @@ -289,6 +335,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.HEADING_2, component: Heading, + icon: Heading2, props: {}, layout: "heading", frappeFieldtype: "HTML", @@ -298,6 +345,7 @@ export const FIELD_TYPE_DEFINITIONS: FieldTypeDefinition[] = [ { name: Fieldtype.HEADING_3, component: Heading, + icon: Heading3, props: {}, layout: "heading", frappeFieldtype: "HTML", From 2745f623c1230d4975ae9ed61e6c1cf945b9f203 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Sat, 16 May 2026 01:27:25 +0530 Subject: [PATCH 2/6] test(e2e): cover Add Fields palette layout, search, and add-to-canvas --- frontend/e2e/helpers/form-builder.ts | 9 +- frontend/e2e/specs/add-fields-palette.spec.ts | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 frontend/e2e/specs/add-fields-palette.spec.ts diff --git a/frontend/e2e/helpers/form-builder.ts b/frontend/e2e/helpers/form-builder.ts index fcbd4b1..d5af874 100644 --- a/frontend/e2e/helpers/form-builder.ts +++ b/frontend/e2e/helpers/form-builder.ts @@ -266,12 +266,9 @@ export class FormBuilderPage { } async addField(fieldType: string) { - // Each card shows the field type name as visible text - const card = this.sidebar() - .getByText(fieldType, { exact: true }) - .locator(".."); - await card.hover(); - await card.getByRole("button").click(); + await this.sidebar() + .getByRole("button", { name: fieldType, exact: true }) + .click(); } // The canvas shows "Click on fields to add them…" when empty diff --git a/frontend/e2e/specs/add-fields-palette.spec.ts b/frontend/e2e/specs/add-fields-palette.spec.ts new file mode 100644 index 0000000..03c65e5 --- /dev/null +++ b/frontend/e2e/specs/add-fields-palette.spec.ts @@ -0,0 +1,87 @@ +import { test, expect } from "../fixtures/test-data.fixture"; +import { FormBuilderPage } from "../helpers/form-builder"; + +test.describe("Add Fields palette", () => { + test("renders palette items as buttons without live input previews", async ({ + page, + createForm, + }) => { + const formId = await createForm(); + const builder = new FormBuilderPage(page); + await builder.goto(formId); + + const sidebar = page.locator( + '[data-form-builder-component="form-builder-sidebar"]' + ); + + // Palette items are buttons, named after the fieldtype + await expect( + sidebar.getByRole("button", { name: "Data", exact: true }) + ).toBeVisible(); + await expect( + sidebar.getByRole("button", { name: "Email", exact: true }) + ).toBeVisible(); + await expect( + sidebar.getByRole("button", { name: "Phone", exact: true }) + ).toBeVisible(); + + // No autofillable inputs inside palette buttons (regression: previously + // each palette card mounted a live FormControl, which triggered browser + // autofill on type=email / type=tel / type=password). + const emailBtn = sidebar.getByRole("button", { + name: "Email", + exact: true, + }); + await expect(emailBtn.locator("input")).toHaveCount(0); + + const phoneBtn = sidebar.getByRole("button", { + name: "Phone", + exact: true, + }); + await expect(phoneBtn.locator("input")).toHaveCount(0); + }); + + test("search filters palette items", async ({ page, createForm }) => { + const formId = await createForm(); + const builder = new FormBuilderPage(page); + await builder.goto(formId); + + const sidebar = page.locator( + '[data-form-builder-component="form-builder-sidebar"]' + ); + + await expect( + sidebar.getByRole("button", { name: "Data", exact: true }) + ).toBeVisible(); + await expect( + sidebar.getByRole("button", { name: "Phone", exact: true }) + ).toBeVisible(); + + await sidebar.getByPlaceholder("Search Fields").fill("date"); + + // "Date", "Date Time", "Date Range" remain (case-insensitive substring) + await expect( + sidebar.getByRole("button", { name: "Date", exact: true }) + ).toBeVisible(); + // Unrelated types are filtered out + await expect( + sidebar.getByRole("button", { name: "Phone", exact: true }) + ).toHaveCount(0); + await expect( + sidebar.getByRole("button", { name: "Data", exact: true }) + ).toHaveCount(0); + }); + + test("clicking a palette item adds the field to the canvas", async ({ + page, + createForm, + }) => { + const formId = await createForm(); + const builder = new FormBuilderPage(page); + await builder.goto(formId); + + await expect(builder.canvasEmptyState()).toBeVisible(); + await builder.addField("Email"); + await expect(builder.canvasEmptyState()).not.toBeVisible(); + }); +}); From 9cbabe0eebc445041ce0c96c2435397314873069 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Sat, 16 May 2026 01:33:36 +0530 Subject: [PATCH 3/6] polish(builder): a11y + press feedback on Add Fields palette buttons --- .../builder/sidebar/AddFieldsSection.vue | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/builder/sidebar/AddFieldsSection.vue b/frontend/src/components/builder/sidebar/AddFieldsSection.vue index fb0f986..ebd62dc 100644 --- a/frontend/src/components/builder/sidebar/AddFieldsSection.vue +++ b/frontend/src/components/builder/sidebar/AddFieldsSection.vue @@ -24,15 +24,23 @@ const editFormStore = useEditForm(); variant="outline" placeholder="Search Fields" /> -
+

+ No fields match "{{ search }}" +

+
From b0d23fa524a3a2df56ae6c267a2fa3b9863f8af9 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Sat, 16 May 2026 01:36:24 +0530 Subject: [PATCH 4/6] polish(builder): tooltip on palette buttons explaining click action --- .../builder/sidebar/AddFieldsSection.vue | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/builder/sidebar/AddFieldsSection.vue b/frontend/src/components/builder/sidebar/AddFieldsSection.vue index ebd62dc..4cdaba8 100644 --- a/frontend/src/components/builder/sidebar/AddFieldsSection.vue +++ b/frontend/src/components/builder/sidebar/AddFieldsSection.vue @@ -2,7 +2,7 @@ import { ref, computed } from "vue"; import { formFields, FormFields } from "@/utils/form_fields"; import { Fieldtype } from "@/types/formfield"; -import { FormControl } from "frappe-ui"; +import { FormControl, Tooltip } from "frappe-ui"; import { useEditForm } from "@/stores/editForm"; const search = ref(""); @@ -28,21 +28,25 @@ const editFormStore = useEditForm(); No fields match "{{ search }}"

- + +
From e0975e4a3e13b9bfee95e009336d0dbcb77aed62 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Sat, 16 May 2026 11:43:26 +0530 Subject: [PATCH 5/6] polish(builder): fix scale transition and drop redundant tooltip on palette buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - transition-colors → transition-all so active:scale-[0.98] animates - remove constant-text Tooltip wrapper (icon + label already convey action) - drop redundant `as Fieldtype` cast and unused Fieldtype import - type-only import for FormFields --- .../builder/sidebar/AddFieldsSection.vue | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/builder/sidebar/AddFieldsSection.vue b/frontend/src/components/builder/sidebar/AddFieldsSection.vue index 4cdaba8..2db0450 100644 --- a/frontend/src/components/builder/sidebar/AddFieldsSection.vue +++ b/frontend/src/components/builder/sidebar/AddFieldsSection.vue @@ -1,8 +1,7 @@ + + diff --git a/frontend/src/components/builder/sidebar/AddFieldsSection.vue b/frontend/src/components/builder/sidebar/AddFieldsSection.vue index 2db0450..3b0af22 100644 --- a/frontend/src/components/builder/sidebar/AddFieldsSection.vue +++ b/frontend/src/components/builder/sidebar/AddFieldsSection.vue @@ -31,7 +31,7 @@ const editFormStore = useEditForm(); v-for="field in filteredFields" :key="field.name" type="button" - class="flex w-full items-center gap-2 px-2.5 py-2 bg-gray-50 rounded border border-gray-200 hover:border-gray-400 hover:bg-gray-100 active:scale-[0.98] active:bg-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-900 focus-visible:ring-offset-1 transition-all duration-150 text-left" + class="flex w-full items-center gap-2 px-2.5 py-2 bg-surface-gray-1 rounded border border-outline-gray-1 hover:border-outline-gray-2 hover:bg-surface-gray-2 active:scale-[0.98] active:bg-surface-gray-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-900 focus-visible:ring-offset-1 transition-all duration-150 text-left" @click="editFormStore.addField(field.name)" >