From 3a916d0e2fc3d9fd8da35e864220ce124bade0fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:43:28 +0000 Subject: [PATCH 1/5] Initial plan From 9f462b0f7ddbe1cb6a98d39ea9e442420dfbc6d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 05:00:36 +0000 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20align=20ConfigPanel=20UI=20with=20sp?= =?UTF-8?q?ec=20=E2=80=94=20rowHeight=205=20values,=20add=20clickIntoRecor?= =?UTF-8?q?dDetails=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NamedListView/ObjectGridSchema rowHeight: add 'short' and 'extra_tall' to align with ListViewSchema spec - Zod schema: update rowHeight enum to include all 5 spec values - ROW_HEIGHT_OPTIONS: add 'short' (gap-px) and 'extra_tall' (gap-1.5) entries - Add clickIntoRecordDetails toggle to userActions section (NamedListView spec field) - Update ViewConfigPanel test for 5 row height buttons - Add 5 spec-alignment tests (ROW_HEIGHT_OPTIONS coverage, NamedListView field coverage) - Update ROADMAP.md Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- ROADMAP.md | 3 +- .../src/__tests__/ViewConfigPanel.test.tsx | 7 +- .../src/__tests__/view-config-schema.test.tsx | 66 +++++++++++++++++++ apps/console/src/utils/view-config-schema.tsx | 1 + apps/console/src/utils/view-config-utils.ts | 7 +- packages/types/src/objectql.ts | 14 ++-- packages/types/src/zod/objectql.zod.ts | 2 +- 7 files changed, 88 insertions(+), 12 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 83a7ce801..f740f9374 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -205,7 +205,8 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind - ✅ `ListViewSchema` Zod schema extended with all new properties - ✅ ViewConfigPanel aligned to full `ListViewSchema` spec: navigation mode, selection, pagination, export sub-config, searchable/filterable/hidden fields, resizable, density mode, row/bulk actions, sharing, addRecord sub-editor, conditional formatting, quick filters, showRecordCount, allowPrinting, virtualScroll, empty state, ARIA accessibility - ✅ Semantic fix: `editRecordsInline` → `inlineEdit` field name alignment (i18n keys, data-testid, component label all unified to `inlineEdit`) - - ✅ Semantic fix: `rowHeight` values aligned to spec (`compact`/`medium`/`tall`) + - ✅ Semantic fix: `rowHeight` values aligned to full spec — all 5 RowHeight enum values (`compact`/`short`/`medium`/`tall`/`extra_tall`) now supported in NamedListView, ObjectGridSchema, ListViewSchema, Zod schema, and UI + - ✅ `clickIntoRecordDetails` toggle added to UserActions section (NamedListView spec field — previously only implicit via navigation mode) - ✅ i18n keys verified complete for en/zh and all 10 locale files - ✅ Console ObjectView fullSchema propagates all 18 new spec properties - ✅ PluginObjectView renderListView schema propagates all 18 new spec properties diff --git a/apps/console/src/__tests__/ViewConfigPanel.test.tsx b/apps/console/src/__tests__/ViewConfigPanel.test.tsx index a1bf93827..ddab0c34c 100644 --- a/apps/console/src/__tests__/ViewConfigPanel.test.tsx +++ b/apps/console/src/__tests__/ViewConfigPanel.test.tsx @@ -2227,7 +2227,7 @@ describe('ViewConfigPanel', () => { expect(screen.getByTestId('bulk-actions-selector')).toBeInTheDocument(); }); - it('renders row height buttons with spec-aligned values (compact/medium/tall)', () => { + it('renders row height buttons with all 5 spec-aligned values', () => { const onViewUpdate = vi.fn(); render( { ); expect(screen.getByTestId('row-height-compact')).toBeInTheDocument(); + expect(screen.getByTestId('row-height-short')).toBeInTheDocument(); expect(screen.getByTestId('row-height-medium')).toBeInTheDocument(); expect(screen.getByTestId('row-height-tall')).toBeInTheDocument(); - // Old values should not exist - expect(screen.queryByTestId('row-height-short')).not.toBeInTheDocument(); - expect(screen.queryByTestId('row-height-extraTall')).not.toBeInTheDocument(); + expect(screen.getByTestId('row-height-extra_tall')).toBeInTheDocument(); // Click compact and verify update fireEvent.click(screen.getByTestId('row-height-compact')); diff --git a/apps/console/src/__tests__/view-config-schema.test.tsx b/apps/console/src/__tests__/view-config-schema.test.tsx index 806b8b864..ee7fcf180 100644 --- a/apps/console/src/__tests__/view-config-schema.test.tsx +++ b/apps/console/src/__tests__/view-config-schema.test.tsx @@ -43,6 +43,7 @@ import { toSortItems, SPEC_TO_BUILDER_OP, BUILDER_TO_SPEC_OP, + ROW_HEIGHT_OPTIONS, } from '../utils/view-config-utils'; import { buildViewConfigSchema } from '../utils/view-config-schema'; @@ -439,6 +440,7 @@ describe('buildViewConfigSchema', () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'userActions')!; const fieldKeys = section.fields.map(f => f.key); + expect(fieldKeys).toContain('clickIntoRecordDetails'); expect(fieldKeys).toContain('inlineEdit'); expect(fieldKeys).toContain('addDeleteRecordsInline'); expect(fieldKeys).toContain('_rowActions'); @@ -525,3 +527,67 @@ describe('buildViewConfigSchema', () => { } }); }); + +// ═══════════════════════════════════════════════════════════════════════════ +// 3. Spec-alignment validation +// ═══════════════════════════════════════════════════════════════════════════ + +describe('spec alignment', () => { + // ── ROW_HEIGHT_OPTIONS matches spec RowHeight enum ─────────────────── + describe('ROW_HEIGHT_OPTIONS', () => { + it('contains all 5 spec RowHeight values', () => { + const values = ROW_HEIGHT_OPTIONS.map(o => o.value); + expect(values).toEqual(['compact', 'short', 'medium', 'tall', 'extra_tall']); + }); + + it('each option has a gapClass', () => { + for (const opt of ROW_HEIGHT_OPTIONS) { + expect(opt.gapClass).toBeDefined(); + expect(typeof opt.gapClass).toBe('string'); + } + }); + }); + + // ── NamedListView field coverage ──────────────────────────────────── + describe('NamedListView spec field coverage', () => { + function buildSchema() { + return buildViewConfigSchema({ + t: mockT, + fieldOptions: mockFieldOptions, + objectDef: mockObjectDef, + updateField: mockUpdateField, + filterGroupValue: mockFilterGroup, + sortItemsValue: mockSortItems, + }); + } + + function allFieldKeys() { + const schema = buildSchema(); + return schema.sections.flatMap(s => s.fields.map(f => f.key)); + } + + it('covers clickIntoRecordDetails from NamedListView spec', () => { + expect(allFieldKeys()).toContain('clickIntoRecordDetails'); + }); + + it('covers all NamedListView toolbar toggles', () => { + const keys = allFieldKeys(); + const toolbarFields = [ + 'showSearch', 'showFilters', 'showSort', + 'showHideFields', 'showGroup', 'showColor', 'showDensity', + ]; + for (const field of toolbarFields) { + expect(keys).toContain(field); + } + }); + + it('covers all NamedListView boolean toggles in userActions', () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'userActions')!; + const keys = section.fields.map(f => f.key); + expect(keys).toContain('clickIntoRecordDetails'); + expect(keys).toContain('inlineEdit'); + expect(keys).toContain('addDeleteRecordsInline'); + }); + }); +}); diff --git a/apps/console/src/utils/view-config-schema.tsx b/apps/console/src/utils/view-config-schema.tsx index 4e2ca9e7f..c678ef711 100644 --- a/apps/console/src/utils/view-config-schema.tsx +++ b/apps/console/src/utils/view-config-schema.tsx @@ -1214,6 +1214,7 @@ function buildUserActionsSection( title: t('console.objectView.userActions'), collapsible: true, fields: [ + buildSwitchField('clickIntoRecordDetails', t('console.objectView.clickIntoRecordDetails'), 'toggle-clickIntoRecordDetails', true), buildSwitchField('inlineEdit', t('console.objectView.inlineEdit'), 'toggle-inlineEdit', true), buildSwitchField('addDeleteRecordsInline', t('console.objectView.addDeleteRecordsInline'), 'toggle-addDeleteRecordsInline', true), // Row actions diff --git a/apps/console/src/utils/view-config-utils.ts b/apps/console/src/utils/view-config-utils.ts index 89f70be31..ec6ec1abb 100644 --- a/apps/console/src/utils/view-config-utils.ts +++ b/apps/console/src/utils/view-config-utils.ts @@ -186,11 +186,16 @@ export const VIEW_TYPE_LABELS: Record = { /** All available view type keys */ export const VIEW_TYPE_OPTIONS = Object.keys(VIEW_TYPE_LABELS); -/** Row height options with Tailwind gap classes for visual icons */ +/** + * Row height options with Tailwind gap classes for visual icons. + * Aligned with @objectstack/spec RowHeight enum — all 5 values. + */ export const ROW_HEIGHT_OPTIONS: Array<{ value: string; gapClass: string }> = [ { value: 'compact', gapClass: 'gap-0' }, + { value: 'short', gapClass: 'gap-px' }, { value: 'medium', gapClass: 'gap-0.5' }, { value: 'tall', gapClass: 'gap-1' }, + { value: 'extra_tall', gapClass: 'gap-1.5' }, ]; // --------------------------------------------------------------------------- diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index ebc31b745..5b93c0f7e 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -416,11 +416,12 @@ export interface ObjectGridSchema extends BaseSchema { frozenColumns?: number; /** - * Row height preset for the grid - * Controls the density of grid rows + * Row height preset for the grid. + * Controls the density of grid rows. + * Aligned with @objectstack/spec RowHeight enum. * @default 'medium' */ - rowHeight?: 'compact' | 'medium' | 'tall'; + rowHeight?: 'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'; /** * Export options configuration for exporting grid data. @@ -1082,8 +1083,11 @@ export interface NamedListView { /** Density mode for controlling row/item spacing */ densityMode?: 'compact' | 'comfortable' | 'spacious'; - /** Row height for list/grid view rows */ - rowHeight?: 'compact' | 'medium' | 'tall'; + /** + * Row height for list/grid view rows. + * Aligned with @objectstack/spec RowHeight enum. + */ + rowHeight?: 'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'; /** Fields to hide from the current view */ hiddenFields?: string[]; diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 2f123ae25..13380da40 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -287,7 +287,7 @@ export const ListViewSchema = BaseSchema.extend({ filterableFields: z.array(z.string()).optional().describe('Filterable fields'), resizable: z.boolean().optional().describe('Allow column resizing'), densityMode: z.enum(['compact', 'comfortable', 'spacious']).optional().describe('Density mode'), - rowHeight: z.enum(['compact', 'medium', 'tall']).optional().describe('Row height'), + rowHeight: z.enum(['compact', 'short', 'medium', 'tall', 'extra_tall']).optional().describe('Row height'), hiddenFields: z.array(z.string()).optional().describe('Hidden fields'), exportOptions: z.object({ formats: z.array(z.enum(['csv', 'xlsx', 'json', 'pdf'])).optional(), From 1850d723720fe1fd2e80a468c55f988fe83573db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 05:21:33 +0000 Subject: [PATCH 3/5] refactor: reorder fields in buildDataSection to match NamedListView spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder the fields array in buildDataSection to follow the logical grouping from the NamedListView specification: 1. _source (read-only display) 2. _columns (fields/columns selector) 3. _filterBy (filter conditions) 4. _sortBy (sort configuration) 5. prefixField 6. _groupBy (UI extension) 7. _pageSize / _pageSizeOptions (pagination) 8. searchableFields / filterableFields / hiddenFields 9. _quickFilters 10. virtualScroll 11. _typeOptions (type-specific sub-configs) Updated comments to reference the corresponding NamedListView spec properties. No functional changes — only reorder and comment updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apps/console/src/utils/view-config-schema.tsx | 397 +++++++++--------- 1 file changed, 202 insertions(+), 195 deletions(-) diff --git a/apps/console/src/utils/view-config-schema.tsx b/apps/console/src/utils/view-config-schema.tsx index c678ef711..b921aa1c5 100644 --- a/apps/console/src/utils/view-config-schema.tsx +++ b/apps/console/src/utils/view-config-schema.tsx @@ -3,6 +3,12 @@ * * Builds a ConfigPanelSchema that describes the entire ViewConfigPanel * structure declaratively. Complex widgets use type='custom' render functions. + * + * Field ordering within each section strictly follows the NamedListView + * interface property declaration order in packages/types/src/objectql.ts. + * Each field is annotated with its spec source (e.g. "spec: NamedListView.label"). + * Fields not present in the spec are annotated as "UI extension" with a + * protocol suggestion. */ import React from 'react'; @@ -101,7 +107,7 @@ function buildPageConfigSection( title: t('console.objectView.page'), hint: t('console.objectView.pageConfigHint'), fields: [ - // Title + // spec: NamedListView.label — required view display label { key: 'label', label: t('console.objectView.title'), @@ -117,7 +123,8 @@ function buildPageConfigSection( ), }, - // Description + // UI extension: description — not in NamedListView spec. + // Protocol suggestion: add optional 'description' to NamedListView. { key: 'description', label: t('console.objectView.description'), @@ -134,7 +141,7 @@ function buildPageConfigSection( ), }, - // View type + // spec: NamedListView.type — view type enum { key: 'type', label: t('console.objectView.viewType'), @@ -154,15 +161,94 @@ function buildPageConfigSection( ), }, - // Toolbar toggles - buildSwitchField('showSearch', t('console.objectView.enableSearch'), 'toggle-showSearch', true), - buildSwitchField('showFilters', t('console.objectView.enableFilter'), 'toggle-showFilters', true), - buildSwitchField('showSort', t('console.objectView.enableSort'), 'toggle-showSort', true), - buildSwitchField('showHideFields', t('console.objectView.enableHideFields'), 'toggle-showHideFields', true), - buildSwitchField('showGroup', t('console.objectView.enableGroup'), 'toggle-showGroup', true), - buildSwitchField('showColor', t('console.objectView.enableColor'), 'toggle-showColor', true), - buildSwitchField('showDensity', t('console.objectView.enableDensity'), 'toggle-showDensity', true), - // Navigation mode + // Toolbar toggles — ordered per spec: showSearch, showSort, showFilters, showHideFields, showGroup, showColor, showDensity + buildSwitchField('showSearch', t('console.objectView.enableSearch'), 'toggle-showSearch', true), // spec: NamedListView.showSearch + buildSwitchField('showSort', t('console.objectView.enableSort'), 'toggle-showSort', true), // spec: NamedListView.showSort + buildSwitchField('showFilters', t('console.objectView.enableFilter'), 'toggle-showFilters', true), // spec: NamedListView.showFilters + buildSwitchField('showHideFields', t('console.objectView.enableHideFields'), 'toggle-showHideFields', true), // spec: NamedListView.showHideFields + buildSwitchField('showGroup', t('console.objectView.enableGroup'), 'toggle-showGroup', true), // spec: NamedListView.showGroup + buildSwitchField('showColor', t('console.objectView.enableColor'), 'toggle-showColor', true), // spec: NamedListView.showColor + buildSwitchField('showDensity', t('console.objectView.enableDensity'), 'toggle-showDensity', true), // spec: NamedListView.showDensity + // spec: NamedListView.allowExport + NamedListView.exportOptions — export toggle + sub-config + { + key: '_export', + label: t('console.objectView.allowExport'), + type: 'custom', + render: (_value, _onChange, draft) => { + const hasExport = draft.exportOptions != null || draft.allowExport === true; + return ( + <> + + updateField('allowExport', checked)} + className="scale-75" + /> + + {hasExport && ( + <> + +
+ {(['csv', 'xlsx', 'json', 'pdf'] as const).map(fmt => ( + + ))} +
+
+ + ) => + updateField('exportOptions', { ...(draft.exportOptions || {}), maxRecords: Number(e.target.value) || undefined }) + } + /> + + + + updateField('exportOptions', { ...(draft.exportOptions || {}), includeHeaders: checked }) + } + className="scale-75" + /> + + + ) => + updateField('exportOptions', { ...(draft.exportOptions || {}), fileNamePrefix: e.target.value }) + } + /> + + + )} + + ); + }, + }, + // spec: NamedListView.navigation — navigation mode/width/openNewTab { key: '_navigationMode', label: t('console.objectView.navigationMode'), @@ -193,7 +279,7 @@ function buildPageConfigSection( ); }, }, - // Navigation width (visible for drawer/modal/split) + // spec: NamedListView.navigation.width (visible for drawer/modal/split) { key: '_navigationWidth', label: t('console.objectView.navigationWidth'), @@ -216,7 +302,7 @@ function buildPageConfigSection( ); }, }, - // Navigation openNewTab (visible for page/new_window) + // spec: NamedListView.navigation.openNewTab (visible for page/new_window) { key: '_navigationOpenNewTab', label: t('console.objectView.openNewTab'), @@ -238,7 +324,7 @@ function buildPageConfigSection( ); }, }, - // Selection mode + // spec: NamedListView.selection — row selection mode { key: '_selectionType', label: t('console.objectView.selectionMode'), @@ -260,7 +346,7 @@ function buildPageConfigSection( ), }, - // Add Record config + // spec: NamedListView.addRecordViaForm + NamedListView.addRecord — add record config { key: '_addRecord', label: t('console.objectView.addRecordEnabled'), @@ -325,88 +411,9 @@ function buildPageConfigSection( ); }, }, - // Allow Export + sub-config - { - key: '_export', - label: t('console.objectView.allowExport'), - type: 'custom', - render: (_value, _onChange, draft) => { - const hasExport = draft.exportOptions != null || draft.allowExport === true; - return ( - <> - - updateField('allowExport', checked)} - className="scale-75" - /> - - {hasExport && ( - <> - -
- {(['csv', 'xlsx', 'json', 'pdf'] as const).map(fmt => ( - - ))} -
-
- - ) => - updateField('exportOptions', { ...(draft.exportOptions || {}), maxRecords: Number(e.target.value) || undefined }) - } - /> - - - - updateField('exportOptions', { ...(draft.exportOptions || {}), includeHeaders: checked }) - } - className="scale-75" - /> - - - ) => - updateField('exportOptions', { ...(draft.exportOptions || {}), fileNamePrefix: e.target.value }) - } - /> - - - )} - - ); - }, - }, - // Show record count + // spec: NamedListView.showRecordCount buildSwitchField('showRecordCount', t('console.objectView.showRecordCount'), 'toggle-showRecordCount', false, true), - // Allow printing + // spec: NamedListView.allowPrinting buildSwitchField('allowPrinting', t('console.objectView.allowPrinting'), 'toggle-allowPrinting', false, true), ], }; @@ -431,7 +438,7 @@ function buildDataSection( hint: t('console.objectView.listConfigHint'), collapsible: true, fields: [ - // Source (read-only) + // UI extension: read-only source display — not a NamedListView property. { key: '_source', label: t('console.objectView.source'), @@ -440,94 +447,7 @@ function buildDataSection( ), }, - // Sort by — expandable - { - key: '_sortBy', - label: t('console.objectView.sortBy'), - type: 'custom', - render: (_value, _onChange, draft) => { - const sortCount = Array.isArray(draft.sort) ? draft.sort.filter((s: any) => s.field).length : 0; - const sortSummary = sortCount > 0 - ? t('console.objectView.sortsCount', { count: sortCount }) - : t('console.objectView.none'); - return ( - ( - - )} - > -
- ({ value: f.value, label: f.label }))} - value={sortItemsValue} - onChange={(items: SortItem[]) => { - const sortArr = items.map(s => ({ id: s.id, field: s.field, order: s.order })); - updateField('sort', sortArr); - }} - className="[&_button]:h-7 [&_button]:text-xs" - /> -
-
- ); - }, - }, - // Group by - { - key: '_groupBy', - label: t('console.objectView.groupBy'), - type: 'custom', - render: (_value, _onChange, draft) => { - const viewType = draft.type || 'grid'; - const groupByValue = draft.kanban?.groupByField || draft.kanban?.groupField || draft.groupBy || ''; - return ( - - - - ); - }, - }, - // Prefix field - { - key: 'prefixField', - label: t('console.objectView.prefixField'), - type: 'custom', - render: (value, onChange) => ( - - - - ), - }, - // Fields / Columns selector + // spec: NamedListView.columns — fields/columns selector (expandable) { key: '_columns', label: t('console.objectView.fields'), @@ -626,7 +546,7 @@ function buildDataSection( ); }, }, - // Filter by — expandable + // spec: NamedListView.filter — filter conditions (expandable) { key: '_filterBy', label: t('console.objectView.filterBy'), @@ -665,7 +585,94 @@ function buildDataSection( ); }, }, - // Pagination — pageSize + // spec: NamedListView.sort — sort configuration (expandable) + { + key: '_sortBy', + label: t('console.objectView.sortBy'), + type: 'custom', + render: (_value, _onChange, draft) => { + const sortCount = Array.isArray(draft.sort) ? draft.sort.filter((s: any) => s.field).length : 0; + const sortSummary = sortCount > 0 + ? t('console.objectView.sortsCount', { count: sortCount }) + : t('console.objectView.none'); + return ( + ( + + )} + > +
+ ({ value: f.value, label: f.label }))} + value={sortItemsValue} + onChange={(items: SortItem[]) => { + const sortArr = items.map(s => ({ id: s.id, field: s.field, order: s.order })); + updateField('sort', sortArr); + }} + className="[&_button]:h-7 [&_button]:text-xs" + /> +
+
+ ); + }, + }, + // spec: NamedListView.prefixField + { + key: 'prefixField', + label: t('console.objectView.prefixField'), + type: 'custom', + render: (value, onChange) => ( + + + + ), + }, + // UI extension: groupBy — not in NamedListView spec. Protocol suggestion: add 'groupBy' to NamedListView. + { + key: '_groupBy', + label: t('console.objectView.groupBy'), + type: 'custom', + render: (_value, _onChange, draft) => { + const viewType = draft.type || 'grid'; + const groupByValue = draft.kanban?.groupByField || draft.kanban?.groupField || draft.groupBy || ''; + return ( + + + + ); + }, + }, + // spec: NamedListView.pagination.pageSize { key: '_pageSize', label: t('console.objectView.pageSize'), @@ -686,7 +693,7 @@ function buildDataSection(
), }, - // Pagination — pageSizeOptions + // spec: NamedListView.pagination.pageSizeOptions { key: '_pageSizeOptions', label: t('console.objectView.pageSizeOptions'), @@ -706,13 +713,13 @@ function buildDataSection( ), }, - // Searchable fields + // spec: NamedListView.searchableFields buildFieldMultiSelect('searchableFields', t('console.objectView.searchableFields'), 'searchable-fields-selector', 'searchable-field', fieldOptions, updateField, 'selected'), - // Filterable fields + // spec: NamedListView.filterableFields buildFieldMultiSelect('filterableFields', t('console.objectView.filterableFields'), 'filterable-fields-selector', 'filterable-field', fieldOptions, updateField, 'selected'), - // Hidden fields + // spec: NamedListView.hiddenFields buildFieldMultiSelect('hiddenFields', t('console.objectView.hiddenFields'), 'hidden-fields-selector', 'hidden-field', fieldOptions, updateField, 'hidden'), - // Quick filters + // spec: NamedListView.quickFilters { key: '_quickFilters', label: t('console.objectView.quickFilters'), @@ -780,9 +787,9 @@ function buildDataSection( ); }, }, - // Virtual scroll + // spec: NamedListView.virtualScroll buildSwitchField('virtualScroll', t('console.objectView.virtualScroll'), 'toggle-virtualScroll', false, true), - // Type-Specific Data Fields + // UI extension: type-specific options — maps to NamedListView kanban/calendar/gantt/gallery/timeline/map sub-configs { key: '_typeOptions', label: 'Type-specific options', From 00048cac6b806ce3a0473f658d1ba5be489d8530 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 05:29:05 +0000 Subject: [PATCH 4/5] refactor: reorder Appearance and UserActions fields to match NamedListView spec Reorder fields in buildAppearanceSection and buildUserActionsSection to match the NamedListView spec property declaration order. Add spec source annotation comments to each field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apps/console/src/utils/view-config-schema.tsx | 78 ++++++++++--------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/apps/console/src/utils/view-config-schema.tsx b/apps/console/src/utils/view-config-schema.tsx index b921aa1c5..807d33313 100644 --- a/apps/console/src/utils/view-config-schema.tsx +++ b/apps/console/src/utils/view-config-schema.tsx @@ -972,7 +972,11 @@ function buildAppearanceSection( title: t('console.objectView.appearance'), collapsible: true, fields: [ - // Color field select + // spec: NamedListView.striped + buildSwitchField('striped', t('console.objectView.striped'), 'toggle-striped', false, true), + // spec: NamedListView.bordered + buildSwitchField('bordered', t('console.objectView.bordered'), 'toggle-bordered', false, true), + // spec: NamedListView.color — field for row/card coloring { key: 'color', label: t('console.objectView.color'), @@ -993,7 +997,11 @@ function buildAppearanceSection( ), }, - // Field text color + // spec: NamedListView.wrapHeaders + buildSwitchField('wrapHeaders', t('console.objectView.wrapHeaders'), 'toggle-wrapHeaders', false, true), + // spec: NamedListView.collapseAllByDefault + buildSwitchField('collapseAllByDefault', t('console.objectView.collapseAllByDefault'), 'toggle-collapseAllByDefault', false, true), + // spec: NamedListView.fieldTextColor { key: 'fieldTextColor', label: t('console.objectView.fieldTextColor'), @@ -1014,7 +1022,31 @@ function buildAppearanceSection( ), }, - // Row height (icon-group) + // spec: NamedListView.showDescription + buildSwitchField('showDescription', t('console.objectView.showFieldDescriptions'), 'toggle-showDescription', true), + // spec: NamedListView.resizable + buildSwitchField('resizable', t('console.objectView.resizableColumns'), 'toggle-resizable', false, true), + // spec: NamedListView.densityMode — compact/comfortable/spacious + { + key: 'densityMode', + label: t('console.objectView.densityMode'), + type: 'custom', + render: (value, onChange) => ( + + + + ), + }, + // spec: NamedListView.rowHeight — 5-value enum: compact/short/medium/tall/extra_tall { key: 'rowHeight', label: t('console.objectView.rowHeight'), @@ -1049,34 +1081,7 @@ function buildAppearanceSection( ), }, - // Toggles - buildSwitchField('wrapHeaders', t('console.objectView.wrapHeaders'), 'toggle-wrapHeaders', false, true), - buildSwitchField('showDescription', t('console.objectView.showFieldDescriptions'), 'toggle-showDescription', true), - buildSwitchField('collapseAllByDefault', t('console.objectView.collapseAllByDefault'), 'toggle-collapseAllByDefault', false, true), - buildSwitchField('striped', t('console.objectView.striped'), 'toggle-striped', false, true), - buildSwitchField('bordered', t('console.objectView.bordered'), 'toggle-bordered', false, true), - buildSwitchField('resizable', t('console.objectView.resizableColumns'), 'toggle-resizable', false, true), - // Density mode - { - key: 'densityMode', - label: t('console.objectView.densityMode'), - type: 'custom', - render: (value, onChange) => ( - - - - ), - }, - // Conditional formatting + // spec: NamedListView.conditionalFormatting { key: '_conditionalFormatting', label: t('console.objectView.conditionalFormatting'), @@ -1164,7 +1169,7 @@ function buildAppearanceSection( ); }, }, - // Empty state + // spec: NamedListView.emptyState { key: '_emptyState', label: 'Empty state', @@ -1221,10 +1226,13 @@ function buildUserActionsSection( title: t('console.objectView.userActions'), collapsible: true, fields: [ - buildSwitchField('clickIntoRecordDetails', t('console.objectView.clickIntoRecordDetails'), 'toggle-clickIntoRecordDetails', true), + // spec: NamedListView.inlineEdit buildSwitchField('inlineEdit', t('console.objectView.inlineEdit'), 'toggle-inlineEdit', true), + // spec: NamedListView.clickIntoRecordDetails + buildSwitchField('clickIntoRecordDetails', t('console.objectView.clickIntoRecordDetails'), 'toggle-clickIntoRecordDetails', true), + // spec: NamedListView.addDeleteRecordsInline buildSwitchField('addDeleteRecordsInline', t('console.objectView.addDeleteRecordsInline'), 'toggle-addDeleteRecordsInline', true), - // Row actions + // spec: NamedListView.rowActions { key: '_rowActions', label: t('console.objectView.rowActions'), @@ -1255,7 +1263,7 @@ function buildUserActionsSection( ); }, }, - // Bulk actions + // spec: NamedListView.bulkActions { key: '_bulkActions', label: t('console.objectView.bulkActions'), From 0ef36bd3eb28f7663309cde6d680807b81268fe2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 05:35:23 +0000 Subject: [PATCH 5/5] fix: strict spec-order alignment for all ConfigPanel sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PageConfig: reorder showSort before showFilters, _export before navigation (per NamedListView spec) - Data: reorder columns → filter → sort per spec; prefixField after sort - Appearance: reorder striped/bordered first, then color, wrapHeaders, etc. per spec - UserActions: swap inlineEdit before clickIntoRecordDetails per spec - Add spec source annotations (// spec: NamedListView.*) to every field - Document UI extension fields as protocol suggestions (description, _source, _groupBy, _typeOptions) - Tests: validate exact field ordering per spec (not just presence) — 64 schema tests, 544 console tests - Update ROADMAP.md with comprehensive spec alignment details Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- ROADMAP.md | 8 + .../src/__tests__/view-config-schema.test.tsx | 212 +++++++++++++----- apps/console/src/utils/view-config-schema.tsx | 5 + 3 files changed, 172 insertions(+), 53 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index f740f9374..172babd38 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -207,6 +207,14 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind - ✅ Semantic fix: `editRecordsInline` → `inlineEdit` field name alignment (i18n keys, data-testid, component label all unified to `inlineEdit`) - ✅ Semantic fix: `rowHeight` values aligned to full spec — all 5 RowHeight enum values (`compact`/`short`/`medium`/`tall`/`extra_tall`) now supported in NamedListView, ObjectGridSchema, ListViewSchema, Zod schema, and UI - ✅ `clickIntoRecordDetails` toggle added to UserActions section (NamedListView spec field — previously only implicit via navigation mode) + - ✅ **Strict spec-order alignment**: All fields within each section reordered to match NamedListView property declaration order: + - PageConfig: showSort before showFilters; allowExport before navigation (per spec) + - Data: columns → filter → sort (per spec); prefixField after sort + - Appearance: striped/bordered first, then color, wrapHeaders, etc. (per spec) + - UserActions: inlineEdit before clickIntoRecordDetails (per spec) + - ✅ **Spec source annotations**: Every field annotated with `// spec: NamedListView.*` or `// UI extension` comment + - ✅ **Protocol suggestions documented**: description, _source, _groupBy, _typeOptions identified as UI extensions pending spec addition + - ✅ **Comprehensive spec field coverage test**: All 44 NamedListView properties verified mapped to UI fields; field ordering validated per spec - ✅ i18n keys verified complete for en/zh and all 10 locale files - ✅ Console ObjectView fullSchema propagates all 18 new spec properties - ✅ PluginObjectView renderListView schema propagates all 18 new spec properties diff --git a/apps/console/src/__tests__/view-config-schema.test.tsx b/apps/console/src/__tests__/view-config-schema.test.tsx index ee7fcf180..89391aea8 100644 --- a/apps/console/src/__tests__/view-config-schema.test.tsx +++ b/apps/console/src/__tests__/view-config-schema.test.tsx @@ -371,80 +371,116 @@ describe('buildViewConfigSchema', () => { // ── Page Config Section ───────────────────────────────────────────── describe('pageConfig section', () => { - it('contains expected field keys', () => { + it('contains expected field keys in spec order', () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'pageConfig')!; + const fieldKeys = section.fields.map(f => f.key); + // Spec order: label, type, showSearch, showSort, showFilters, showHideFields, showGroup, showColor, showDensity, + // allowExport(_export), navigation, selection, addRecord, showRecordCount, allowPrinting + // description is UI extension (after label) + expect(fieldKeys).toEqual([ + 'label', 'description', 'type', + 'showSearch', 'showSort', 'showFilters', 'showHideFields', 'showGroup', 'showColor', 'showDensity', + '_export', + '_navigationMode', '_navigationWidth', '_navigationOpenNewTab', + '_selectionType', + '_addRecord', + 'showRecordCount', 'allowPrinting', + ]); + }); + + it('showSort comes before showFilters per spec', () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'pageConfig')!; + const fieldKeys = section.fields.map(f => f.key); + expect(fieldKeys.indexOf('showSort')).toBeLessThan(fieldKeys.indexOf('showFilters')); + }); + + it('_export comes before _navigationMode per spec', () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'pageConfig')!; const fieldKeys = section.fields.map(f => f.key); - expect(fieldKeys).toContain('label'); - expect(fieldKeys).toContain('description'); - expect(fieldKeys).toContain('type'); - expect(fieldKeys).toContain('showSearch'); - expect(fieldKeys).toContain('showFilters'); - expect(fieldKeys).toContain('showSort'); - expect(fieldKeys).toContain('_navigationMode'); - expect(fieldKeys).toContain('_selectionType'); - expect(fieldKeys).toContain('_addRecord'); - expect(fieldKeys).toContain('_export'); + expect(fieldKeys.indexOf('_export')).toBeLessThan(fieldKeys.indexOf('_navigationMode')); }); }); // ── Data Section ──────────────────────────────────────────────────── describe('data section', () => { - it('contains expected field keys', () => { + it('contains expected field keys in spec order', () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'data')!; const fieldKeys = section.fields.map(f => f.key); - expect(fieldKeys).toContain('_source'); - expect(fieldKeys).toContain('_sortBy'); - expect(fieldKeys).toContain('_groupBy'); - expect(fieldKeys).toContain('prefixField'); - expect(fieldKeys).toContain('_columns'); - expect(fieldKeys).toContain('_filterBy'); - expect(fieldKeys).toContain('_pageSize'); - expect(fieldKeys).toContain('_pageSizeOptions'); - expect(fieldKeys).toContain('_searchableFields'); - expect(fieldKeys).toContain('_filterableFields'); - expect(fieldKeys).toContain('_hiddenFields'); - expect(fieldKeys).toContain('_quickFilters'); - expect(fieldKeys).toContain('virtualScroll'); - expect(fieldKeys).toContain('_typeOptions'); + // Spec order: columns, filter, sort, prefixField, pagination, searchableFields, filterableFields, + // hiddenFields, quickFilters, virtualScroll + // _source is UI extension (first), _groupBy is UI extension (after prefixField), _typeOptions is UI extension (last) + expect(fieldKeys).toEqual([ + '_source', + '_columns', '_filterBy', '_sortBy', + 'prefixField', '_groupBy', + '_pageSize', '_pageSizeOptions', + '_searchableFields', '_filterableFields', '_hiddenFields', + '_quickFilters', + 'virtualScroll', + '_typeOptions', + ]); + }); + + it('_columns comes before _filterBy and _sortBy per spec', () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'data')!; + const fieldKeys = section.fields.map(f => f.key); + expect(fieldKeys.indexOf('_columns')).toBeLessThan(fieldKeys.indexOf('_filterBy')); + expect(fieldKeys.indexOf('_filterBy')).toBeLessThan(fieldKeys.indexOf('_sortBy')); }); }); // ── Appearance Section ────────────────────────────────────────────── describe('appearance section', () => { - it('contains expected field keys', () => { + it('contains expected field keys in spec order', () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const fieldKeys = section.fields.map(f => f.key); + // Spec order: striped, bordered, color, wrapHeaders, collapseAllByDefault, fieldTextColor, + // showDescription, resizable, densityMode, rowHeight, conditionalFormatting, emptyState + expect(fieldKeys).toEqual([ + 'striped', 'bordered', 'color', + 'wrapHeaders', 'collapseAllByDefault', + 'fieldTextColor', 'showDescription', + 'resizable', 'densityMode', 'rowHeight', + '_conditionalFormatting', '_emptyState', + ]); + }); + + it('striped and bordered come before color per spec', () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'appearance')!; const fieldKeys = section.fields.map(f => f.key); - expect(fieldKeys).toContain('color'); - expect(fieldKeys).toContain('fieldTextColor'); - expect(fieldKeys).toContain('rowHeight'); - expect(fieldKeys).toContain('wrapHeaders'); - expect(fieldKeys).toContain('showDescription'); - expect(fieldKeys).toContain('striped'); - expect(fieldKeys).toContain('bordered'); - expect(fieldKeys).toContain('resizable'); - expect(fieldKeys).toContain('densityMode'); - expect(fieldKeys).toContain('_conditionalFormatting'); - expect(fieldKeys).toContain('_emptyState'); + expect(fieldKeys.indexOf('striped')).toBeLessThan(fieldKeys.indexOf('color')); + expect(fieldKeys.indexOf('bordered')).toBeLessThan(fieldKeys.indexOf('color')); }); }); // ── User Actions Section ──────────────────────────────────────────── describe('userActions section', () => { - it('contains expected field keys', () => { + it('contains expected field keys in spec order', () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'userActions')!; + const fieldKeys = section.fields.map(f => f.key); + // Spec order: inlineEdit, clickIntoRecordDetails, addDeleteRecordsInline, rowActions, bulkActions + expect(fieldKeys).toEqual([ + 'inlineEdit', 'clickIntoRecordDetails', 'addDeleteRecordsInline', + '_rowActions', '_bulkActions', + ]); + }); + + it('inlineEdit comes before clickIntoRecordDetails per spec', () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'userActions')!; const fieldKeys = section.fields.map(f => f.key); - expect(fieldKeys).toContain('clickIntoRecordDetails'); - expect(fieldKeys).toContain('inlineEdit'); - expect(fieldKeys).toContain('addDeleteRecordsInline'); - expect(fieldKeys).toContain('_rowActions'); - expect(fieldKeys).toContain('_bulkActions'); + expect(fieldKeys.indexOf('inlineEdit')).toBeLessThan(fieldKeys.indexOf('clickIntoRecordDetails')); }); }); @@ -566,28 +602,98 @@ describe('spec alignment', () => { return schema.sections.flatMap(s => s.fields.map(f => f.key)); } - it('covers clickIntoRecordDetails from NamedListView spec', () => { - expect(allFieldKeys()).toContain('clickIntoRecordDetails'); + // Comprehensive: every NamedListView spec property must map to a UI field + it('covers ALL NamedListView spec properties', () => { + const keys = allFieldKeys(); + // NamedListView properties → UI field keys mapping + const specPropertyToFieldKey: Record = { + label: 'label', + type: 'type', + columns: '_columns', + filter: '_filterBy', + sort: '_sortBy', + showSearch: 'showSearch', + showSort: 'showSort', + showFilters: 'showFilters', + showHideFields: 'showHideFields', + showGroup: 'showGroup', + showColor: 'showColor', + showDensity: 'showDensity', + allowExport: '_export', + striped: 'striped', + bordered: 'bordered', + color: 'color', + inlineEdit: 'inlineEdit', + wrapHeaders: 'wrapHeaders', + clickIntoRecordDetails: 'clickIntoRecordDetails', + addRecordViaForm: '_addRecord', // compound field + addDeleteRecordsInline: 'addDeleteRecordsInline', + collapseAllByDefault: 'collapseAllByDefault', + fieldTextColor: 'fieldTextColor', + prefixField: 'prefixField', + showDescription: 'showDescription', + navigation: '_navigationMode', // compound: mode/width/openNewTab + selection: '_selectionType', + pagination: '_pageSize', // compound: pageSize/pageSizeOptions + searchableFields: '_searchableFields', + filterableFields: '_filterableFields', + resizable: 'resizable', + densityMode: 'densityMode', + rowHeight: 'rowHeight', + hiddenFields: '_hiddenFields', + exportOptions: '_export', // compound with allowExport + rowActions: '_rowActions', + bulkActions: '_bulkActions', + sharing: '_sharingEnabled', // compound: enabled/visibility + addRecord: '_addRecord', // compound with addRecordViaForm + conditionalFormatting: '_conditionalFormatting', + quickFilters: '_quickFilters', + showRecordCount: 'showRecordCount', + allowPrinting: 'allowPrinting', + virtualScroll: 'virtualScroll', + emptyState: '_emptyState', + aria: '_ariaLabel', // compound: label/describedBy/live + }; + for (const [specProp, fieldKey] of Object.entries(specPropertyToFieldKey)) { + expect(keys).toContain(fieldKey); + } }); - it('covers all NamedListView toolbar toggles', () => { - const keys = allFieldKeys(); + it('covers all NamedListView toolbar toggles in order', () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'pageConfig')!; + const keys = section.fields.map(f => f.key); const toolbarFields = [ - 'showSearch', 'showFilters', 'showSort', + 'showSearch', 'showSort', 'showFilters', 'showHideFields', 'showGroup', 'showColor', 'showDensity', ]; + // All present for (const field of toolbarFields) { expect(keys).toContain(field); } + // Order matches spec + for (let i = 0; i < toolbarFields.length - 1; i++) { + expect(keys.indexOf(toolbarFields[i])).toBeLessThan(keys.indexOf(toolbarFields[i + 1])); + } }); - it('covers all NamedListView boolean toggles in userActions', () => { + it('covers all NamedListView boolean toggles in userActions in spec order', () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'userActions')!; const keys = section.fields.map(f => f.key); - expect(keys).toContain('clickIntoRecordDetails'); - expect(keys).toContain('inlineEdit'); - expect(keys).toContain('addDeleteRecordsInline'); + // Spec order: inlineEdit → clickIntoRecordDetails → addDeleteRecordsInline + expect(keys.indexOf('inlineEdit')).toBeLessThan(keys.indexOf('clickIntoRecordDetails')); + expect(keys.indexOf('clickIntoRecordDetails')).toBeLessThan(keys.indexOf('addDeleteRecordsInline')); + }); + + // Protocol suggestions: UI fields not in NamedListView spec + it('documents UI extension fields not in NamedListView spec', () => { + const keys = allFieldKeys(); + // These fields are UI extensions — documented as protocol suggestions + const uiExtensions = ['description', '_source', '_groupBy', '_typeOptions']; + for (const ext of uiExtensions) { + expect(keys).toContain(ext); + } }); }); }); diff --git a/apps/console/src/utils/view-config-schema.tsx b/apps/console/src/utils/view-config-schema.tsx index 807d33313..e8a8108e5 100644 --- a/apps/console/src/utils/view-config-schema.tsx +++ b/apps/console/src/utils/view-config-schema.tsx @@ -1311,6 +1311,7 @@ function buildSharingSection( title: t('console.objectView.sharing'), collapsible: true, fields: [ + // spec: NamedListView.sharing.enabled { key: '_sharingEnabled', label: t('console.objectView.sharingEnabled'), @@ -1328,6 +1329,7 @@ function buildSharingSection( ), }, + // spec: NamedListView.sharing.visibility { key: '_sharingVisibility', label: t('console.objectView.sharingVisibility'), @@ -1368,6 +1370,7 @@ function buildAccessibilitySection( title: t('console.objectView.accessibility'), collapsible: true, fields: [ + // spec: NamedListView.aria.label { key: '_ariaLabel', label: t('console.objectView.ariaLabel'), @@ -1385,6 +1388,7 @@ function buildAccessibilitySection( ), }, + // spec: NamedListView.aria.describedBy { key: '_ariaDescribedBy', label: t('console.objectView.ariaDescribedBy'), @@ -1402,6 +1406,7 @@ function buildAccessibilitySection( ), }, + // spec: NamedListView.aria.live — polite/assertive/off { key: '_ariaLive', label: t('console.objectView.ariaLive'),