From e5d98bf443a38e1173a4d542b6c31a655fc1287c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:51:04 +0000 Subject: [PATCH 1/5] Initial plan From b07284258ca6ba8c18f955aced160ef7990f8359 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:57:36 +0000 Subject: [PATCH 2/5] Fix TypeScript build errors in CRM example - Add phone field type helper to Field factory - Update Field.select/multiselect to support both array and object signatures - Add factory methods (Action.create, Dashboard.create, Report.create) - Use z.input and parse for factory methods to support default values Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- content/docs/references/ai/meta.json | 3 +- content/docs/references/data/meta.json | 1 + content/docs/references/meta.json | 1 - content/docs/references/system/meta.json | 1 + content/docs/references/ui/meta.json | 1 + packages/spec/src/data/field.zod.ts | 47 +++++++++++++++++++----- packages/spec/src/ui/action.zod.ts | 8 ++++ packages/spec/src/ui/dashboard.zod.ts | 7 ++++ packages/spec/src/ui/report.zod.ts | 10 +++++ 9 files changed, 67 insertions(+), 12 deletions(-) diff --git a/content/docs/references/ai/meta.json b/content/docs/references/ai/meta.json index 161f093f0..d22dfdb77 100644 --- a/content/docs/references/ai/meta.json +++ b/content/docs/references/ai/meta.json @@ -1,3 +1,4 @@ { - "title": "AI Protocol" + "title": "AI Protocol", + "root": true } \ No newline at end of file diff --git a/content/docs/references/data/meta.json b/content/docs/references/data/meta.json index 7929d9d3c..f4047f698 100644 --- a/content/docs/references/data/meta.json +++ b/content/docs/references/data/meta.json @@ -1,5 +1,6 @@ { "title": "Data Protocol", + "root": true, "pages": [ "core", "logic", diff --git a/content/docs/references/meta.json b/content/docs/references/meta.json index 4836dcd5d..bd92db030 100644 --- a/content/docs/references/meta.json +++ b/content/docs/references/meta.json @@ -1,6 +1,5 @@ { "label": "Protocol Reference", - "root": true, "order": 100, "pages": [ "data", diff --git a/content/docs/references/system/meta.json b/content/docs/references/system/meta.json index 97cd1ba65..8c557d803 100644 --- a/content/docs/references/system/meta.json +++ b/content/docs/references/system/meta.json @@ -1,5 +1,6 @@ { "title": "System Protocol", + "root": true, "pages": [ "identity", "integration", diff --git a/content/docs/references/ui/meta.json b/content/docs/references/ui/meta.json index 762100efc..282495744 100644 --- a/content/docs/references/ui/meta.json +++ b/content/docs/references/ui/meta.json @@ -1,5 +1,6 @@ { "title": "UI Protocol", + "root": true, "pages": [ "app", "views", diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 56fb2afb4..919c8dc2e 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -111,6 +111,7 @@ export const Field = { percent: (config: FieldInput = {}) => ({ type: 'percent', ...config } as const), url: (config: FieldInput = {}) => ({ type: 'url', ...config } as const), email: (config: FieldInput = {}) => ({ type: 'email', ...config } as const), + phone: (config: FieldInput = {}) => ({ type: 'phone', ...config } as const), image: (config: FieldInput = {}) => ({ type: 'image', ...config } as const), file: (config: FieldInput = {}) => ({ type: 'file', ...config } as const), avatar: (config: FieldInput = {}) => ({ type: 'avatar', ...config } as const), @@ -121,17 +122,43 @@ export const Field = { html: (config: FieldInput = {}) => ({ type: 'html', ...config } as const), password: (config: FieldInput = {}) => ({ type: 'password', ...config } as const), - select: (options: string[], config: FieldInput = {}) => ({ - type: 'select', - options: options.map(o => ({ label: o, value: o })), - ...config - } as const), + select: (optionsOrConfig: SelectOption[] | string[] | FieldInput & { options: SelectOption[] | string[] }, config?: FieldInput) => { + // Support both old and new signatures: + // Old: Field.select(['a', 'b'], { label: 'X' }) + // New: Field.select({ options: [{label: 'A', value: 'a'}], label: 'X' }) + let options: SelectOption[]; + let finalConfig: FieldInput; + + if (Array.isArray(optionsOrConfig)) { + // Old signature: array as first param + options = optionsOrConfig.map(o => typeof o === 'string' ? { label: o, value: o } : o); + finalConfig = config || {}; + } else { + // New signature: config object with options + options = (optionsOrConfig.options || []).map(o => typeof o === 'string' ? { label: o, value: o } : o); + finalConfig = optionsOrConfig; + } + + return { type: 'select', ...finalConfig, options } as const; + }, - multiselect: (options: string[], config: FieldInput = {}) => ({ - type: 'multiselect', - options: options.map(o => ({ label: o, value: o })), - ...config - } as const), + multiselect: (optionsOrConfig: SelectOption[] | string[] | FieldInput & { options: SelectOption[] | string[] }, config?: FieldInput) => { + // Support both old and new signatures + let options: SelectOption[]; + let finalConfig: FieldInput; + + if (Array.isArray(optionsOrConfig)) { + // Old signature: array as first param + options = optionsOrConfig.map(o => typeof o === 'string' ? { label: o, value: o } : o); + finalConfig = config || {}; + } else { + // New signature: config object with options + options = (optionsOrConfig.options || []).map(o => typeof o === 'string' ? { label: o, value: o } : o); + finalConfig = optionsOrConfig; + } + + return { type: 'multiselect', ...finalConfig, options } as const; + }, lookup: (reference: string, config: FieldInput = {}) => ({ type: 'lookup', diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index f2b4497fe..33f3af4af 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -56,3 +56,11 @@ export const ActionSchema = z.object({ }); export type Action = z.infer; +export type ActionParam = z.infer; + +/** + * Action Factory Helper + */ +export const Action = { + create: (config: z.input): Action => ActionSchema.parse(config), +} as const; diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index 60fa27e57..6b399491a 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -78,3 +78,10 @@ export const DashboardSchema = z.object({ export type Dashboard = z.infer; export type DashboardWidget = z.infer; + +/** + * Dashboard Factory Helper + */ +export const Dashboard = { + create: (config: z.input): Dashboard => DashboardSchema.parse(config), +} as const; diff --git a/packages/spec/src/ui/report.zod.ts b/packages/spec/src/ui/report.zod.ts index 18ec7fac9..44d601e6c 100644 --- a/packages/spec/src/ui/report.zod.ts +++ b/packages/spec/src/ui/report.zod.ts @@ -76,3 +76,13 @@ export const ReportSchema = z.object({ }); export type Report = z.infer; +export type ReportColumn = z.infer; +export type ReportGrouping = z.infer; +export type ReportChart = z.infer; + +/** + * Report Factory Helper + */ +export const Report = { + create: (config: z.input): Report => ReportSchema.parse(config), +} as const; From 4326c5a746228f84cbca5451272b72b148537d24 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:02:07 +0000 Subject: [PATCH 3/5] Add comprehensive tests for Field and Action factory helpers - Add tests for Field.phone() helper - Add tests for Field.select() backward compatibility (old and new API) - Add tests for Field.multiselect() backward compatibility - Add tests for Action.create() with default value handling - Add tests for Dashboard.create() with default value handling - All 279 tests passing Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- packages/spec/src/data/field.test.ts | 121 ++++++++++++++++++++++++- packages/spec/src/ui/action.test.ts | 93 ++++++++++++++----- packages/spec/src/ui/dashboard.test.ts | 52 +++++++++-- 3 files changed, 236 insertions(+), 30 deletions(-) diff --git a/packages/spec/src/data/field.test.ts b/packages/spec/src/data/field.test.ts index 524d0a4d4..e70c8b1b2 100644 --- a/packages/spec/src/data/field.test.ts +++ b/packages/spec/src/data/field.test.ts @@ -3,7 +3,7 @@ import { FieldSchema, FieldType, SelectOptionSchema, - type Field, + Field, type SelectOption } from './field.zod'; @@ -304,3 +304,122 @@ describe('FieldSchema', () => { }); }); }); + +describe('Field Factory Helpers', () => { + describe('Basic Field Types', () => { + it('should create phone field', () => { + const phoneField = Field.phone({ label: 'Mobile Phone', required: true }); + + expect(phoneField.type).toBe('phone'); + expect(phoneField.label).toBe('Mobile Phone'); + expect(phoneField.required).toBe(true); + }); + + it('should create text field', () => { + const textField = Field.text({ label: 'Name', maxLength: 100 }); + + expect(textField.type).toBe('text'); + expect(textField.label).toBe('Name'); + expect(textField.maxLength).toBe(100); + }); + + it('should create email field', () => { + const emailField = Field.email({ label: 'Email Address' }); + + expect(emailField.type).toBe('email'); + expect(emailField.label).toBe('Email Address'); + }); + }); + + describe('Select Field Factory', () => { + it('should create select field with string array (old API)', () => { + const selectField = Field.select(['High', 'Medium', 'Low'], { label: 'Priority' }); + + expect(selectField.type).toBe('select'); + expect(selectField.label).toBe('Priority'); + expect(selectField.options).toHaveLength(3); + expect(selectField.options[0]).toEqual({ label: 'High', value: 'High' }); + }); + + it('should create select field with SelectOption array in config (new API)', () => { + const selectField = Field.select({ + label: 'Priority', + options: [ + { label: 'High Priority', value: 'high', color: '#FF0000' }, + { label: 'Low Priority', value: 'low', color: '#00FF00' }, + ], + }); + + expect(selectField.type).toBe('select'); + expect(selectField.label).toBe('Priority'); + expect(selectField.options).toHaveLength(2); + expect(selectField.options[0].color).toBe('#FF0000'); + expect(selectField.options[1].value).toBe('low'); + }); + + it('should create select field with mixed string/object array (new API)', () => { + const selectField = Field.select({ + label: 'Status', + options: [ + { label: 'Active', value: 'active', color: '#00AA00' }, + 'Inactive', + 'Pending', + ], + }); + + expect(selectField.type).toBe('select'); + expect(selectField.options).toHaveLength(3); + expect(selectField.options[0]).toEqual({ label: 'Active', value: 'active', color: '#00AA00' }); + expect(selectField.options[1]).toEqual({ label: 'Inactive', value: 'Inactive' }); + expect(selectField.options[2]).toEqual({ label: 'Pending', value: 'Pending' }); + }); + }); + + describe('Multiselect Field Factory', () => { + it('should create multiselect field with string array (old API)', () => { + const multiselectField = Field.multiselect(['Tag1', 'Tag2', 'Tag3'], { label: 'Tags' }); + + expect(multiselectField.type).toBe('multiselect'); + expect(multiselectField.label).toBe('Tags'); + expect(multiselectField.options).toHaveLength(3); + }); + + it('should create multiselect field with SelectOption array (new API)', () => { + const multiselectField = Field.multiselect({ + label: 'Categories', + options: [ + { label: 'Technology', value: 'tech' }, + { label: 'Business', value: 'biz' }, + ], + }); + + expect(multiselectField.type).toBe('multiselect'); + expect(multiselectField.options).toHaveLength(2); + expect(multiselectField.options[0].value).toBe('tech'); + }); + }); + + describe('Lookup and Master-Detail Fields', () => { + it('should create lookup field', () => { + const lookupField = Field.lookup('account', { + label: 'Account', + referenceFilters: ['status = "active"'], + }); + + expect(lookupField.type).toBe('lookup'); + expect(lookupField.reference).toBe('account'); + expect(lookupField.label).toBe('Account'); + }); + + it('should create master_detail field', () => { + const masterDetailField = Field.master_detail('parent_object', { + label: 'Parent', + deleteBehavior: 'cascade', + }); + + expect(masterDetailField.type).toBe('master_detail'); + expect(masterDetailField.reference).toBe('parent_object'); + expect(masterDetailField.deleteBehavior).toBe('cascade'); + }); + }); +}); diff --git a/packages/spec/src/ui/action.test.ts b/packages/spec/src/ui/action.test.ts index f73a1bcf7..a8d1ca9e2 100644 --- a/packages/spec/src/ui/action.test.ts +++ b/packages/spec/src/ui/action.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { ActionSchema, ActionParamSchema, type Action } from './action.zod'; +import { ActionSchema, ActionParamSchema, Action, type Action as ActionType } from './action.zod'; describe('ActionParamSchema', () => { it('should accept minimal action parameter', () => { @@ -43,7 +43,7 @@ describe('ActionParamSchema', () => { describe('ActionSchema', () => { describe('Basic Action Properties', () => { it('should accept minimal action', () => { - const action: Action = { + const action: ActionType = { name: 'approve', label: 'Approve', }; @@ -66,7 +66,7 @@ describe('ActionSchema', () => { }); it('should accept action with icon', () => { - const action: Action = { + const action: ActionType = { name: 'delete_record', label: 'Delete', icon: 'trash-2', @@ -81,7 +81,7 @@ describe('ActionSchema', () => { const types = ['script', 'url', 'modal', 'flow', 'api'] as const; types.forEach(type => { - const action: Action = { + const action: ActionType = { name: 'test_action', label: 'Test', type, @@ -112,7 +112,7 @@ describe('ActionSchema', () => { 'global_nav', ] as const; - const action: Action = { + const action: ActionType = { name: 'multi_location', label: 'Multi Location', locations, @@ -122,7 +122,7 @@ describe('ActionSchema', () => { }); it('should accept single location', () => { - const action: Action = { + const action: ActionType = { name: 'toolbar_action', label: 'Toolbar Action', locations: ['list_toolbar'], @@ -134,7 +134,7 @@ describe('ActionSchema', () => { describe('Action Targets', () => { it('should accept URL action with target', () => { - const action: Action = { + const action: ActionType = { name: 'open_external', label: 'Open External', type: 'url', @@ -145,7 +145,7 @@ describe('ActionSchema', () => { }); it('should accept flow action with target', () => { - const action: Action = { + const action: ActionType = { name: 'run_approval_flow', label: 'Run Approval', type: 'flow', @@ -156,7 +156,7 @@ describe('ActionSchema', () => { }); it('should accept API action with target', () => { - const action: Action = { + const action: ActionType = { name: 'call_api', label: 'Call API', type: 'api', @@ -169,7 +169,7 @@ describe('ActionSchema', () => { describe('Action Parameters', () => { it('should accept action with parameters', () => { - const action: Action = { + const action: ActionType = { name: 'transfer_ownership', label: 'Transfer Ownership', type: 'script', @@ -193,7 +193,7 @@ describe('ActionSchema', () => { }); it('should accept action with select parameter', () => { - const action: Action = { + const action: ActionType = { name: 'change_status', label: 'Change Status', params: [ @@ -216,7 +216,7 @@ describe('ActionSchema', () => { describe('UX Behavior', () => { it('should accept action with confirmation', () => { - const action: Action = { + const action: ActionType = { name: 'delete_all', label: 'Delete All', confirmText: 'Are you sure you want to delete all records? This cannot be undone.', @@ -226,7 +226,7 @@ describe('ActionSchema', () => { }); it('should accept action with success message', () => { - const action: Action = { + const action: ActionType = { name: 'send_notification', label: 'Send Notification', successMessage: 'Notification sent successfully!', @@ -236,7 +236,7 @@ describe('ActionSchema', () => { }); it('should accept action that refreshes view', () => { - const action: Action = { + const action: ActionType = { name: 'update_status', label: 'Update Status', refreshAfter: true, @@ -246,7 +246,7 @@ describe('ActionSchema', () => { }); it('should accept action with all UX properties', () => { - const action: Action = { + const action: ActionType = { name: 'complete_task', label: 'Complete Task', confirmText: 'Mark this task as complete?', @@ -260,7 +260,7 @@ describe('ActionSchema', () => { describe('Visibility Control', () => { it('should accept action with visibility formula', () => { - const action: Action = { + const action: ActionType = { name: 'approve', label: 'Approve', visible: 'status == "pending" && user.can_approve', @@ -272,7 +272,7 @@ describe('ActionSchema', () => { describe('Real-World Action Examples', () => { it('should accept approve opportunity action', () => { - const approveAction: Action = { + const approveAction: ActionType = { name: 'approve_opportunity', label: 'Approve', icon: 'check-circle', @@ -289,7 +289,7 @@ describe('ActionSchema', () => { }); it('should accept transfer case action with parameters', () => { - const transferAction: Action = { + const transferAction: ActionType = { name: 'transfer_case', label: 'Transfer Case', icon: 'arrow-right', @@ -323,7 +323,7 @@ describe('ActionSchema', () => { }); it('should accept send email action', () => { - const emailAction: Action = { + const emailAction: ActionType = { name: 'send_quote', label: 'Send Quote', icon: 'mail', @@ -355,7 +355,7 @@ describe('ActionSchema', () => { }); it('should accept export to Excel action', () => { - const exportAction: Action = { + const exportAction: ActionType = { name: 'export_excel', label: 'Export to Excel', icon: 'file-spreadsheet', @@ -369,7 +369,7 @@ describe('ActionSchema', () => { }); it('should accept delete action with confirmation', () => { - const deleteAction: Action = { + const deleteAction: ActionType = { name: 'delete_record', label: 'Delete', icon: 'trash-2', @@ -386,7 +386,7 @@ describe('ActionSchema', () => { }); it('should accept clone record action', () => { - const cloneAction: Action = { + const cloneAction: ActionType = { name: 'clone_record', label: 'Clone', icon: 'copy', @@ -409,7 +409,7 @@ describe('ActionSchema', () => { }); it('should accept open external link action', () => { - const linkAction: Action = { + const linkAction: ActionType = { name: 'view_on_map', label: 'View on Map', icon: 'map-pin', @@ -423,3 +423,50 @@ describe('ActionSchema', () => { }); }); }); + +describe('Action Factory', () => { + it('should create action with default values via factory', () => { + const action = Action.create({ + name: 'test_action', + label: 'Test Action', + }); + + expect(action.name).toBe('test_action'); + expect(action.label).toBe('Test Action'); + expect(action.type).toBe('script'); + expect(action.refreshAfter).toBe(false); + }); + + it('should create action without refreshAfter property (uses default)', () => { + const action = Action.create({ + name: 'send_email', + label: 'Send Email', + type: 'flow', + target: 'email_flow', + }); + + expect(action.refreshAfter).toBe(false); + }); + + it('should create action with explicit refreshAfter', () => { + const action = Action.create({ + name: 'update_record', + label: 'Update', + refreshAfter: true, + }); + + expect(action.refreshAfter).toBe(true); + }); + + it('should validate snake_case name in factory', () => { + expect(() => Action.create({ + name: 'invalidName', + label: 'Invalid', + })).toThrow(); + + expect(() => Action.create({ + name: 'valid_name', + label: 'Valid', + })).not.toThrow(); + }); +}); diff --git a/packages/spec/src/ui/dashboard.test.ts b/packages/spec/src/ui/dashboard.test.ts index 489932e91..17ca2ae5b 100644 --- a/packages/spec/src/ui/dashboard.test.ts +++ b/packages/spec/src/ui/dashboard.test.ts @@ -3,7 +3,8 @@ import { DashboardSchema, DashboardWidgetSchema, ChartType, - type Dashboard, + Dashboard, + type Dashboard as DashboardType, type DashboardWidget, } from './dashboard.zod'; @@ -163,7 +164,7 @@ describe('DashboardWidgetSchema', () => { describe('DashboardSchema', () => { it('should accept minimal dashboard', () => { - const dashboard: Dashboard = { + const dashboard: DashboardType = { name: 'sales_overview', label: 'Sales Overview', widgets: [], @@ -185,7 +186,7 @@ describe('DashboardSchema', () => { }); it('should accept dashboard with description', () => { - const dashboard: Dashboard = { + const dashboard: DashboardType = { name: 'executive_dashboard', label: 'Executive Dashboard', description: 'High-level metrics for executive team', @@ -197,7 +198,7 @@ describe('DashboardSchema', () => { describe('Real-World Dashboard Examples', () => { it('should accept sales pipeline dashboard', () => { - const salesDashboard: Dashboard = { + const salesDashboard: DashboardType = { name: 'sales_pipeline', label: 'Sales Pipeline', description: 'Overview of sales opportunities and pipeline health', @@ -281,7 +282,7 @@ describe('DashboardSchema', () => { }); it('should accept service desk dashboard', () => { - const serviceDashboard: Dashboard = { + const serviceDashboard: DashboardType = { name: 'service_desk', label: 'Service Desk Overview', description: 'Customer support metrics and case tracking', @@ -367,7 +368,7 @@ describe('DashboardSchema', () => { }); it('should accept executive dashboard with mixed widgets', () => { - const executiveDashboard: Dashboard = { + const executiveDashboard: DashboardType = { name: 'executive_overview', label: 'Executive Overview', description: 'Key business metrics at a glance', @@ -439,3 +440,42 @@ describe('DashboardSchema', () => { }); }); }); + +describe('Dashboard Factory', () => { + it('should create dashboard with default widget values', () => { + const dashboard = Dashboard.create({ + name: 'test_dashboard', + label: 'Test Dashboard', + widgets: [ + { + title: 'Test Widget', + type: 'table', + object: 'account', + layout: { x: 0, y: 0, w: 12, h: 4 }, + }, + ], + }); + + expect(dashboard.name).toBe('test_dashboard'); + expect(dashboard.widgets).toHaveLength(1); + expect(dashboard.widgets[0].aggregate).toBe('count'); + }); + + it('should create dashboard without aggregate (uses default)', () => { + const dashboard = Dashboard.create({ + name: 'sales_dashboard', + label: 'Sales Dashboard', + widgets: [ + { + title: 'Total Revenue', + type: 'metric', + object: 'opportunity', + valueField: 'amount', + layout: { x: 0, y: 0, w: 3, h: 2 }, + }, + ], + }); + + expect(dashboard.widgets[0].aggregate).toBe('count'); + }); +}); From 95614cdbb5b8ad7eae55dd80aeaf0e8be630a95d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:05:28 +0000 Subject: [PATCH 4/5] Add JSDoc comments for select/multiselect field helpers Document backward-compatible API with examples for both old and new signatures Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- packages/spec/src/data/field.zod.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 919c8dc2e..27e944117 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -122,6 +122,15 @@ export const Field = { html: (config: FieldInput = {}) => ({ type: 'html', ...config } as const), password: (config: FieldInput = {}) => ({ type: 'password', ...config } as const), + /** + * Select field helper with backward-compatible API + * + * @example Old API (array first) + * Field.select(['High', 'Low'], { label: 'Priority' }) + * + * @example New API (config object) + * Field.select({ options: [{label: 'High', value: 'high'}], label: 'Priority' }) + */ select: (optionsOrConfig: SelectOption[] | string[] | FieldInput & { options: SelectOption[] | string[] }, config?: FieldInput) => { // Support both old and new signatures: // Old: Field.select(['a', 'b'], { label: 'X' }) @@ -142,6 +151,15 @@ export const Field = { return { type: 'select', ...finalConfig, options } as const; }, + /** + * Multiselect field helper with backward-compatible API + * + * @example Old API (array first) + * Field.multiselect(['Tag1', 'Tag2'], { label: 'Tags' }) + * + * @example New API (config object) + * Field.multiselect({ options: [{label: 'Tag 1', value: 'tag1'}], label: 'Tags' }) + */ multiselect: (optionsOrConfig: SelectOption[] | string[] | FieldInput & { options: SelectOption[] | string[] }, config?: FieldInput) => { // Support both old and new signatures let options: SelectOption[]; From 49030d18c80804a80be84b923f04904f4b41e81f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:11:31 +0000 Subject: [PATCH 5/5] Fix spread order in select/multiselect field helpers - Extract options from config object to avoid confusion - Change spread order to options first, then finalConfig - Ensures processed options always take precedence Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- packages/spec/src/data/field.zod.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 27e944117..efb76dc58 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -145,10 +145,12 @@ export const Field = { } else { // New signature: config object with options options = (optionsOrConfig.options || []).map(o => typeof o === 'string' ? { label: o, value: o } : o); - finalConfig = optionsOrConfig; + // Remove options from config to avoid confusion + const { options: _, ...restConfig } = optionsOrConfig; + finalConfig = restConfig; } - return { type: 'select', ...finalConfig, options } as const; + return { type: 'select', options, ...finalConfig } as const; }, /** @@ -172,10 +174,12 @@ export const Field = { } else { // New signature: config object with options options = (optionsOrConfig.options || []).map(o => typeof o === 'string' ? { label: o, value: o } : o); - finalConfig = optionsOrConfig; + // Remove options from config to avoid confusion + const { options: _, ...restConfig } = optionsOrConfig; + finalConfig = restConfig; } - return { type: 'multiselect', ...finalConfig, options } as const; + return { type: 'multiselect', options, ...finalConfig } as const; }, lookup: (reference: string, config: FieldInput = {}) => ({