From b982da159982a7cebb20b789f56c161568060dd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 04:18:47 +0000 Subject: [PATCH 1/4] Initial plan From a6f76542bb8d5d451823cdadd4d91220fdd501dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 04:22:51 +0000 Subject: [PATCH 2/4] feat: enhance List View and Form View protocols with detailed configuration schemas - Add ListColumnSchema with field, label, width, align, hidden, sortable, resizable, wrap, type - Add SelectionConfigSchema for row selection (none/single/multiple) - Add PaginationConfigSchema with pageSize and pageSizeOptions - Update ListViewSchema to support both string[] (legacy) and ListColumnSchema[] columns - Add resizable, striped, bordered features to ListViewSchema - Add FormFieldSchema with field, label, placeholder, helpText, readonly, required, hidden, colSpan - Add widget, dependsOn, visibleOn to FormFieldSchema for custom components and conditional logic - Update FormSectionSchema to support mixed field types (string | FormFieldSchema) - Add comprehensive test coverage for all new schemas - Generate JSON schemas and TypeScript type definitions Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- content/docs/references/ui/view/FormField.mdx | 20 + .../docs/references/ui/view/FormSection.mdx | 2 +- .../docs/references/ui/view/ListColumn.mdx | 18 + content/docs/references/ui/view/ListView.mdx | 7 +- .../references/ui/view/PaginationConfig.mdx | 11 + .../references/ui/view/SelectionConfig.mdx | 10 + packages/spec/json-schema/ui/FormField.json | 59 +++ packages/spec/json-schema/ui/FormSection.json | 59 ++- packages/spec/json-schema/ui/FormView.json | 118 ++++- packages/spec/json-schema/ui/ListColumn.json | 56 ++ packages/spec/json-schema/ui/ListView.json | 113 +++- .../spec/json-schema/ui/PaginationConfig.json | 24 + .../spec/json-schema/ui/SelectionConfig.json | 22 + packages/spec/json-schema/ui/View.json | 462 +++++++++++++++- packages/spec/package-lock.json | 4 +- packages/spec/src/ui/view.test.ts | 499 ++++++++++++++++++ packages/spec/src/ui/view.zod.ts | 74 ++- 17 files changed, 1533 insertions(+), 25 deletions(-) create mode 100644 content/docs/references/ui/view/FormField.mdx create mode 100644 content/docs/references/ui/view/ListColumn.mdx create mode 100644 content/docs/references/ui/view/PaginationConfig.mdx create mode 100644 content/docs/references/ui/view/SelectionConfig.mdx create mode 100644 packages/spec/json-schema/ui/FormField.json create mode 100644 packages/spec/json-schema/ui/ListColumn.json create mode 100644 packages/spec/json-schema/ui/PaginationConfig.json create mode 100644 packages/spec/json-schema/ui/SelectionConfig.json diff --git a/content/docs/references/ui/view/FormField.mdx b/content/docs/references/ui/view/FormField.mdx new file mode 100644 index 000000000..c3390d970 --- /dev/null +++ b/content/docs/references/ui/view/FormField.mdx @@ -0,0 +1,20 @@ +--- +title: FormField +description: FormField Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name (snake_case) | +| **label** | `string` | optional | Display label override | +| **placeholder** | `string` | optional | Placeholder text | +| **helpText** | `string` | optional | Help/hint text | +| **readonly** | `boolean` | optional | Read-only override | +| **required** | `boolean` | optional | Required override | +| **hidden** | `boolean` | optional | Hidden override | +| **colSpan** | `number` | optional | Column span in grid layout (1-4) | +| **widget** | `string` | optional | Custom widget/component name | +| **dependsOn** | `string` | optional | Parent field name for cascading | +| **visibleOn** | `string` | optional | Visibility condition expression | diff --git a/content/docs/references/ui/view/FormSection.mdx b/content/docs/references/ui/view/FormSection.mdx index 801c98338..a5c8e68e4 100644 --- a/content/docs/references/ui/view/FormSection.mdx +++ b/content/docs/references/ui/view/FormSection.mdx @@ -11,4 +11,4 @@ description: FormSection Schema Reference | **collapsible** | `boolean` | optional | | | **collapsed** | `boolean` | optional | | | **columns** | `Enum<'1' \| '2' \| '3' \| '4'>` | optional | | -| **fields** | `string[]` | ✅ | | +| **fields** | `string \| object[]` | ✅ | | diff --git a/content/docs/references/ui/view/ListColumn.mdx b/content/docs/references/ui/view/ListColumn.mdx new file mode 100644 index 000000000..a9578a61b --- /dev/null +++ b/content/docs/references/ui/view/ListColumn.mdx @@ -0,0 +1,18 @@ +--- +title: ListColumn +description: ListColumn Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name (snake_case) | +| **label** | `string` | optional | Display label override | +| **width** | `number` | optional | Column width in pixels | +| **align** | `Enum<'left' \| 'center' \| 'right'>` | optional | Text alignment | +| **hidden** | `boolean` | optional | Hide column by default | +| **sortable** | `boolean` | optional | Allow sorting by this column | +| **resizable** | `boolean` | optional | Allow resizing this column | +| **wrap** | `boolean` | optional | Allow text wrapping | +| **type** | `string` | optional | Renderer type override (e.g., "currency", "date") | diff --git a/content/docs/references/ui/view/ListView.mdx b/content/docs/references/ui/view/ListView.mdx index 0bad8e199..3903e45a3 100644 --- a/content/docs/references/ui/view/ListView.mdx +++ b/content/docs/references/ui/view/ListView.mdx @@ -10,10 +10,15 @@ description: ListView Schema Reference | **name** | `string` | optional | | | **label** | `string` | optional | | | **type** | `Enum<'grid' \| 'kanban' \| 'calendar' \| 'gantt' \| 'map'>` | optional | | -| **columns** | `string[]` | ✅ | Fields to display as columns | +| **columns** | `string[] \| object[]` | ✅ | Fields to display as columns | | **filter** | `any[]` | optional | Filter criteria (JSON Rules) | | **sort** | `string \| object[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | +| **resizable** | `boolean` | optional | Enable column resizing | +| **striped** | `boolean` | optional | Striped row styling | +| **bordered** | `boolean` | optional | Show borders | +| **selection** | `object` | optional | Row selection configuration | +| **pagination** | `object` | optional | Pagination configuration | | **kanban** | `object` | optional | | | **calendar** | `object` | optional | | | **gantt** | `object` | optional | | diff --git a/content/docs/references/ui/view/PaginationConfig.mdx b/content/docs/references/ui/view/PaginationConfig.mdx new file mode 100644 index 000000000..dcd9a1b77 --- /dev/null +++ b/content/docs/references/ui/view/PaginationConfig.mdx @@ -0,0 +1,11 @@ +--- +title: PaginationConfig +description: PaginationConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **pageSize** | `number` | optional | Number of records per page | +| **pageSizeOptions** | `number[]` | optional | Available page size options | diff --git a/content/docs/references/ui/view/SelectionConfig.mdx b/content/docs/references/ui/view/SelectionConfig.mdx new file mode 100644 index 000000000..cd7aa6348 --- /dev/null +++ b/content/docs/references/ui/view/SelectionConfig.mdx @@ -0,0 +1,10 @@ +--- +title: SelectionConfig +description: SelectionConfig Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `Enum<'none' \| 'single' \| 'multiple'>` | optional | Selection mode | diff --git a/packages/spec/json-schema/ui/FormField.json b/packages/spec/json-schema/ui/FormField.json new file mode 100644 index 000000000..1790fd546 --- /dev/null +++ b/packages/spec/json-schema/ui/FormField.json @@ -0,0 +1,59 @@ +{ + "$ref": "#/definitions/FormField", + "definitions": { + "FormField": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "placeholder": { + "type": "string", + "description": "Placeholder text" + }, + "helpText": { + "type": "string", + "description": "Help/hint text" + }, + "readonly": { + "type": "boolean", + "description": "Read-only override" + }, + "required": { + "type": "boolean", + "description": "Required override" + }, + "hidden": { + "type": "boolean", + "description": "Hidden override" + }, + "colSpan": { + "type": "number", + "description": "Column span in grid layout (1-4)" + }, + "widget": { + "type": "string", + "description": "Custom widget/component name" + }, + "dependsOn": { + "type": "string", + "description": "Parent field name for cascading" + }, + "visibleOn": { + "type": "string", + "description": "Visibility condition expression" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/ui/FormSection.json b/packages/spec/json-schema/ui/FormSection.json index bfc9a6d93..65a3ef80f 100644 --- a/packages/spec/json-schema/ui/FormSection.json +++ b/packages/spec/json-schema/ui/FormSection.json @@ -28,7 +28,64 @@ "fields": { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "placeholder": { + "type": "string", + "description": "Placeholder text" + }, + "helpText": { + "type": "string", + "description": "Help/hint text" + }, + "readonly": { + "type": "boolean", + "description": "Read-only override" + }, + "required": { + "type": "boolean", + "description": "Required override" + }, + "hidden": { + "type": "boolean", + "description": "Hidden override" + }, + "colSpan": { + "type": "number", + "description": "Column span in grid layout (1-4)" + }, + "widget": { + "type": "string", + "description": "Custom widget/component name" + }, + "dependsOn": { + "type": "string", + "description": "Parent field name for cascading" + }, + "visibleOn": { + "type": "string", + "description": "Visibility condition expression" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + ] } } }, diff --git a/packages/spec/json-schema/ui/FormView.json b/packages/spec/json-schema/ui/FormView.json index 72e606494..0a0dd5a04 100644 --- a/packages/spec/json-schema/ui/FormView.json +++ b/packages/spec/json-schema/ui/FormView.json @@ -42,7 +42,64 @@ "fields": { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "placeholder": { + "type": "string", + "description": "Placeholder text" + }, + "helpText": { + "type": "string", + "description": "Help/hint text" + }, + "readonly": { + "type": "boolean", + "description": "Read-only override" + }, + "required": { + "type": "boolean", + "description": "Required override" + }, + "hidden": { + "type": "boolean", + "description": "Hidden override" + }, + "colSpan": { + "type": "number", + "description": "Column span in grid layout (1-4)" + }, + "widget": { + "type": "string", + "description": "Custom widget/component name" + }, + "dependsOn": { + "type": "string", + "description": "Parent field name for cascading" + }, + "visibleOn": { + "type": "string", + "description": "Visibility condition expression" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + ] } } }, @@ -81,7 +138,64 @@ "fields": { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "placeholder": { + "type": "string", + "description": "Placeholder text" + }, + "helpText": { + "type": "string", + "description": "Help/hint text" + }, + "readonly": { + "type": "boolean", + "description": "Read-only override" + }, + "required": { + "type": "boolean", + "description": "Required override" + }, + "hidden": { + "type": "boolean", + "description": "Hidden override" + }, + "colSpan": { + "type": "number", + "description": "Column span in grid layout (1-4)" + }, + "widget": { + "type": "string", + "description": "Custom widget/component name" + }, + "dependsOn": { + "type": "string", + "description": "Parent field name for cascading" + }, + "visibleOn": { + "type": "string", + "description": "Visibility condition expression" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + ] } } }, diff --git a/packages/spec/json-schema/ui/ListColumn.json b/packages/spec/json-schema/ui/ListColumn.json new file mode 100644 index 000000000..45aecbb68 --- /dev/null +++ b/packages/spec/json-schema/ui/ListColumn.json @@ -0,0 +1,56 @@ +{ + "$ref": "#/definitions/ListColumn", + "definitions": { + "ListColumn": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "width": { + "type": "number", + "description": "Column width in pixels" + }, + "align": { + "type": "string", + "enum": [ + "left", + "center", + "right" + ], + "description": "Text alignment" + }, + "hidden": { + "type": "boolean", + "description": "Hide column by default" + }, + "sortable": { + "type": "boolean", + "description": "Allow sorting by this column" + }, + "resizable": { + "type": "boolean", + "description": "Allow resizing this column" + }, + "wrap": { + "type": "boolean", + "description": "Allow text wrapping" + }, + "type": { + "type": "string", + "description": "Renderer type override (e.g., \"currency\", \"date\")" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/ui/ListView.json b/packages/spec/json-schema/ui/ListView.json index ecdb6aef9..af6e8ddb3 100644 --- a/packages/spec/json-schema/ui/ListView.json +++ b/packages/spec/json-schema/ui/ListView.json @@ -22,10 +22,67 @@ "default": "grid" }, "columns": { - "type": "array", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "width": { + "type": "number", + "description": "Column width in pixels" + }, + "align": { + "type": "string", + "enum": [ + "left", + "center", + "right" + ], + "description": "Text alignment" + }, + "hidden": { + "type": "boolean", + "description": "Hide column by default" + }, + "sortable": { + "type": "boolean", + "description": "Allow sorting by this column" + }, + "resizable": { + "type": "boolean", + "description": "Allow resizing this column" + }, + "wrap": { + "type": "boolean", + "description": "Allow text wrapping" + }, + "type": { + "type": "string", + "description": "Renderer type override (e.g., \"currency\", \"date\")" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + } + ], "description": "Fields to display as columns" }, "filter": { @@ -69,6 +126,54 @@ }, "description": "Fields enabled for search" }, + "resizable": { + "type": "boolean", + "description": "Enable column resizing" + }, + "striped": { + "type": "boolean", + "description": "Striped row styling" + }, + "bordered": { + "type": "boolean", + "description": "Show borders" + }, + "selection": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none", + "single", + "multiple" + ], + "default": "none", + "description": "Selection mode" + } + }, + "additionalProperties": false, + "description": "Row selection configuration" + }, + "pagination": { + "type": "object", + "properties": { + "pageSize": { + "type": "number", + "default": 25, + "description": "Number of records per page" + }, + "pageSizeOptions": { + "type": "array", + "items": { + "type": "number" + }, + "description": "Available page size options" + } + }, + "additionalProperties": false, + "description": "Pagination configuration" + }, "kanban": { "type": "object", "properties": { diff --git a/packages/spec/json-schema/ui/PaginationConfig.json b/packages/spec/json-schema/ui/PaginationConfig.json new file mode 100644 index 000000000..7fffcecb2 --- /dev/null +++ b/packages/spec/json-schema/ui/PaginationConfig.json @@ -0,0 +1,24 @@ +{ + "$ref": "#/definitions/PaginationConfig", + "definitions": { + "PaginationConfig": { + "type": "object", + "properties": { + "pageSize": { + "type": "number", + "default": 25, + "description": "Number of records per page" + }, + "pageSizeOptions": { + "type": "array", + "items": { + "type": "number" + }, + "description": "Available page size options" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/ui/SelectionConfig.json b/packages/spec/json-schema/ui/SelectionConfig.json new file mode 100644 index 000000000..b45a17c5e --- /dev/null +++ b/packages/spec/json-schema/ui/SelectionConfig.json @@ -0,0 +1,22 @@ +{ + "$ref": "#/definitions/SelectionConfig", + "definitions": { + "SelectionConfig": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none", + "single", + "multiple" + ], + "default": "none", + "description": "Selection mode" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/ui/View.json b/packages/spec/json-schema/ui/View.json index 1a30b5fd7..c9b9ae30f 100644 --- a/packages/spec/json-schema/ui/View.json +++ b/packages/spec/json-schema/ui/View.json @@ -25,10 +25,67 @@ "default": "grid" }, "columns": { - "type": "array", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "width": { + "type": "number", + "description": "Column width in pixels" + }, + "align": { + "type": "string", + "enum": [ + "left", + "center", + "right" + ], + "description": "Text alignment" + }, + "hidden": { + "type": "boolean", + "description": "Hide column by default" + }, + "sortable": { + "type": "boolean", + "description": "Allow sorting by this column" + }, + "resizable": { + "type": "boolean", + "description": "Allow resizing this column" + }, + "wrap": { + "type": "boolean", + "description": "Allow text wrapping" + }, + "type": { + "type": "string", + "description": "Renderer type override (e.g., \"currency\", \"date\")" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + } + ], "description": "Fields to display as columns" }, "filter": { @@ -72,6 +129,54 @@ }, "description": "Fields enabled for search" }, + "resizable": { + "type": "boolean", + "description": "Enable column resizing" + }, + "striped": { + "type": "boolean", + "description": "Striped row styling" + }, + "bordered": { + "type": "boolean", + "description": "Show borders" + }, + "selection": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none", + "single", + "multiple" + ], + "default": "none", + "description": "Selection mode" + } + }, + "additionalProperties": false, + "description": "Row selection configuration" + }, + "pagination": { + "type": "object", + "properties": { + "pageSize": { + "type": "number", + "default": 25, + "description": "Number of records per page" + }, + "pageSizeOptions": { + "type": "array", + "items": { + "type": "number" + }, + "description": "Available page size options" + } + }, + "additionalProperties": false, + "description": "Pagination configuration" + }, "kanban": { "type": "object", "properties": { @@ -192,7 +297,64 @@ "fields": { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "placeholder": { + "type": "string", + "description": "Placeholder text" + }, + "helpText": { + "type": "string", + "description": "Help/hint text" + }, + "readonly": { + "type": "boolean", + "description": "Read-only override" + }, + "required": { + "type": "boolean", + "description": "Required override" + }, + "hidden": { + "type": "boolean", + "description": "Hidden override" + }, + "colSpan": { + "type": "number", + "description": "Column span in grid layout (1-4)" + }, + "widget": { + "type": "string", + "description": "Custom widget/component name" + }, + "dependsOn": { + "type": "string", + "description": "Parent field name for cascading" + }, + "visibleOn": { + "type": "string", + "description": "Visibility condition expression" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + ] } } }, @@ -231,7 +393,64 @@ "fields": { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "placeholder": { + "type": "string", + "description": "Placeholder text" + }, + "helpText": { + "type": "string", + "description": "Help/hint text" + }, + "readonly": { + "type": "boolean", + "description": "Read-only override" + }, + "required": { + "type": "boolean", + "description": "Required override" + }, + "hidden": { + "type": "boolean", + "description": "Hidden override" + }, + "colSpan": { + "type": "number", + "description": "Column span in grid layout (1-4)" + }, + "widget": { + "type": "string", + "description": "Custom widget/component name" + }, + "dependsOn": { + "type": "string", + "description": "Parent field name for cascading" + }, + "visibleOn": { + "type": "string", + "description": "Visibility condition expression" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + ] } } }, @@ -267,10 +486,67 @@ "default": "grid" }, "columns": { - "type": "array", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "width": { + "type": "number", + "description": "Column width in pixels" + }, + "align": { + "type": "string", + "enum": [ + "left", + "center", + "right" + ], + "description": "Text alignment" + }, + "hidden": { + "type": "boolean", + "description": "Hide column by default" + }, + "sortable": { + "type": "boolean", + "description": "Allow sorting by this column" + }, + "resizable": { + "type": "boolean", + "description": "Allow resizing this column" + }, + "wrap": { + "type": "boolean", + "description": "Allow text wrapping" + }, + "type": { + "type": "string", + "description": "Renderer type override (e.g., \"currency\", \"date\")" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + } + ], "description": "Fields to display as columns" }, "filter": { @@ -314,6 +590,54 @@ }, "description": "Fields enabled for search" }, + "resizable": { + "type": "boolean", + "description": "Enable column resizing" + }, + "striped": { + "type": "boolean", + "description": "Striped row styling" + }, + "bordered": { + "type": "boolean", + "description": "Show borders" + }, + "selection": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "none", + "single", + "multiple" + ], + "default": "none", + "description": "Selection mode" + } + }, + "additionalProperties": false, + "description": "Row selection configuration" + }, + "pagination": { + "type": "object", + "properties": { + "pageSize": { + "type": "number", + "default": 25, + "description": "Number of records per page" + }, + "pageSizeOptions": { + "type": "array", + "items": { + "type": "number" + }, + "description": "Available page size options" + } + }, + "additionalProperties": false, + "description": "Pagination configuration" + }, "kanban": { "type": "object", "properties": { @@ -438,7 +762,64 @@ "fields": { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "placeholder": { + "type": "string", + "description": "Placeholder text" + }, + "helpText": { + "type": "string", + "description": "Help/hint text" + }, + "readonly": { + "type": "boolean", + "description": "Read-only override" + }, + "required": { + "type": "boolean", + "description": "Required override" + }, + "hidden": { + "type": "boolean", + "description": "Hidden override" + }, + "colSpan": { + "type": "number", + "description": "Column span in grid layout (1-4)" + }, + "widget": { + "type": "string", + "description": "Custom widget/component name" + }, + "dependsOn": { + "type": "string", + "description": "Parent field name for cascading" + }, + "visibleOn": { + "type": "string", + "description": "Visibility condition expression" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + ] } } }, @@ -477,7 +858,64 @@ "fields": { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Field name (snake_case)" + }, + "label": { + "type": "string", + "description": "Display label override" + }, + "placeholder": { + "type": "string", + "description": "Placeholder text" + }, + "helpText": { + "type": "string", + "description": "Help/hint text" + }, + "readonly": { + "type": "boolean", + "description": "Read-only override" + }, + "required": { + "type": "boolean", + "description": "Required override" + }, + "hidden": { + "type": "boolean", + "description": "Hidden override" + }, + "colSpan": { + "type": "number", + "description": "Column span in grid layout (1-4)" + }, + "widget": { + "type": "string", + "description": "Custom widget/component name" + }, + "dependsOn": { + "type": "string", + "description": "Parent field name for cascading" + }, + "visibleOn": { + "type": "string", + "description": "Visibility condition expression" + } + }, + "required": [ + "field" + ], + "additionalProperties": false + } + ] } } }, diff --git a/packages/spec/package-lock.json b/packages/spec/package-lock.json index 5e9e01f61..43ffd2417 100644 --- a/packages/spec/package-lock.json +++ b/packages/spec/package-lock.json @@ -1,12 +1,12 @@ { "name": "@objectstack/spec", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@objectstack/spec", - "version": "0.3.0", + "version": "0.3.1", "license": "Apache-2.0", "dependencies": { "zod": "^3.22.4" diff --git a/packages/spec/src/ui/view.test.ts b/packages/spec/src/ui/view.test.ts index 104b15743..90569ac67 100644 --- a/packages/spec/src/ui/view.test.ts +++ b/packages/spec/src/ui/view.test.ts @@ -7,9 +7,15 @@ import { KanbanConfigSchema, CalendarConfigSchema, GanttConfigSchema, + ListColumnSchema, + FormFieldSchema, + SelectionConfigSchema, + PaginationConfigSchema, type View, type ListView, type FormView, + type ListColumn, + type FormField, } from './view.zod'; describe('KanbanConfigSchema', () => { @@ -507,3 +513,496 @@ describe('ViewSchema', () => { }); }); }); + +describe('ListColumnSchema', () => { + it('should accept minimal column config', () => { + const column: ListColumn = { + field: 'account_name', + }; + + expect(() => ListColumnSchema.parse(column)).not.toThrow(); + }); + + it('should accept full column config', () => { + const column: ListColumn = { + field: 'annual_revenue', + label: 'Annual Revenue', + width: 150, + align: 'right', + hidden: false, + sortable: true, + resizable: true, + wrap: false, + type: 'currency', + }; + + expect(() => ListColumnSchema.parse(column)).not.toThrow(); + }); + + it('should accept column with alignment options', () => { + const alignments = ['left', 'center', 'right'] as const; + + alignments.forEach(align => { + const column: ListColumn = { + field: 'test_field', + align, + }; + expect(() => ListColumnSchema.parse(column)).not.toThrow(); + }); + }); +}); + +describe('SelectionConfigSchema', () => { + it('should default to none', () => { + const selection = {}; + + const result = SelectionConfigSchema.parse(selection); + expect(result.type).toBe('none'); + }); + + it('should accept all selection types', () => { + const types = ['none', 'single', 'multiple'] as const; + + types.forEach(type => { + const selection = { type }; + expect(() => SelectionConfigSchema.parse(selection)).not.toThrow(); + }); + }); +}); + +describe('PaginationConfigSchema', () => { + it('should default pageSize to 25', () => { + const pagination = {}; + + const result = PaginationConfigSchema.parse(pagination); + expect(result.pageSize).toBe(25); + }); + + it('should accept custom page size', () => { + const pagination = { + pageSize: 50, + }; + + const result = PaginationConfigSchema.parse(pagination); + expect(result.pageSize).toBe(50); + }); + + it('should accept page size options', () => { + const pagination = { + pageSize: 25, + pageSizeOptions: [10, 25, 50, 100], + }; + + expect(() => PaginationConfigSchema.parse(pagination)).not.toThrow(); + }); +}); + +describe('Enhanced ListViewSchema', () => { + it('should accept legacy string array columns', () => { + const listView: ListView = { + columns: ['name', 'email', 'phone'], + }; + + expect(() => ListViewSchema.parse(listView)).not.toThrow(); + }); + + it('should accept enhanced column config array', () => { + const listView: ListView = { + columns: [ + { field: 'name', sortable: true }, + { field: 'email', width: 200 }, + { field: 'annual_revenue', align: 'right', type: 'currency' }, + ], + }; + + expect(() => ListViewSchema.parse(listView)).not.toThrow(); + }); + + it('should accept grid features', () => { + const listView: ListView = { + columns: ['name', 'status'], + resizable: true, + striped: true, + bordered: true, + }; + + expect(() => ListViewSchema.parse(listView)).not.toThrow(); + }); + + it('should accept selection configuration', () => { + const listView: ListView = { + columns: ['name', 'status'], + selection: { + type: 'multiple', + }, + }; + + expect(() => ListViewSchema.parse(listView)).not.toThrow(); + }); + + it('should accept pagination configuration', () => { + const listView: ListView = { + columns: ['name', 'status'], + pagination: { + pageSize: 50, + pageSizeOptions: [25, 50, 100], + }, + }; + + expect(() => ListViewSchema.parse(listView)).not.toThrow(); + }); + + it('should accept complete enhanced list view', () => { + const listView: ListView = { + name: 'advanced_grid', + label: 'Advanced Data Grid', + type: 'grid', + columns: [ + { + field: 'account_name', + label: 'Account Name', + sortable: true, + resizable: true, + width: 200, + }, + { + field: 'industry', + width: 150, + sortable: true, + }, + { + field: 'annual_revenue', + label: 'Revenue', + align: 'right', + type: 'currency', + sortable: true, + width: 150, + }, + { + field: 'status', + width: 100, + }, + ], + filter: [{ field: 'status', operator: 'equals', value: 'active' }], + sort: [{ field: 'annual_revenue', order: 'desc' }], + searchableFields: ['account_name', 'industry'], + resizable: true, + striped: true, + bordered: false, + selection: { + type: 'multiple', + }, + pagination: { + pageSize: 50, + pageSizeOptions: [25, 50, 100, 200], + }, + }; + + expect(() => ListViewSchema.parse(listView)).not.toThrow(); + }); +}); + +describe('FormFieldSchema', () => { + it('should accept minimal field config', () => { + const field: FormField = { + field: 'first_name', + }; + + expect(() => FormFieldSchema.parse(field)).not.toThrow(); + }); + + it('should accept full field config', () => { + const field: FormField = { + field: 'email_address', + label: 'Email Address', + placeholder: 'Enter your email', + helpText: 'We will never share your email', + readonly: false, + required: true, + hidden: false, + colSpan: 2, + widget: 'email-input', + }; + + expect(() => FormFieldSchema.parse(field)).not.toThrow(); + }); + + it('should accept field with conditional logic', () => { + const field: FormField = { + field: 'state', + dependsOn: 'country', + visibleOn: 'country === "USA"', + }; + + expect(() => FormFieldSchema.parse(field)).not.toThrow(); + }); + + it('should accept field with custom widget', () => { + const field: FormField = { + field: 'color_preference', + widget: 'color-picker', + }; + + expect(() => FormFieldSchema.parse(field)).not.toThrow(); + }); +}); + +describe('Enhanced FormSectionSchema', () => { + it('should accept legacy string array fields', () => { + const section = { + fields: ['name', 'email', 'phone'], + }; + + expect(() => FormSectionSchema.parse(section)).not.toThrow(); + }); + + it('should accept enhanced field config array', () => { + const section = { + label: 'Contact Information', + fields: [ + { field: 'first_name', required: true }, + { field: 'last_name', required: true }, + { field: 'email', widget: 'email-input', colSpan: 2 }, + ], + }; + + expect(() => FormSectionSchema.parse(section)).not.toThrow(); + }); + + it('should accept mixed field types (string and FormFieldSchema)', () => { + const section = { + label: 'User Profile', + columns: '2', + fields: [ + 'username', // Simple string + { field: 'email', required: true, widget: 'email-input' }, // Enhanced config + 'phone', // Simple string + { + field: 'bio', + placeholder: 'Tell us about yourself', + colSpan: 2, + }, // Enhanced config + ], + }; + + expect(() => FormSectionSchema.parse(section)).not.toThrow(); + }); + + it('should accept section with conditional fields', () => { + const section = { + label: 'Address', + columns: '2', + fields: [ + { field: 'country', required: true }, + { + field: 'state', + dependsOn: 'country', + visibleOn: 'country === "USA"', + }, + { + field: 'province', + dependsOn: 'country', + visibleOn: 'country === "Canada"', + }, + 'city', + 'postal_code', + ], + }; + + expect(() => FormSectionSchema.parse(section)).not.toThrow(); + }); +}); + +describe('Enhanced FormViewSchema with Complex Fields', () => { + it('should accept form with enhanced field configurations', () => { + const formView: FormView = { + type: 'simple', + sections: [ + { + label: 'Basic Information', + columns: '2', + fields: [ + { field: 'first_name', required: true, placeholder: 'First name' }, + { field: 'last_name', required: true, placeholder: 'Last name' }, + { + field: 'email', + required: true, + widget: 'email-input', + helpText: 'We will send confirmation to this email', + }, + 'phone', + ], + }, + { + label: 'Address', + collapsible: true, + columns: '2', + fields: [ + 'street', + 'city', + { + field: 'country', + required: true, + widget: 'country-select', + }, + { + field: 'state', + dependsOn: 'country', + visibleOn: 'country === "USA"', + widget: 'state-select', + }, + ], + }, + ], + }; + + expect(() => FormViewSchema.parse(formView)).not.toThrow(); + }); + + it('should accept tabbed form with enhanced fields', () => { + const formView: FormView = { + type: 'tabbed', + sections: [ + { + label: 'Personal', + fields: [ + { field: 'name', required: true }, + { field: 'email', required: true, widget: 'email-input' }, + ], + }, + { + label: 'Preferences', + fields: [ + { field: 'theme', widget: 'theme-selector' }, + { field: 'notifications', widget: 'toggle-group' }, + ], + }, + ], + }; + + expect(() => FormViewSchema.parse(formView)).not.toThrow(); + }); +}); + +describe('Real-World Enhanced View Examples', () => { + it('should accept CRM account view with enhanced columns', () => { + const accountViews: View = { + list: { + type: 'grid', + columns: [ + { field: 'account_name', label: 'Account Name', sortable: true, width: 200 }, + { field: 'industry', sortable: true, width: 150 }, + { field: 'annual_revenue', align: 'right', type: 'currency', sortable: true }, + { field: 'employees', align: 'right', type: 'number', sortable: true }, + { field: 'status', width: 100 }, + ], + resizable: true, + striped: true, + selection: { + type: 'multiple', + }, + pagination: { + pageSize: 50, + pageSizeOptions: [25, 50, 100], + }, + }, + form: { + type: 'tabbed', + sections: [ + { + label: 'Account Details', + columns: '2', + fields: [ + { field: 'account_name', required: true, colSpan: 2 }, + { field: 'industry', widget: 'industry-select' }, + { field: 'employees', widget: 'number-input' }, + { field: 'annual_revenue', widget: 'currency-input' }, + 'website', + ], + }, + { + label: 'Address', + columns: '2', + fields: [ + 'billing_street', + 'billing_city', + { field: 'billing_country', widget: 'country-select' }, + { + field: 'billing_state', + dependsOn: 'billing_country', + visibleOn: 'billing_country === "USA"', + }, + ], + }, + ], + }, + }; + + expect(() => ViewSchema.parse(accountViews)).not.toThrow(); + }); + + it('should accept project management view with all enhancements', () => { + const projectViews: View = { + list: { + type: 'grid', + columns: [ + { field: 'project_name', sortable: true, width: 250, resizable: true }, + { field: 'status', width: 120, sortable: true }, + { field: 'priority', width: 100, align: 'center' }, + { field: 'start_date', type: 'date', sortable: true, width: 120 }, + { field: 'due_date', type: 'date', sortable: true, width: 120 }, + { field: 'completion', type: 'percent', align: 'right', width: 100 }, + ], + resizable: true, + striped: true, + bordered: true, + selection: { + type: 'single', + }, + pagination: { + pageSize: 25, + pageSizeOptions: [10, 25, 50, 100], + }, + }, + form: { + type: 'wizard', + sections: [ + { + label: 'Step 1: Project Basics', + fields: [ + { + field: 'project_name', + required: true, + placeholder: 'Enter project name', + helpText: 'Choose a descriptive name for your project', + }, + { + field: 'description', + widget: 'rich-text-editor', + helpText: 'Detailed project description', + }, + ], + }, + { + label: 'Step 2: Timeline', + columns: '2', + fields: [ + { field: 'start_date', required: true, widget: 'date-picker' }, + { field: 'due_date', required: true, widget: 'date-picker' }, + { field: 'estimated_hours', widget: 'number-input' }, + ], + }, + { + label: 'Step 3: Team', + fields: [ + { field: 'project_manager', required: true, widget: 'user-lookup' }, + { field: 'team_members', widget: 'multi-user-lookup' }, + ], + }, + ], + }, + }; + + expect(() => ViewSchema.parse(projectViews)).not.toThrow(); + }); +}); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 8c6c0b35b..b7d80fa38 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1,5 +1,36 @@ import { z } from 'zod'; +/** + * List Column Configuration Schema + * Detailed configuration for individual list view columns + */ +export const ListColumnSchema = z.object({ + field: z.string().describe('Field name (snake_case)'), + label: z.string().optional().describe('Display label override'), + width: z.number().optional().describe('Column width in pixels'), + align: z.enum(['left', 'center', 'right']).optional().describe('Text alignment'), + hidden: z.boolean().optional().describe('Hide column by default'), + sortable: z.boolean().optional().describe('Allow sorting by this column'), + resizable: z.boolean().optional().describe('Allow resizing this column'), + wrap: z.boolean().optional().describe('Allow text wrapping'), + type: z.string().optional().describe('Renderer type override (e.g., "currency", "date")'), +}); + +/** + * List View Selection Configuration + */ +export const SelectionConfigSchema = z.object({ + type: z.enum(['none', 'single', 'multiple']).default('none').describe('Selection mode'), +}); + +/** + * List View Pagination Configuration + */ +export const PaginationConfigSchema = z.object({ + pageSize: z.number().default(25).describe('Number of records per page'), + pageSizeOptions: z.array(z.number()).optional().describe('Available page size options'), +}); + /** * Kanban Settings */ @@ -39,7 +70,10 @@ export const ListViewSchema = z.object({ type: z.enum(['grid', 'kanban', 'calendar', 'gantt', 'map']).default('grid'), /** Shared Query Config */ - columns: z.array(z.string()).describe('Fields to display as columns'), + columns: z.union([ + z.array(z.string()), // Legacy: simple field names + z.array(ListColumnSchema), // Enhanced: detailed column config + ]).describe('Fields to display as columns'), filter: z.array(z.any()).optional().describe('Filter criteria (JSON Rules)'), sort: z.union([ z.string(), //Legacy "field desc" @@ -52,12 +86,41 @@ export const ListViewSchema = z.object({ /** Search */ searchableFields: z.array(z.string()).optional().describe('Fields enabled for search'), + /** Grid Features */ + resizable: z.boolean().optional().describe('Enable column resizing'), + striped: z.boolean().optional().describe('Striped row styling'), + bordered: z.boolean().optional().describe('Show borders'), + + /** Selection */ + selection: SelectionConfigSchema.optional().describe('Row selection configuration'), + + /** Pagination */ + pagination: PaginationConfigSchema.optional().describe('Pagination configuration'), + /** Type Specific Config */ kanban: KanbanConfigSchema.optional(), calendar: CalendarConfigSchema.optional(), gantt: GanttConfigSchema.optional(), }); +/** + * Form Field Configuration Schema + * Detailed configuration for individual form fields + */ +export const FormFieldSchema = z.object({ + field: z.string().describe('Field name (snake_case)'), + label: z.string().optional().describe('Display label override'), + placeholder: z.string().optional().describe('Placeholder text'), + helpText: z.string().optional().describe('Help/hint text'), + readonly: z.boolean().optional().describe('Read-only override'), + required: z.boolean().optional().describe('Required override'), + hidden: z.boolean().optional().describe('Hidden override'), + colSpan: z.number().optional().describe('Column span in grid layout (1-4)'), + widget: z.string().optional().describe('Custom widget/component name'), + dependsOn: z.string().optional().describe('Parent field name for cascading'), + visibleOn: z.string().optional().describe('Visibility condition expression'), +}); + /** * Form Layout Section */ @@ -66,7 +129,10 @@ export const FormSectionSchema = z.object({ collapsible: z.boolean().default(false), collapsed: z.boolean().default(false), columns: z.enum(['1', '2', '3', '4']).default('2').transform(val => parseInt(val) as 1 | 2 | 3 | 4), - fields: z.array(z.string()), // or complex FieldConfig + fields: z.array(z.union([ + z.string(), // Legacy: simple field name + FormFieldSchema, // Enhanced: detailed field config + ])), }); /** @@ -93,3 +159,7 @@ export type View = z.infer; export type ListView = z.infer; export type FormView = z.infer; export type FormSection = z.infer; +export type ListColumn = z.infer; +export type FormField = z.infer; +export type SelectionConfig = z.infer; +export type PaginationConfig = z.infer; From c9bd09ea38ad20de3dc8d2f90efb7c636244d828 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 04:24:35 +0000 Subject: [PATCH 3/4] refactor: add validation constraints to numeric fields - Add .positive() validation to ListColumnSchema.width - Add .int().positive() validation to PaginationConfigSchema.pageSize and pageSizeOptions - Add .int().min(1).max(4) validation to FormFieldSchema.colSpan Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- content/docs/references/ui/view/FormField.mdx | 2 +- .../references/ui/view/PaginationConfig.mdx | 4 +-- packages/spec/json-schema/ui/FormField.json | 4 ++- packages/spec/json-schema/ui/FormSection.json | 4 ++- packages/spec/json-schema/ui/FormView.json | 8 +++-- packages/spec/json-schema/ui/ListColumn.json | 1 + packages/spec/json-schema/ui/ListView.json | 7 +++-- .../spec/json-schema/ui/PaginationConfig.json | 6 ++-- packages/spec/json-schema/ui/View.json | 30 ++++++++++++++----- packages/spec/src/ui/view.zod.ts | 8 ++--- 10 files changed, 51 insertions(+), 23 deletions(-) diff --git a/content/docs/references/ui/view/FormField.mdx b/content/docs/references/ui/view/FormField.mdx index c3390d970..e0e56aa1c 100644 --- a/content/docs/references/ui/view/FormField.mdx +++ b/content/docs/references/ui/view/FormField.mdx @@ -14,7 +14,7 @@ description: FormField Schema Reference | **readonly** | `boolean` | optional | Read-only override | | **required** | `boolean` | optional | Required override | | **hidden** | `boolean` | optional | Hidden override | -| **colSpan** | `number` | optional | Column span in grid layout (1-4) | +| **colSpan** | `integer` | optional | Column span in grid layout (1-4) | | **widget** | `string` | optional | Custom widget/component name | | **dependsOn** | `string` | optional | Parent field name for cascading | | **visibleOn** | `string` | optional | Visibility condition expression | diff --git a/content/docs/references/ui/view/PaginationConfig.mdx b/content/docs/references/ui/view/PaginationConfig.mdx index dcd9a1b77..887a7f44b 100644 --- a/content/docs/references/ui/view/PaginationConfig.mdx +++ b/content/docs/references/ui/view/PaginationConfig.mdx @@ -7,5 +7,5 @@ description: PaginationConfig Schema Reference | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **pageSize** | `number` | optional | Number of records per page | -| **pageSizeOptions** | `number[]` | optional | Available page size options | +| **pageSize** | `integer` | optional | Number of records per page | +| **pageSizeOptions** | `integer[]` | optional | Available page size options | diff --git a/packages/spec/json-schema/ui/FormField.json b/packages/spec/json-schema/ui/FormField.json index 1790fd546..f8bfdf621 100644 --- a/packages/spec/json-schema/ui/FormField.json +++ b/packages/spec/json-schema/ui/FormField.json @@ -33,7 +33,9 @@ "description": "Hidden override" }, "colSpan": { - "type": "number", + "type": "integer", + "minimum": 1, + "maximum": 4, "description": "Column span in grid layout (1-4)" }, "widget": { diff --git a/packages/spec/json-schema/ui/FormSection.json b/packages/spec/json-schema/ui/FormSection.json index 65a3ef80f..1b0289e96 100644 --- a/packages/spec/json-schema/ui/FormSection.json +++ b/packages/spec/json-schema/ui/FormSection.json @@ -64,7 +64,9 @@ "description": "Hidden override" }, "colSpan": { - "type": "number", + "type": "integer", + "minimum": 1, + "maximum": 4, "description": "Column span in grid layout (1-4)" }, "widget": { diff --git a/packages/spec/json-schema/ui/FormView.json b/packages/spec/json-schema/ui/FormView.json index 0a0dd5a04..f738791d2 100644 --- a/packages/spec/json-schema/ui/FormView.json +++ b/packages/spec/json-schema/ui/FormView.json @@ -78,7 +78,9 @@ "description": "Hidden override" }, "colSpan": { - "type": "number", + "type": "integer", + "minimum": 1, + "maximum": 4, "description": "Column span in grid layout (1-4)" }, "widget": { @@ -174,7 +176,9 @@ "description": "Hidden override" }, "colSpan": { - "type": "number", + "type": "integer", + "minimum": 1, + "maximum": 4, "description": "Column span in grid layout (1-4)" }, "widget": { diff --git a/packages/spec/json-schema/ui/ListColumn.json b/packages/spec/json-schema/ui/ListColumn.json index 45aecbb68..f3ed157f8 100644 --- a/packages/spec/json-schema/ui/ListColumn.json +++ b/packages/spec/json-schema/ui/ListColumn.json @@ -14,6 +14,7 @@ }, "width": { "type": "number", + "exclusiveMinimum": 0, "description": "Column width in pixels" }, "align": { diff --git a/packages/spec/json-schema/ui/ListView.json b/packages/spec/json-schema/ui/ListView.json index af6e8ddb3..e59da97df 100644 --- a/packages/spec/json-schema/ui/ListView.json +++ b/packages/spec/json-schema/ui/ListView.json @@ -44,6 +44,7 @@ }, "width": { "type": "number", + "exclusiveMinimum": 0, "description": "Column width in pixels" }, "align": { @@ -159,14 +160,16 @@ "type": "object", "properties": { "pageSize": { - "type": "number", + "type": "integer", + "exclusiveMinimum": 0, "default": 25, "description": "Number of records per page" }, "pageSizeOptions": { "type": "array", "items": { - "type": "number" + "type": "integer", + "exclusiveMinimum": 0 }, "description": "Available page size options" } diff --git a/packages/spec/json-schema/ui/PaginationConfig.json b/packages/spec/json-schema/ui/PaginationConfig.json index 7fffcecb2..663980d3b 100644 --- a/packages/spec/json-schema/ui/PaginationConfig.json +++ b/packages/spec/json-schema/ui/PaginationConfig.json @@ -5,14 +5,16 @@ "type": "object", "properties": { "pageSize": { - "type": "number", + "type": "integer", + "exclusiveMinimum": 0, "default": 25, "description": "Number of records per page" }, "pageSizeOptions": { "type": "array", "items": { - "type": "number" + "type": "integer", + "exclusiveMinimum": 0 }, "description": "Available page size options" } diff --git a/packages/spec/json-schema/ui/View.json b/packages/spec/json-schema/ui/View.json index c9b9ae30f..2913e7cdd 100644 --- a/packages/spec/json-schema/ui/View.json +++ b/packages/spec/json-schema/ui/View.json @@ -47,6 +47,7 @@ }, "width": { "type": "number", + "exclusiveMinimum": 0, "description": "Column width in pixels" }, "align": { @@ -162,14 +163,16 @@ "type": "object", "properties": { "pageSize": { - "type": "number", + "type": "integer", + "exclusiveMinimum": 0, "default": 25, "description": "Number of records per page" }, "pageSizeOptions": { "type": "array", "items": { - "type": "number" + "type": "integer", + "exclusiveMinimum": 0 }, "description": "Available page size options" } @@ -333,7 +336,9 @@ "description": "Hidden override" }, "colSpan": { - "type": "number", + "type": "integer", + "minimum": 1, + "maximum": 4, "description": "Column span in grid layout (1-4)" }, "widget": { @@ -429,7 +434,9 @@ "description": "Hidden override" }, "colSpan": { - "type": "number", + "type": "integer", + "minimum": 1, + "maximum": 4, "description": "Column span in grid layout (1-4)" }, "widget": { @@ -508,6 +515,7 @@ }, "width": { "type": "number", + "exclusiveMinimum": 0, "description": "Column width in pixels" }, "align": { @@ -623,14 +631,16 @@ "type": "object", "properties": { "pageSize": { - "type": "number", + "type": "integer", + "exclusiveMinimum": 0, "default": 25, "description": "Number of records per page" }, "pageSizeOptions": { "type": "array", "items": { - "type": "number" + "type": "integer", + "exclusiveMinimum": 0 }, "description": "Available page size options" } @@ -798,7 +808,9 @@ "description": "Hidden override" }, "colSpan": { - "type": "number", + "type": "integer", + "minimum": 1, + "maximum": 4, "description": "Column span in grid layout (1-4)" }, "widget": { @@ -894,7 +906,9 @@ "description": "Hidden override" }, "colSpan": { - "type": "number", + "type": "integer", + "minimum": 1, + "maximum": 4, "description": "Column span in grid layout (1-4)" }, "widget": { diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index b7d80fa38..2338a4055 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -7,7 +7,7 @@ import { z } from 'zod'; export const ListColumnSchema = z.object({ field: z.string().describe('Field name (snake_case)'), label: z.string().optional().describe('Display label override'), - width: z.number().optional().describe('Column width in pixels'), + width: z.number().positive().optional().describe('Column width in pixels'), align: z.enum(['left', 'center', 'right']).optional().describe('Text alignment'), hidden: z.boolean().optional().describe('Hide column by default'), sortable: z.boolean().optional().describe('Allow sorting by this column'), @@ -27,8 +27,8 @@ export const SelectionConfigSchema = z.object({ * List View Pagination Configuration */ export const PaginationConfigSchema = z.object({ - pageSize: z.number().default(25).describe('Number of records per page'), - pageSizeOptions: z.array(z.number()).optional().describe('Available page size options'), + pageSize: z.number().int().positive().default(25).describe('Number of records per page'), + pageSizeOptions: z.array(z.number().int().positive()).optional().describe('Available page size options'), }); /** @@ -115,7 +115,7 @@ export const FormFieldSchema = z.object({ readonly: z.boolean().optional().describe('Read-only override'), required: z.boolean().optional().describe('Required override'), hidden: z.boolean().optional().describe('Hidden override'), - colSpan: z.number().optional().describe('Column span in grid layout (1-4)'), + colSpan: z.number().int().min(1).max(4).optional().describe('Column span in grid layout (1-4)'), widget: z.string().optional().describe('Custom widget/component name'), dependsOn: z.string().optional().describe('Parent field name for cascading'), visibleOn: z.string().optional().describe('Visibility condition expression'), From 84a6427780373842cbc0230fcd9501bed139628f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:29:13 +0000 Subject: [PATCH 4/4] fix: add missing ViewDataSchema and validation tests - Add HttpMethodSchema, HttpRequestSchema, and ViewDataSchema from merged PR - Add validation tests for negative/boundary values in ListColumnSchema.width - Add validation tests for negative/zero/non-integer values in PaginationConfigSchema - Add validation tests for colSpan outside 1-4 range in FormFieldSchema - Fix documentation error in FormSection.mdx: fields type should be (string | object)[] not string | object[] - Fix duplicate type exports - All 257 UI tests passing Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- content/docs/references/ui/api/HttpMethod.mdx | 12 + content/docs/references/ui/view/FormView.mdx | 1 + .../docs/references/ui/view/HttpRequest.mdx | 14 + content/docs/references/ui/view/ListView.mdx | 1 + content/docs/references/ui/view/ViewData.mdx | 5 + packages/spec/json-schema/ui/FormView.json | 137 +++++ packages/spec/json-schema/ui/HttpMethod.json | 16 + packages/spec/json-schema/ui/HttpRequest.json | 46 ++ packages/spec/json-schema/ui/ListView.json | 137 +++++ packages/spec/json-schema/ui/View.json | 548 ++++++++++++++++++ packages/spec/json-schema/ui/ViewData.json | 142 +++++ packages/spec/src/ui/view.test.ts | 122 ++++ packages/spec/src/ui/view.zod.ts | 39 ++ 13 files changed, 1220 insertions(+) create mode 100644 content/docs/references/ui/api/HttpMethod.mdx create mode 100644 content/docs/references/ui/view/HttpRequest.mdx create mode 100644 content/docs/references/ui/view/ViewData.mdx create mode 100644 packages/spec/json-schema/ui/HttpMethod.json create mode 100644 packages/spec/json-schema/ui/HttpRequest.json create mode 100644 packages/spec/json-schema/ui/ViewData.json diff --git a/content/docs/references/ui/api/HttpMethod.mdx b/content/docs/references/ui/api/HttpMethod.mdx new file mode 100644 index 000000000..6373632f5 --- /dev/null +++ b/content/docs/references/ui/api/HttpMethod.mdx @@ -0,0 +1,12 @@ +--- +title: HttpMethod +description: HttpMethod Schema Reference +--- + +## Allowed Values + +* `GET` +* `POST` +* `PUT` +* `PATCH` +* `DELETE` \ No newline at end of file diff --git a/content/docs/references/ui/view/FormView.mdx b/content/docs/references/ui/view/FormView.mdx index bc4ab27ed..8216a98f4 100644 --- a/content/docs/references/ui/view/FormView.mdx +++ b/content/docs/references/ui/view/FormView.mdx @@ -8,5 +8,6 @@ description: FormView Schema Reference | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **type** | `Enum<'simple' \| 'tabbed' \| 'wizard'>` | optional | | +| **data** | `object \| object \| object` | optional | Data source configuration (defaults to "object" provider) | | **sections** | `object[]` | optional | | | **groups** | `object[]` | optional | | diff --git a/content/docs/references/ui/view/HttpRequest.mdx b/content/docs/references/ui/view/HttpRequest.mdx new file mode 100644 index 000000000..9e7e931dc --- /dev/null +++ b/content/docs/references/ui/view/HttpRequest.mdx @@ -0,0 +1,14 @@ +--- +title: HttpRequest +description: HttpRequest Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | API endpoint URL | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` | optional | HTTP method | +| **headers** | `Record` | optional | Custom HTTP headers | +| **params** | `Record` | optional | Query parameters | +| **body** | `any` | optional | Request body for POST/PUT/PATCH | diff --git a/content/docs/references/ui/view/ListView.mdx b/content/docs/references/ui/view/ListView.mdx index 3903e45a3..096a87c3a 100644 --- a/content/docs/references/ui/view/ListView.mdx +++ b/content/docs/references/ui/view/ListView.mdx @@ -10,6 +10,7 @@ description: ListView Schema Reference | **name** | `string` | optional | | | **label** | `string` | optional | | | **type** | `Enum<'grid' \| 'kanban' \| 'calendar' \| 'gantt' \| 'map'>` | optional | | +| **data** | `object \| object \| object` | optional | Data source configuration (defaults to "object" provider) | | **columns** | `string[] \| object[]` | ✅ | Fields to display as columns | | **filter** | `any[]` | optional | Filter criteria (JSON Rules) | | **sort** | `string \| object[]` | optional | | diff --git a/content/docs/references/ui/view/ViewData.mdx b/content/docs/references/ui/view/ViewData.mdx new file mode 100644 index 000000000..297acad9a --- /dev/null +++ b/content/docs/references/ui/view/ViewData.mdx @@ -0,0 +1,5 @@ +--- +title: ViewData +description: ViewData Schema Reference +--- + diff --git a/packages/spec/json-schema/ui/FormView.json b/packages/spec/json-schema/ui/FormView.json index f738791d2..3131cf9b7 100644 --- a/packages/spec/json-schema/ui/FormView.json +++ b/packages/spec/json-schema/ui/FormView.json @@ -13,6 +13,143 @@ ], "default": "simple" }, + "data": { + "anyOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "object" + }, + "object": { + "type": "string", + "description": "Target object name" + } + }, + "required": [ + "provider", + "object" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "api" + }, + "read": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for fetching data" + }, + "write": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for submitting data (for forms/editable tables)" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "value" + }, + "items": { + "type": "array", + "items": {}, + "description": "Static data array" + } + }, + "required": [ + "provider", + "items" + ], + "additionalProperties": false + } + ], + "description": "Data source configuration (defaults to \"object\" provider)" + }, "sections": { "type": "array", "items": { diff --git a/packages/spec/json-schema/ui/HttpMethod.json b/packages/spec/json-schema/ui/HttpMethod.json new file mode 100644 index 000000000..4036e3350 --- /dev/null +++ b/packages/spec/json-schema/ui/HttpMethod.json @@ -0,0 +1,16 @@ +{ + "$ref": "#/definitions/HttpMethod", + "definitions": { + "HttpMethod": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/ui/HttpRequest.json b/packages/spec/json-schema/ui/HttpRequest.json new file mode 100644 index 000000000..b95a99894 --- /dev/null +++ b/packages/spec/json-schema/ui/HttpRequest.json @@ -0,0 +1,46 @@ +{ + "$ref": "#/definitions/HttpRequest", + "definitions": { + "HttpRequest": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/ui/ListView.json b/packages/spec/json-schema/ui/ListView.json index e59da97df..896465f18 100644 --- a/packages/spec/json-schema/ui/ListView.json +++ b/packages/spec/json-schema/ui/ListView.json @@ -21,6 +21,143 @@ ], "default": "grid" }, + "data": { + "anyOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "object" + }, + "object": { + "type": "string", + "description": "Target object name" + } + }, + "required": [ + "provider", + "object" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "api" + }, + "read": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for fetching data" + }, + "write": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for submitting data (for forms/editable tables)" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "value" + }, + "items": { + "type": "array", + "items": {}, + "description": "Static data array" + } + }, + "required": [ + "provider", + "items" + ], + "additionalProperties": false + } + ], + "description": "Data source configuration (defaults to \"object\" provider)" + }, "columns": { "anyOf": [ { diff --git a/packages/spec/json-schema/ui/View.json b/packages/spec/json-schema/ui/View.json index 2913e7cdd..8f88368e5 100644 --- a/packages/spec/json-schema/ui/View.json +++ b/packages/spec/json-schema/ui/View.json @@ -24,6 +24,143 @@ ], "default": "grid" }, + "data": { + "anyOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "object" + }, + "object": { + "type": "string", + "description": "Target object name" + } + }, + "required": [ + "provider", + "object" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "api" + }, + "read": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for fetching data" + }, + "write": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for submitting data (for forms/editable tables)" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "value" + }, + "items": { + "type": "array", + "items": {}, + "description": "Static data array" + } + }, + "required": [ + "provider", + "items" + ], + "additionalProperties": false + } + ], + "description": "Data source configuration (defaults to \"object\" provider)" + }, "columns": { "anyOf": [ { @@ -271,6 +408,143 @@ ], "default": "simple" }, + "data": { + "anyOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "object" + }, + "object": { + "type": "string", + "description": "Target object name" + } + }, + "required": [ + "provider", + "object" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "api" + }, + "read": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for fetching data" + }, + "write": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for submitting data (for forms/editable tables)" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "value" + }, + "items": { + "type": "array", + "items": {}, + "description": "Static data array" + } + }, + "required": [ + "provider", + "items" + ], + "additionalProperties": false + } + ], + "description": "Data source configuration (defaults to \"object\" provider)" + }, "sections": { "type": "array", "items": { @@ -492,6 +766,143 @@ ], "default": "grid" }, + "data": { + "anyOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "object" + }, + "object": { + "type": "string", + "description": "Target object name" + } + }, + "required": [ + "provider", + "object" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "api" + }, + "read": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for fetching data" + }, + "write": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for submitting data (for forms/editable tables)" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "value" + }, + "items": { + "type": "array", + "items": {}, + "description": "Static data array" + } + }, + "required": [ + "provider", + "items" + ], + "additionalProperties": false + } + ], + "description": "Data source configuration (defaults to \"object\" provider)" + }, "columns": { "anyOf": [ { @@ -743,6 +1154,143 @@ ], "default": "simple" }, + "data": { + "anyOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "object" + }, + "object": { + "type": "string", + "description": "Target object name" + } + }, + "required": [ + "provider", + "object" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "api" + }, + "read": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for fetching data" + }, + "write": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for submitting data (for forms/editable tables)" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "value" + }, + "items": { + "type": "array", + "items": {}, + "description": "Static data array" + } + }, + "required": [ + "provider", + "items" + ], + "additionalProperties": false + } + ], + "description": "Data source configuration (defaults to \"object\" provider)" + }, "sections": { "type": "array", "items": { diff --git a/packages/spec/json-schema/ui/ViewData.json b/packages/spec/json-schema/ui/ViewData.json new file mode 100644 index 000000000..cb261ef85 --- /dev/null +++ b/packages/spec/json-schema/ui/ViewData.json @@ -0,0 +1,142 @@ +{ + "$ref": "#/definitions/ViewData", + "definitions": { + "ViewData": { + "anyOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "object" + }, + "object": { + "type": "string", + "description": "Target object name" + } + }, + "required": [ + "provider", + "object" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "api" + }, + "read": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for fetching data" + }, + "write": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API endpoint URL" + }, + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "default": "GET", + "description": "HTTP method" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom HTTP headers" + }, + "params": { + "type": "object", + "additionalProperties": {}, + "description": "Query parameters" + }, + "body": { + "description": "Request body for POST/PUT/PATCH" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Configuration for submitting data (for forms/editable tables)" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "value" + }, + "items": { + "type": "array", + "items": {}, + "description": "Static data array" + } + }, + "required": [ + "provider", + "items" + ], + "additionalProperties": false + } + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/src/ui/view.test.ts b/packages/spec/src/ui/view.test.ts index 463c092ea..721c34539 100644 --- a/packages/spec/src/ui/view.test.ts +++ b/packages/spec/src/ui/view.test.ts @@ -11,11 +11,16 @@ import { FormFieldSchema, SelectionConfigSchema, PaginationConfigSchema, + ViewDataSchema, + HttpRequestSchema, + HttpMethodSchema, type View, type ListView, type FormView, type ListColumn, type FormField, + type ViewData, + type HttpRequest, } from './view.zod'; describe('HttpMethodSchema', () => { @@ -781,6 +786,24 @@ describe('ListColumnSchema', () => { expect(() => ListColumnSchema.parse(column)).not.toThrow(); }); }); + + it('should reject negative width', () => { + const column = { + field: 'test_field', + width: -100, + }; + + expect(() => ListColumnSchema.parse(column)).toThrow(); + }); + + it('should reject zero width', () => { + const column = { + field: 'test_field', + width: 0, + }; + + expect(() => ListColumnSchema.parse(column)).toThrow(); + }); }); describe('SelectionConfigSchema', () => { @@ -826,6 +849,57 @@ describe('PaginationConfigSchema', () => { expect(() => PaginationConfigSchema.parse(pagination)).not.toThrow(); }); + + it('should reject negative pageSize', () => { + const pagination = { + pageSize: -10, + }; + + expect(() => PaginationConfigSchema.parse(pagination)).toThrow(); + }); + + it('should reject zero pageSize', () => { + const pagination = { + pageSize: 0, + }; + + expect(() => PaginationConfigSchema.parse(pagination)).toThrow(); + }); + + it('should reject non-integer pageSize', () => { + const pagination = { + pageSize: 25.5, + }; + + expect(() => PaginationConfigSchema.parse(pagination)).toThrow(); + }); + + it('should reject negative values in pageSizeOptions', () => { + const pagination = { + pageSize: 25, + pageSizeOptions: [10, -25, 50], + }; + + expect(() => PaginationConfigSchema.parse(pagination)).toThrow(); + }); + + it('should reject zero values in pageSizeOptions', () => { + const pagination = { + pageSize: 25, + pageSizeOptions: [10, 0, 50], + }; + + expect(() => PaginationConfigSchema.parse(pagination)).toThrow(); + }); + + it('should reject non-integer values in pageSizeOptions', () => { + const pagination = { + pageSize: 25, + pageSizeOptions: [10, 25.5, 50], + }; + + expect(() => PaginationConfigSchema.parse(pagination)).toThrow(); + }); }); describe('Enhanced ListViewSchema', () => { @@ -976,6 +1050,54 @@ describe('FormFieldSchema', () => { expect(() => FormFieldSchema.parse(field)).not.toThrow(); }); + + it('should reject colSpan less than 1', () => { + const field = { + field: 'test_field', + colSpan: 0, + }; + + expect(() => FormFieldSchema.parse(field)).toThrow(); + }); + + it('should reject colSpan greater than 4', () => { + const field = { + field: 'test_field', + colSpan: 5, + }; + + expect(() => FormFieldSchema.parse(field)).toThrow(); + }); + + it('should reject negative colSpan', () => { + const field = { + field: 'test_field', + colSpan: -1, + }; + + expect(() => FormFieldSchema.parse(field)).toThrow(); + }); + + it('should reject non-integer colSpan', () => { + const field = { + field: 'test_field', + colSpan: 2.5, + }; + + expect(() => FormFieldSchema.parse(field)).toThrow(); + }); + + it('should accept valid colSpan values (1-4)', () => { + const validColSpans = [1, 2, 3, 4]; + + validColSpans.forEach(colSpan => { + const field: FormField = { + field: 'test_field', + colSpan, + }; + expect(() => FormFieldSchema.parse(field)).not.toThrow(); + }); + }); }); describe('Enhanced FormSectionSchema', () => { diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index aae67cf1f..54553ade1 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1,5 +1,44 @@ import { z } from 'zod'; +/** + * HTTP Method Enum + */ +export const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); + +/** + * HTTP Request Configuration for API Provider + */ +export const HttpRequestSchema = z.object({ + url: z.string().describe('API endpoint URL'), + method: HttpMethodSchema.optional().default('GET').describe('HTTP method'), + headers: z.record(z.string()).optional().describe('Custom HTTP headers'), + params: z.record(z.unknown()).optional().describe('Query parameters'), + body: z.unknown().optional().describe('Request body for POST/PUT/PATCH'), +}); + +/** + * View Data Source Configuration + * Supports three modes: + * 1. 'object': Standard Protocol - Auto-connects to ObjectStack Metadata and Data APIs + * 2. 'api': Custom API - Explicitly provided API URLs + * 3. 'value': Static Data - Hardcoded data array + */ +export const ViewDataSchema = z.discriminatedUnion('provider', [ + z.object({ + provider: z.literal('object'), + object: z.string().describe('Target object name'), + }), + z.object({ + provider: z.literal('api'), + read: HttpRequestSchema.optional().describe('Configuration for fetching data'), + write: HttpRequestSchema.optional().describe('Configuration for submitting data (for forms/editable tables)'), + }), + z.object({ + provider: z.literal('value'), + items: z.array(z.unknown()).describe('Static data array'), + }), +]); + /** * List Column Configuration Schema * Detailed configuration for individual list view columns