From 372b4e191c9fc2566dac88bd2f543fc931a1c601 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 13 Feb 2026 22:01:18 +0530 Subject: [PATCH] feat: implement custom field form and utility functions for managing custom fields --- .../[boardId]/TaskCustomFieldsEditor.test.tsx | 84 +++ .../[boardId]/TaskCustomFieldsEditor.tsx | 155 +++++ .../[boardId]/custom-field-utils.test.tsx | 150 +++++ .../boards/[boardId]/custom-field-utils.tsx | 329 ++++++++++ frontend/src/app/boards/[boardId]/page.tsx | 608 +----------------- .../app/custom-fields/[fieldId]/edit/page.tsx | 548 +--------------- frontend/src/app/custom-fields/new/page.tsx | 482 +------------- frontend/src/app/custom-fields/page.tsx | 9 +- .../custom-fields/CustomFieldForm.test.tsx | 136 ++++ .../custom-fields/CustomFieldForm.tsx | 368 +++++++++++ .../custom-fields/CustomFieldsTable.tsx | 13 +- .../custom-fields/custom-field-form-types.ts | 63 ++ .../custom-field-form-utils.test.ts | 167 +++++ .../custom-fields/custom-field-form-utils.ts | 273 ++++++++ 14 files changed, 1808 insertions(+), 1577 deletions(-) create mode 100644 frontend/src/app/boards/[boardId]/TaskCustomFieldsEditor.test.tsx create mode 100644 frontend/src/app/boards/[boardId]/TaskCustomFieldsEditor.tsx create mode 100644 frontend/src/app/boards/[boardId]/custom-field-utils.test.tsx create mode 100644 frontend/src/app/boards/[boardId]/custom-field-utils.tsx create mode 100644 frontend/src/components/custom-fields/CustomFieldForm.test.tsx create mode 100644 frontend/src/components/custom-fields/CustomFieldForm.tsx create mode 100644 frontend/src/components/custom-fields/custom-field-form-types.ts create mode 100644 frontend/src/components/custom-fields/custom-field-form-utils.test.ts create mode 100644 frontend/src/components/custom-fields/custom-field-form-utils.ts diff --git a/frontend/src/app/boards/[boardId]/TaskCustomFieldsEditor.test.tsx b/frontend/src/app/boards/[boardId]/TaskCustomFieldsEditor.test.tsx new file mode 100644 index 00000000..a0dcab03 --- /dev/null +++ b/frontend/src/app/boards/[boardId]/TaskCustomFieldsEditor.test.tsx @@ -0,0 +1,84 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import type { TaskCustomFieldDefinitionRead } from "@/api/generated/model"; +import { TaskCustomFieldsEditor } from "./TaskCustomFieldsEditor"; + +const buildDefinition = ( + overrides: Partial = {}, +): TaskCustomFieldDefinitionRead => ({ + id: "field-1", + organization_id: "org-1", + field_key: "client_name", + field_type: "text", + ui_visibility: "always", + label: "Client name", + required: false, + default_value: null, + description: null, + validation_regex: null, + board_ids: ["board-1"], + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + ...overrides, +}); + +describe("TaskCustomFieldsEditor", () => { + it("renders loading and empty states", () => { + const { rerender } = render( + , + ); + + expect(screen.getByText("Loading custom fields…")).toBeInTheDocument(); + + rerender( + , + ); + + expect( + screen.getByText("No custom fields configured for this board."), + ).toBeInTheDocument(); + }); + + it("updates field values and respects visibility rules", () => { + const setValues = vi.fn(); + render( + , + ); + + expect(screen.queryByText("hidden_if_unset")).not.toBeInTheDocument(); + + fireEvent.change(screen.getByRole("textbox"), { + target: { value: "Acme Corp" }, + }); + + const updater = setValues.mock.calls.at(-1)?.[0] as ( + prev: Record, + ) => Record; + expect(updater({})).toEqual({ client_name: "Acme Corp" }); + }); +}); diff --git a/frontend/src/app/boards/[boardId]/TaskCustomFieldsEditor.tsx b/frontend/src/app/boards/[boardId]/TaskCustomFieldsEditor.tsx new file mode 100644 index 00000000..c0e31042 --- /dev/null +++ b/frontend/src/app/boards/[boardId]/TaskCustomFieldsEditor.tsx @@ -0,0 +1,155 @@ +import type { Dispatch, SetStateAction } from "react"; + +import type { TaskCustomFieldDefinitionRead } from "@/api/generated/model"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; + +import { + customFieldInputText, + isCustomFieldVisible, + parseCustomFieldInputValue, + type TaskCustomFieldValues, +} from "./custom-field-utils"; + +type TaskCustomFieldsEditorProps = { + definitions: TaskCustomFieldDefinitionRead[]; + values: TaskCustomFieldValues; + setValues: Dispatch>; + isLoading: boolean; + disabled: boolean; + loadingMessage?: string; + emptyMessage?: string; +}; + +export function TaskCustomFieldsEditor({ + definitions, + values, + setValues, + isLoading, + disabled, + loadingMessage = "Loading custom fields…", + emptyMessage = "No custom fields configured for this board.", +}: TaskCustomFieldsEditorProps) { + if (isLoading) + return

{loadingMessage}

; + if (definitions.length === 0) { + return

{emptyMessage}

; + } + + return ( +
+ {definitions.map((definition) => { + const fieldValue = values[definition.field_key]; + if (!isCustomFieldVisible(definition, fieldValue)) return null; + + return ( +
+ + + {definition.field_type === "boolean" ? ( + + ) : definition.field_type === "text_long" || + definition.field_type === "json" ? ( +