Skip to content

Commit eae41b9

Browse files
Copilothotlong
andcommitted
feat(ui): add source, groupBy, typeOptions fields to ListViewSchema
Standardize UI extension fields from objectui console into the spec: - source: shorthand for source object name (UI display) - groupBy: simple field name for quick grouping - typeOptions: extensible key-value map for view-type-specific config - description: already existed, no changes needed All fields are optional for backward compatibility. Includes 13 new tests covering all new fields. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 29fce01 commit eae41b9

3 files changed

Lines changed: 204 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ business/custom objects, aligning with industry best practices (e.g., ServiceNow
318318

319319
- [x] **Data Protocol** — Object, Field (35+ types), Query, Filter, Validation, Hook, Datasource, Dataset, Analytics, Document, Storage Name Mapping (`tableName`/`columnName`), Feed & Activity Timeline (FeedItem, Comment, Mention, Reaction, FieldChange), Record Subscription (notification channels)
320320
- [x] **Driver Specifications** — Memory, PostgreSQL, MongoDB driver schemas + SQL/NoSQL abstractions
321-
- [x] **UI Protocol** — View (List/Form/Kanban/Calendar/Gantt), App, Dashboard, Report, Action, Page (16 types), Chart, Widget, Theme, Animation, DnD, Touch, Keyboard, Responsive, Offline, Notification, i18n, Content Elements, Enhanced Activity Timeline (`RecordActivityProps` unified timeline, `RecordChatterProps` sidebar/drawer), Unified Navigation Protocol (`NavigationItem` as single source of truth with 7 types: object/dashboard/page/url/report/action/group; `NavigationArea` for business domain partitioning; `order`/`badge`/`requiredPermissions` on all nav items), Airtable Interface Parity (`UserActionsConfig`, `AppearanceConfig`, `ViewTab`, `AddRecordConfig`, `InterfacePageConfig`, `showRecordCount`, `allowPrinting`)
321+
- [x] **UI Protocol** — View (List/Form/Kanban/Calendar/Gantt), App, Dashboard, Report, Action, Page (16 types), Chart, Widget, Theme, Animation, DnD, Touch, Keyboard, Responsive, Offline, Notification, i18n, Content Elements, Enhanced Activity Timeline (`RecordActivityProps` unified timeline, `RecordChatterProps` sidebar/drawer), Unified Navigation Protocol (`NavigationItem` as single source of truth with 7 types: object/dashboard/page/url/report/action/group; `NavigationArea` for business domain partitioning; `order`/`badge`/`requiredPermissions` on all nav items), Airtable Interface Parity (`UserActionsConfig`, `AppearanceConfig`, `ViewTab`, `AddRecordConfig`, `InterfacePageConfig`, `showRecordCount`, `allowPrinting`), UI Extension Fields (`source`, `groupBy`, `typeOptions` on `ListViewSchema` for objectui console alignment)
322322
- [x] **System Protocol** — Manifest, Auth Config, Cache, Logging, Metrics, Tracing, Audit, Encryption, Masking, Migration, Tenant, Translation, Search Engine, HTTP Server, Worker, Job, Object Storage, Notification, Message Queue, Registry Config, Collaboration, Compliance, Change Management, Disaster Recovery, License, Security Context, Core Services, SystemObjectName/SystemFieldName Constants, StorageNameMapping Utilities
323323
- [x] **Automation Protocol** — Flow (autolaunched/screen/schedule), Workflow, State Machine, Trigger Registry, Approval, ETL, Sync, Webhook, BPMN Semantics (parallel/join gateways, boundary events, wait events, default sequence flows), Node Executor Plugin Protocol (wait pause/resume, executor descriptors), BPMN XML Interop (import/export options, element mappings, diagnostics)
324324
- [x] **AI Protocol** — Agent, Agent Action, Conversation, Cost, MCP, Model Registry, NLQ, Orchestration, Predictive, RAG Pipeline, Runtime Ops, Feedback Loop, DevOps Agent, Plugin Development

packages/spec/src/ui/view.test.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2300,3 +2300,170 @@ describe('ListViewSchema — Airtable Interface parity fields', () => {
23002300
expect(listView.allowPrinting).toBeUndefined();
23012301
});
23022302
});
2303+
2304+
// ============================================================================
2305+
// UI Extension Fields: source, groupBy, typeOptions
2306+
// ============================================================================
2307+
2308+
describe('ListViewSchema — UI Extension fields (source, groupBy, typeOptions)', () => {
2309+
// --- source ---
2310+
2311+
it('should accept list view with source field', () => {
2312+
const listView = ListViewSchema.parse({
2313+
columns: ['name', 'status'],
2314+
source: 'project_task',
2315+
});
2316+
expect(listView.source).toBe('project_task');
2317+
});
2318+
2319+
it('should accept list view without source (optional)', () => {
2320+
const listView = ListViewSchema.parse({
2321+
columns: ['name', 'status'],
2322+
});
2323+
expect(listView.source).toBeUndefined();
2324+
});
2325+
2326+
it('should accept source alongside data provider', () => {
2327+
const listView = ListViewSchema.parse({
2328+
columns: ['name', 'status'],
2329+
source: 'project_task',
2330+
data: { provider: 'object', object: 'project_task' },
2331+
});
2332+
expect(listView.source).toBe('project_task');
2333+
expect(listView.data?.provider).toBe('object');
2334+
});
2335+
2336+
// --- groupBy ---
2337+
2338+
it('should accept list view with groupBy field', () => {
2339+
const listView = ListViewSchema.parse({
2340+
columns: ['name', 'status', 'department'],
2341+
groupBy: 'department',
2342+
});
2343+
expect(listView.groupBy).toBe('department');
2344+
});
2345+
2346+
it('should accept list view without groupBy (optional)', () => {
2347+
const listView = ListViewSchema.parse({
2348+
columns: ['name', 'status'],
2349+
});
2350+
expect(listView.groupBy).toBeUndefined();
2351+
});
2352+
2353+
it('should accept groupBy alongside full grouping config', () => {
2354+
const listView = ListViewSchema.parse({
2355+
columns: ['name', 'status', 'department'],
2356+
groupBy: 'status',
2357+
grouping: {
2358+
fields: [
2359+
{ field: 'department', order: 'asc' },
2360+
{ field: 'status', order: 'desc', collapsed: true },
2361+
],
2362+
},
2363+
});
2364+
expect(listView.groupBy).toBe('status');
2365+
expect(listView.grouping?.fields).toHaveLength(2);
2366+
});
2367+
2368+
// --- typeOptions ---
2369+
2370+
it('should accept list view with typeOptions', () => {
2371+
const listView = ListViewSchema.parse({
2372+
columns: ['name', 'status'],
2373+
type: 'calendar',
2374+
typeOptions: {
2375+
showWeekends: false,
2376+
firstDayOfWeek: 1,
2377+
},
2378+
});
2379+
expect(listView.typeOptions).toEqual({
2380+
showWeekends: false,
2381+
firstDayOfWeek: 1,
2382+
});
2383+
});
2384+
2385+
it('should accept list view without typeOptions (optional)', () => {
2386+
const listView = ListViewSchema.parse({
2387+
columns: ['name', 'status'],
2388+
});
2389+
expect(listView.typeOptions).toBeUndefined();
2390+
});
2391+
2392+
it('should accept typeOptions with nested objects', () => {
2393+
const listView = ListViewSchema.parse({
2394+
columns: ['name', 'status'],
2395+
type: 'kanban',
2396+
typeOptions: {
2397+
cardTemplate: 'compact',
2398+
showAvatar: true,
2399+
cardLayout: { maxFields: 4, showCover: true },
2400+
},
2401+
});
2402+
expect(listView.typeOptions?.cardTemplate).toBe('compact');
2403+
expect(listView.typeOptions?.cardLayout).toEqual({ maxFields: 4, showCover: true });
2404+
});
2405+
2406+
it('should accept typeOptions alongside built-in type config', () => {
2407+
const listView = ListViewSchema.parse({
2408+
columns: ['name', 'status'],
2409+
type: 'kanban',
2410+
kanban: {
2411+
groupByField: 'stage',
2412+
summarizeField: 'amount',
2413+
columns: ['name', 'owner'],
2414+
},
2415+
typeOptions: {
2416+
swimlanes: true,
2417+
wipLimit: 5,
2418+
},
2419+
});
2420+
expect(listView.kanban?.groupByField).toBe('stage');
2421+
expect(listView.typeOptions?.swimlanes).toBe(true);
2422+
expect(listView.typeOptions?.wipLimit).toBe(5);
2423+
});
2424+
2425+
it('should accept empty typeOptions', () => {
2426+
const listView = ListViewSchema.parse({
2427+
columns: ['name', 'status'],
2428+
typeOptions: {},
2429+
});
2430+
expect(listView.typeOptions).toEqual({});
2431+
});
2432+
2433+
// --- Combined: all UI extension fields together ---
2434+
2435+
it('should accept all UI extension fields together', () => {
2436+
const listView = ListViewSchema.parse({
2437+
name: 'task_board',
2438+
label: 'Task Board',
2439+
description: 'Project task tracking board',
2440+
type: 'kanban',
2441+
columns: ['name', 'status', 'assignee'],
2442+
source: 'project_task',
2443+
groupBy: 'status',
2444+
typeOptions: {
2445+
swimlanes: true,
2446+
wipLimit: 10,
2447+
},
2448+
kanban: {
2449+
groupByField: 'status',
2450+
summarizeField: 'story_points',
2451+
columns: ['name', 'assignee', 'priority'],
2452+
},
2453+
});
2454+
expect(listView.name).toBe('task_board');
2455+
expect(listView.description).toBe('Project task tracking board');
2456+
expect(listView.source).toBe('project_task');
2457+
expect(listView.groupBy).toBe('status');
2458+
expect(listView.typeOptions?.swimlanes).toBe(true);
2459+
});
2460+
2461+
it('should maintain backward compatibility — new fields are all optional', () => {
2462+
const listView = ListViewSchema.parse({
2463+
columns: ['name', 'status'],
2464+
});
2465+
expect(listView.source).toBeUndefined();
2466+
expect(listView.groupBy).toBeUndefined();
2467+
expect(listView.typeOptions).toBeUndefined();
2468+
});
2469+
});

packages/spec/src/ui/view.zod.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,17 @@ export const ListViewSchema = z.object({
354354

355355
/** Data Source Configuration */
356356
data: ViewDataSchema.optional().describe('Data source configuration (defaults to "object" provider)'),
357+
358+
/**
359+
* Source Object Name (UI Extension)
360+
* Shorthand identifier for the underlying data object this view is bound to.
361+
* Used by UI settings panels and console tools to display the source object
362+
* without needing to parse the full `data` configuration.
363+
*
364+
* @example 'project_task'
365+
* @see data — full data source configuration
366+
*/
367+
source: z.string().optional().describe('Source object name for this view (UI display shorthand)'),
357368

358369
/** Shared Query Config */
359370
columns: z.union([
@@ -402,6 +413,19 @@ export const ListViewSchema = z.object({
402413
gallery: GalleryConfigSchema.optional(),
403414
timeline: TimelineConfigSchema.optional(),
404415

416+
/**
417+
* Type-Specific Options (UI Extension)
418+
* Extensible key-value map for view-type-specific configuration that goes
419+
* beyond the built-in type configs (kanban, calendar, gantt, gallery, timeline).
420+
* Useful for custom/third-party view types or advanced settings not covered
421+
* by the standard typed configs above.
422+
*
423+
* @example { showWeekends: false, firstDayOfWeek: 1 } // calendar extras
424+
* @example { cardTemplate: 'compact', showAvatar: true } // custom card view
425+
*/
426+
typeOptions: z.record(z.string(), z.unknown()).optional()
427+
.describe('Extensible key-value options for view-type-specific configuration'),
428+
405429
/** View Metadata (Airtable-style view management) */
406430
description: I18nLabelSchema.optional().describe('View description for documentation/tooltips'),
407431
sharing: ViewSharingSchema.optional().describe('View sharing and access configuration'),
@@ -412,6 +436,18 @@ export const ListViewSchema = z.object({
412436
/** Record Grouping (Airtable-style) */
413437
grouping: GroupingConfigSchema.optional().describe('Group records by one or more fields'),
414438

439+
/**
440+
* Simple Group-By Field (UI Extension)
441+
* A lightweight shorthand for grouping records by a single field name.
442+
* UI settings panels use this for quick "Group by" dropdowns.
443+
* For multi-level grouping with sort order and collapse state, use `grouping` instead.
444+
*
445+
* @example 'status'
446+
* @example 'department'
447+
* @see grouping — full multi-level grouping configuration
448+
*/
449+
groupBy: z.string().optional().describe('Field name to group records by (simple shorthand for grouping)'),
450+
415451
/** Row Color (Airtable-style) */
416452
rowColor: RowColorConfigSchema.optional().describe('Color rows based on field value'),
417453

0 commit comments

Comments
 (0)