Skip to content

Commit 8beab63

Browse files
authored
Merge pull request #15 from objectstack-ai/copilot/fix-action-job-step-error
2 parents 7013316 + 49030d1 commit 8beab63

7 files changed

Lines changed: 320 additions & 40 deletions

File tree

packages/spec/src/data/field.test.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
FieldSchema,
44
FieldType,
55
SelectOptionSchema,
6-
type Field,
6+
Field,
77
type SelectOption
88
} from './field.zod';
99

@@ -304,3 +304,122 @@ describe('FieldSchema', () => {
304304
});
305305
});
306306
});
307+
308+
describe('Field Factory Helpers', () => {
309+
describe('Basic Field Types', () => {
310+
it('should create phone field', () => {
311+
const phoneField = Field.phone({ label: 'Mobile Phone', required: true });
312+
313+
expect(phoneField.type).toBe('phone');
314+
expect(phoneField.label).toBe('Mobile Phone');
315+
expect(phoneField.required).toBe(true);
316+
});
317+
318+
it('should create text field', () => {
319+
const textField = Field.text({ label: 'Name', maxLength: 100 });
320+
321+
expect(textField.type).toBe('text');
322+
expect(textField.label).toBe('Name');
323+
expect(textField.maxLength).toBe(100);
324+
});
325+
326+
it('should create email field', () => {
327+
const emailField = Field.email({ label: 'Email Address' });
328+
329+
expect(emailField.type).toBe('email');
330+
expect(emailField.label).toBe('Email Address');
331+
});
332+
});
333+
334+
describe('Select Field Factory', () => {
335+
it('should create select field with string array (old API)', () => {
336+
const selectField = Field.select(['High', 'Medium', 'Low'], { label: 'Priority' });
337+
338+
expect(selectField.type).toBe('select');
339+
expect(selectField.label).toBe('Priority');
340+
expect(selectField.options).toHaveLength(3);
341+
expect(selectField.options[0]).toEqual({ label: 'High', value: 'High' });
342+
});
343+
344+
it('should create select field with SelectOption array in config (new API)', () => {
345+
const selectField = Field.select({
346+
label: 'Priority',
347+
options: [
348+
{ label: 'High Priority', value: 'high', color: '#FF0000' },
349+
{ label: 'Low Priority', value: 'low', color: '#00FF00' },
350+
],
351+
});
352+
353+
expect(selectField.type).toBe('select');
354+
expect(selectField.label).toBe('Priority');
355+
expect(selectField.options).toHaveLength(2);
356+
expect(selectField.options[0].color).toBe('#FF0000');
357+
expect(selectField.options[1].value).toBe('low');
358+
});
359+
360+
it('should create select field with mixed string/object array (new API)', () => {
361+
const selectField = Field.select({
362+
label: 'Status',
363+
options: [
364+
{ label: 'Active', value: 'active', color: '#00AA00' },
365+
'Inactive',
366+
'Pending',
367+
],
368+
});
369+
370+
expect(selectField.type).toBe('select');
371+
expect(selectField.options).toHaveLength(3);
372+
expect(selectField.options[0]).toEqual({ label: 'Active', value: 'active', color: '#00AA00' });
373+
expect(selectField.options[1]).toEqual({ label: 'Inactive', value: 'Inactive' });
374+
expect(selectField.options[2]).toEqual({ label: 'Pending', value: 'Pending' });
375+
});
376+
});
377+
378+
describe('Multiselect Field Factory', () => {
379+
it('should create multiselect field with string array (old API)', () => {
380+
const multiselectField = Field.multiselect(['Tag1', 'Tag2', 'Tag3'], { label: 'Tags' });
381+
382+
expect(multiselectField.type).toBe('multiselect');
383+
expect(multiselectField.label).toBe('Tags');
384+
expect(multiselectField.options).toHaveLength(3);
385+
});
386+
387+
it('should create multiselect field with SelectOption array (new API)', () => {
388+
const multiselectField = Field.multiselect({
389+
label: 'Categories',
390+
options: [
391+
{ label: 'Technology', value: 'tech' },
392+
{ label: 'Business', value: 'biz' },
393+
],
394+
});
395+
396+
expect(multiselectField.type).toBe('multiselect');
397+
expect(multiselectField.options).toHaveLength(2);
398+
expect(multiselectField.options[0].value).toBe('tech');
399+
});
400+
});
401+
402+
describe('Lookup and Master-Detail Fields', () => {
403+
it('should create lookup field', () => {
404+
const lookupField = Field.lookup('account', {
405+
label: 'Account',
406+
referenceFilters: ['status = "active"'],
407+
});
408+
409+
expect(lookupField.type).toBe('lookup');
410+
expect(lookupField.reference).toBe('account');
411+
expect(lookupField.label).toBe('Account');
412+
});
413+
414+
it('should create master_detail field', () => {
415+
const masterDetailField = Field.master_detail('parent_object', {
416+
label: 'Parent',
417+
deleteBehavior: 'cascade',
418+
});
419+
420+
expect(masterDetailField.type).toBe('master_detail');
421+
expect(masterDetailField.reference).toBe('parent_object');
422+
expect(masterDetailField.deleteBehavior).toBe('cascade');
423+
});
424+
});
425+
});

packages/spec/src/data/field.zod.ts

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export const Field = {
111111
percent: (config: FieldInput = {}) => ({ type: 'percent', ...config } as const),
112112
url: (config: FieldInput = {}) => ({ type: 'url', ...config } as const),
113113
email: (config: FieldInput = {}) => ({ type: 'email', ...config } as const),
114+
phone: (config: FieldInput = {}) => ({ type: 'phone', ...config } as const),
114115
image: (config: FieldInput = {}) => ({ type: 'image', ...config } as const),
115116
file: (config: FieldInput = {}) => ({ type: 'file', ...config } as const),
116117
avatar: (config: FieldInput = {}) => ({ type: 'avatar', ...config } as const),
@@ -121,17 +122,65 @@ export const Field = {
121122
html: (config: FieldInput = {}) => ({ type: 'html', ...config } as const),
122123
password: (config: FieldInput = {}) => ({ type: 'password', ...config } as const),
123124

124-
select: (options: string[], config: FieldInput = {}) => ({
125-
type: 'select',
126-
options: options.map(o => ({ label: o, value: o })),
127-
...config
128-
} as const),
125+
/**
126+
* Select field helper with backward-compatible API
127+
*
128+
* @example Old API (array first)
129+
* Field.select(['High', 'Low'], { label: 'Priority' })
130+
*
131+
* @example New API (config object)
132+
* Field.select({ options: [{label: 'High', value: 'high'}], label: 'Priority' })
133+
*/
134+
select: (optionsOrConfig: SelectOption[] | string[] | FieldInput & { options: SelectOption[] | string[] }, config?: FieldInput) => {
135+
// Support both old and new signatures:
136+
// Old: Field.select(['a', 'b'], { label: 'X' })
137+
// New: Field.select({ options: [{label: 'A', value: 'a'}], label: 'X' })
138+
let options: SelectOption[];
139+
let finalConfig: FieldInput;
140+
141+
if (Array.isArray(optionsOrConfig)) {
142+
// Old signature: array as first param
143+
options = optionsOrConfig.map(o => typeof o === 'string' ? { label: o, value: o } : o);
144+
finalConfig = config || {};
145+
} else {
146+
// New signature: config object with options
147+
options = (optionsOrConfig.options || []).map(o => typeof o === 'string' ? { label: o, value: o } : o);
148+
// Remove options from config to avoid confusion
149+
const { options: _, ...restConfig } = optionsOrConfig;
150+
finalConfig = restConfig;
151+
}
152+
153+
return { type: 'select', options, ...finalConfig } as const;
154+
},
129155

130-
multiselect: (options: string[], config: FieldInput = {}) => ({
131-
type: 'multiselect',
132-
options: options.map(o => ({ label: o, value: o })),
133-
...config
134-
} as const),
156+
/**
157+
* Multiselect field helper with backward-compatible API
158+
*
159+
* @example Old API (array first)
160+
* Field.multiselect(['Tag1', 'Tag2'], { label: 'Tags' })
161+
*
162+
* @example New API (config object)
163+
* Field.multiselect({ options: [{label: 'Tag 1', value: 'tag1'}], label: 'Tags' })
164+
*/
165+
multiselect: (optionsOrConfig: SelectOption[] | string[] | FieldInput & { options: SelectOption[] | string[] }, config?: FieldInput) => {
166+
// Support both old and new signatures
167+
let options: SelectOption[];
168+
let finalConfig: FieldInput;
169+
170+
if (Array.isArray(optionsOrConfig)) {
171+
// Old signature: array as first param
172+
options = optionsOrConfig.map(o => typeof o === 'string' ? { label: o, value: o } : o);
173+
finalConfig = config || {};
174+
} else {
175+
// New signature: config object with options
176+
options = (optionsOrConfig.options || []).map(o => typeof o === 'string' ? { label: o, value: o } : o);
177+
// Remove options from config to avoid confusion
178+
const { options: _, ...restConfig } = optionsOrConfig;
179+
finalConfig = restConfig;
180+
}
181+
182+
return { type: 'multiselect', options, ...finalConfig } as const;
183+
},
135184

136185
lookup: (reference: string, config: FieldInput = {}) => ({
137186
type: 'lookup',

0 commit comments

Comments
 (0)