From 0f064eb7462c34f36e19e3c3e041ce686a90a240 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:11:26 +0000 Subject: [PATCH 1/6] Initial plan From c5aa70cec1e78ad3cee1bbaef0775303f9dc9d2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:22:38 +0000 Subject: [PATCH 2/6] fix: add visibleWhen conditions to view config panel fields, remove redundant densityMode - Add visibleWhen to toolbar toggles: showGroup (grid/kanban), showColor (grid), showDensity (grid) - Add visibleWhen to data fields: prefixField (grid), _groupBy (hide for kanban), searchableFields/filterableFields/quickFilters/virtualScroll (grid) - Add visibleWhen to appearance fields: collapseAllByDefault (groupBy-dependent), fieldTextColor/showDescription/rowHeight/conditionalFormatting (grid) - Remove densityMode field (redundant with rowHeight) - Extend buildSwitchField and buildFieldMultiSelect helpers with visibleWhen param - Add 16 new tests for visibility predicates - Update ROADMAP.md with Phase 6 cleanup Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- ROADMAP.md | 13 +- .../src/__tests__/ViewConfigPanel.test.tsx | 59 +------ .../src/__tests__/view-config-schema.test.tsx | 167 +++++++++++++++++- apps/console/src/utils/view-config-schema.tsx | 89 ++++++---- 4 files changed, 232 insertions(+), 96 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 83f9aa698..b1561a4d6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -403,7 +403,7 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind **Phase 2 — Schema Factory (All Sections):** - [x] Page Config section: label, description, viewType, toolbar toggles (7 switches), navigation mode/width/openNewTab, selection, addRecord sub-editor, export + sub-config, showRecordCount, allowPrinting - [x] Data section: source, sortBy (expandable), groupBy, prefixField, columns selector (expandable w/ reorder), filterBy (expandable), pagination, searchable/filterable/hidden fields (expandable), quickFilters (expandable), virtualScroll, type-specific options (kanban/calendar/map/gallery/timeline/gantt) -- [x] Appearance section: color, fieldTextColor, rowHeight (icon group), wrapHeaders, showDescription, collapseAllByDefault, striped, bordered, resizable, densityMode, conditionalFormatting (expandable), emptyState (title/message/icon) +- [x] Appearance section: color, fieldTextColor (grid only), rowHeight (icon group, grid only), wrapHeaders, showDescription (grid only), collapseAllByDefault (groupBy-dependent), striped, bordered, resizable, conditionalFormatting (expandable, grid only), emptyState (title/message/icon) - [x] User Actions section: inlineEdit, addDeleteRecordsInline, rowActions (expandable), bulkActions (expandable) - [x] Sharing section: sharingEnabled, sharingVisibility (visibleWhen: sharing.enabled) - [x] Accessibility section: ariaLabel, ariaDescribedBy, ariaLive @@ -433,6 +433,17 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind - [x] Add `helpText` to navigation-dependent fields (width/openNewTab) with i18n hints (all 11 locales) - [x] 24 new tests: expandedSections override (3), disabledWhen evaluation (2), grid-only disabledWhen predicates (16), helpText validation (2), description spec alignment (1) +**Phase 6 — Config Panel Cleanup (Invalid Items Fix):** +- [x] Remove `densityMode` field from appearance section (redundant with `rowHeight` which provides finer 5-value granularity) +- [x] Add `visibleWhen` to toolbar toggles: `showGroup` (grid/kanban only), `showColor` (grid only), `showDensity` (grid only) +- [x] Add `visibleWhen` to data fields: `prefixField` (grid only), `_groupBy` (hidden for kanban — uses dedicated type-specific `kanban.groupByField`), `searchableFields`/`filterableFields`/`quickFilters`/`virtualScroll` (grid only) +- [x] Add `visibleWhen` to appearance fields: `collapseAllByDefault` (only when `groupBy` is set), `fieldTextColor`/`showDescription`/`rowHeight`/`conditionalFormatting` (grid only) +- [x] Extend `buildSwitchField` and `buildFieldMultiSelect` helpers to accept `visibleWhen` parameter +- [x] 15 new visibleWhen predicate tests covering all new conditional visibility rules +- [x] 1 new test verifying `densityMode` removal +- [x] Updated ViewConfigPanel interaction tests to account for new visibility conditions +- [x] All 244 schema + panel tests pass, 10 config-sync integration tests pass + **Code Reduction:** ~1655 lines imperative → ~170 lines declarative wrapper + ~1100 lines schema factory + ~180 lines shared utils = **>50% net reduction in component code** with significantly improved maintainability ### P1.9 Console — Content Area Layout & Responsiveness diff --git a/apps/console/src/__tests__/ViewConfigPanel.test.tsx b/apps/console/src/__tests__/ViewConfigPanel.test.tsx index 9a833250f..65ad0d9d4 100644 --- a/apps/console/src/__tests__/ViewConfigPanel.test.tsx +++ b/apps/console/src/__tests__/ViewConfigPanel.test.tsx @@ -1255,7 +1255,7 @@ describe('ViewConfigPanel', () => { ); @@ -1349,13 +1349,13 @@ describe('ViewConfigPanel', () => { expect(screen.getByTestId('data-prefixField')).toBeInTheDocument(); }); - it('changes groupBy and propagates to kanban type option', () => { + it('changes groupBy for grid view', () => { const onViewUpdate = vi.fn(); render( @@ -1365,7 +1365,6 @@ describe('ViewConfigPanel', () => { fireEvent.change(groupBySelect, { target: { value: 'stage' } }); expect(onViewUpdate).toHaveBeenCalledWith('groupBy', 'stage'); - expect(onViewUpdate).toHaveBeenCalledWith('kanban', expect.objectContaining({ groupByField: 'stage' })); }); // ── Calendar endDateField test ── @@ -2014,34 +2013,7 @@ describe('ViewConfigPanel', () => { expect(onViewUpdate).toHaveBeenCalledWith('resizable', true); }); - it('renders density mode select in appearance section', () => { - render( - - ); - - expect(screen.getByTestId('select-densityMode')).toBeInTheDocument(); - }); - - it('changes densityMode and calls onViewUpdate', () => { - const onViewUpdate = vi.fn(); - render( - - ); - - fireEvent.change(screen.getByTestId('select-densityMode'), { target: { value: 'compact' } }); - expect(onViewUpdate).toHaveBeenCalledWith('densityMode', 'compact'); - }); + // densityMode tests removed — densityMode field was removed (redundant with rowHeight) it('renders conditional formatting editor when expanded', () => { render( @@ -2442,7 +2414,7 @@ describe('ViewConfigPanel', () => { @@ -2693,26 +2665,7 @@ describe('ViewConfigPanel', () => { })); }); - // ── Boundary: densityMode enum selection ── - - it('changes densityMode to all enum values', () => { - const onViewUpdate = vi.fn(); - render( - - ); - - const select = screen.getByTestId('select-densityMode'); - ['compact', 'comfortable', 'spacious'].forEach((mode) => { - fireEvent.change(select, { target: { value: mode } }); - expect(onViewUpdate).toHaveBeenCalledWith('densityMode', mode); - }); - }); + // densityMode enum test removed — densityMode field was removed (redundant with rowHeight) // ── Conditional rendering: addRecord sub-editor hidden when not enabled ── diff --git a/apps/console/src/__tests__/view-config-schema.test.tsx b/apps/console/src/__tests__/view-config-schema.test.tsx index f5bb11dfe..e66e97802 100644 --- a/apps/console/src/__tests__/view-config-schema.test.tsx +++ b/apps/console/src/__tests__/view-config-schema.test.tsx @@ -443,12 +443,13 @@ describe('buildViewConfigSchema', () => { 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 + // showDescription, resizable, rowHeight, conditionalFormatting, emptyState + // NOTE: densityMode removed (redundant with rowHeight) expect(fieldKeys).toEqual([ 'striped', 'bordered', 'color', 'wrapHeaders', 'collapseAllByDefault', 'fieldTextColor', 'showDescription', - 'resizable', 'densityMode', 'rowHeight', + 'resizable', 'rowHeight', '_conditionalFormatting', '_emptyState', ]); }); @@ -649,7 +650,7 @@ describe('spec alignment', () => { searchableFields: '_searchableFields', filterableFields: '_filterableFields', resizable: 'resizable', - densityMode: 'densityMode', + // densityMode removed — redundant with rowHeight rowHeight: 'rowHeight', hiddenFields: '_hiddenFields', exportOptions: '_export', // compound with allowExport @@ -764,6 +765,13 @@ describe('spec alignment', () => { expect(field.type).toBe('custom'); expect(field.key).toBe('inlineEdit'); }); + + it('densityMode field was removed (redundant with rowHeight)', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const field = section.fields.find(f => f.key === 'densityMode'); + expect(field).toBeUndefined(); + }); }); // ── All conditional visibleWhen predicates ─────────────────────────── @@ -804,6 +812,159 @@ describe('spec alignment', () => { // defaults to page mode when navigation is undefined → visible expect(field.visibleWhen!({})).toBe(true); }); + + // ── Toolbar toggle visibility by view type ────────────────────── + it('showGroup visible for grid (default) and kanban, hidden for others', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'pageConfig')!; + const field = section.fields.find(f => f.key === 'showGroup')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); // default = grid + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(true); + expect(field.visibleWhen!({ type: 'calendar' })).toBe(false); + expect(field.visibleWhen!({ type: 'gallery' })).toBe(false); + expect(field.visibleWhen!({ type: 'timeline' })).toBe(false); + expect(field.visibleWhen!({ type: 'map' })).toBe(false); + }); + + it('showColor visible only for grid (default), hidden for others', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'pageConfig')!; + const field = section.fields.find(f => f.key === 'showColor')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); // default = grid + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + expect(field.visibleWhen!({ type: 'calendar' })).toBe(false); + expect(field.visibleWhen!({ type: 'gallery' })).toBe(false); + }); + + it('showDensity visible only for grid (default), hidden for others', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'pageConfig')!; + const field = section.fields.find(f => f.key === 'showDensity')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); // default = grid + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + expect(field.visibleWhen!({ type: 'calendar' })).toBe(false); + }); + + // ── Data section visibility by view type ──────────────────────── + it('prefixField visible only for grid (default), hidden for others', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'data')!; + const field = section.fields.find(f => f.key === 'prefixField')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); // default = grid + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + expect(field.visibleWhen!({ type: 'calendar' })).toBe(false); + }); + + it('_groupBy hidden for kanban (uses type-specific kanban.groupByField)', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'data')!; + const field = section.fields.find(f => f.key === '_groupBy')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); // default = grid + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); // kanban uses dedicated groupByField + expect(field.visibleWhen!({ type: 'calendar' })).toBe(true); + expect(field.visibleWhen!({ type: 'gallery' })).toBe(true); + }); + + it('searchableFields visible only for grid', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'data')!; + const field = section.fields.find(f => f.key === '_searchableFields')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + }); + + it('filterableFields visible only for grid', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'data')!; + const field = section.fields.find(f => f.key === '_filterableFields')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + }); + + it('quickFilters visible only for grid', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'data')!; + const field = section.fields.find(f => f.key === '_quickFilters')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + }); + + it('virtualScroll visible only for grid', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'data')!; + const field = section.fields.find(f => f.key === 'virtualScroll')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + }); + + // ── Appearance section visibility by view type / state ────────── + it('collapseAllByDefault visible only when groupBy is set', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const field = section.fields.find(f => f.key === 'collapseAllByDefault')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(false); // no groupBy → hidden + expect(field.visibleWhen!({ groupBy: '' })).toBe(false); // empty groupBy → hidden + expect(field.visibleWhen!({ groupBy: 'status' })).toBe(true); // groupBy set → visible + }); + + it('fieldTextColor visible only for grid', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const field = section.fields.find(f => f.key === 'fieldTextColor')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + }); + + it('showDescription visible only for grid', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const field = section.fields.find(f => f.key === 'showDescription')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + }); + + it('rowHeight visible only for grid', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const field = section.fields.find(f => f.key === 'rowHeight')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + }); + + it('conditionalFormatting visible only for grid', () => { + const schema = buildSpecSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const field = section.fields.find(f => f.key === '_conditionalFormatting')!; + expect(field.visibleWhen).toBeDefined(); + expect(field.visibleWhen!({})).toBe(true); + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + }); }); // ── disabledWhen predicates for grid-only fields ───────────────────── diff --git a/apps/console/src/utils/view-config-schema.tsx b/apps/console/src/utils/view-config-schema.tsx index e22acd423..96a04aa53 100644 --- a/apps/console/src/utils/view-config-schema.tsx +++ b/apps/console/src/utils/view-config-schema.tsx @@ -59,6 +59,19 @@ export interface ViewSchemaFactoryOptions { sortItemsValue: SortItem[]; } +// --------------------------------------------------------------------------- +// View-type visibility predicates +// --------------------------------------------------------------------------- + +/** True when the view type is grid (or unset, since grid is the default) */ +const isGridView = (draft: Record) => draft.type == null || draft.type === 'grid'; + +/** True when the view type is grid or kanban (both support grouping) */ +const isGridOrKanbanView = (draft: Record) => draft.type == null || draft.type === 'grid' || draft.type === 'kanban'; + +/** True when the view type is NOT kanban (kanban has dedicated groupByField in type-specific options) */ +const isNotKanbanView = (draft: Record) => draft.type !== 'kanban'; + // --------------------------------------------------------------------------- // Schema factory // --------------------------------------------------------------------------- @@ -165,9 +178,12 @@ function buildPageConfigSection( 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 + buildSwitchField('showGroup', t('console.objectView.enableGroup'), 'toggle-showGroup', true, // spec: NamedListView.showGroup + false, undefined, isGridOrKanbanView), + buildSwitchField('showColor', t('console.objectView.enableColor'), 'toggle-showColor', true, // spec: NamedListView.showColor + false, undefined, isGridView), + buildSwitchField('showDensity', t('console.objectView.enableDensity'), 'toggle-showDensity', true, // spec: NamedListView.showDensity + false, undefined, isGridView), // spec: NamedListView.allowExport + NamedListView.exportOptions — export toggle + sub-config { key: '_export', @@ -621,11 +637,12 @@ function buildDataSection( ); }, }, - // spec: NamedListView.prefixField + // spec: NamedListView.prefixField (grid-only: prefix column is a grid concept) { key: 'prefixField', label: t('console.objectView.prefixField'), type: 'custom', + visibleWhen: isGridView, render: (value, onChange) => ( ), }, - // spec: NamedListView.showDescription - buildSwitchField('showDescription', t('console.objectView.showFieldDescriptions'), 'toggle-showDescription', true), + // spec: NamedListView.showDescription (grid-only: field descriptions only shown in grid headers) + buildSwitchField('showDescription', t('console.objectView.showFieldDescriptions'), 'toggle-showDescription', true, + false, undefined, isGridView), // spec: NamedListView.resizable (grid-only) buildSwitchField('resizable', t('console.objectView.resizableColumns'), 'toggle-resizable', false, true, (draft) => draft.type != null && draft.type !== 'grid'), - // 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 + // NOTE: densityMode removed — redundant with rowHeight which provides finer 5-value granularity + // spec: NamedListView.rowHeight — 5-value enum: compact/short/medium/tall/extra_tall (grid-only) { key: 'rowHeight', label: t('console.objectView.rowHeight'), type: 'custom', + visibleWhen: isGridView, render: (value, onChange) => (
@@ -1086,11 +1091,12 @@ function buildAppearanceSection( ), }, - // spec: NamedListView.conditionalFormatting + // spec: NamedListView.conditionalFormatting (grid-only: conditional formatting applies to grid cells/rows) { key: '_conditionalFormatting', label: t('console.objectView.conditionalFormatting'), type: 'custom', + visibleWhen: isGridView, render: (_value, _onChange, draft) => { return ( ) => boolean, + visibleWhen?: (draft: Record) => boolean, ): ConfigField { return { key, label, type: 'custom', disabledWhen, + visibleWhen, render: (value, onChange, draft) => ( ) => boolean, ): ConfigField { return { key: `_${key}`, label, type: 'custom', + visibleWhen, render: (_value, _onChange, draft) => { return ( Date: Tue, 24 Feb 2026 02:43:51 +0000 Subject: [PATCH 3/6] test: update view-config-schema tests to match schema changes - Remove tests for removed fields: prefixField, collapseAllByDefault, fieldTextColor, clickIntoRecordDetails - Update data/appearance/userActions section field key assertions - Convert disabledWhen tests for striped/bordered/wrapHeaders/resizable to visibleWhen tests (schema changed from disable to hide) - Update visibleWhen predicates: showGroup now includes gallery, showColor uses supportsColorField, _groupBy uses supportsGenericGroupBy, conditionalFormatting uses supportsConditionalFormatting - Update searchableFields/filterableFields/quickFilters/showDescription to be universal (no visibleWhen) - Add color field visibleWhen test for supportsColorField predicate - Remove spec coverage entries for removed fields Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/__tests__/view-config-schema.test.tsx | 154 ++++++++---------- 1 file changed, 66 insertions(+), 88 deletions(-) diff --git a/apps/console/src/__tests__/view-config-schema.test.tsx b/apps/console/src/__tests__/view-config-schema.test.tsx index e66e97802..163410401 100644 --- a/apps/console/src/__tests__/view-config-schema.test.tsx +++ b/apps/console/src/__tests__/view-config-schema.test.tsx @@ -411,13 +411,14 @@ describe('buildViewConfigSchema', () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'data')!; const fieldKeys = section.fields.map(f => f.key); - // Spec order: columns, filter, sort, prefixField, pagination, searchableFields, filterableFields, + // Spec order: columns, filter, sort, pagination, searchableFields, filterableFields, // hiddenFields, quickFilters, virtualScroll - // _source is UI extension (first), _groupBy is UI extension (after prefixField), _typeOptions is UI extension (last) + // _source is UI extension (first), _groupBy is UI extension (after sort), _typeOptions is UI extension (last) + // NOTE: prefixField removed — not consumed by runtime expect(fieldKeys).toEqual([ '_source', '_columns', '_filterBy', '_sortBy', - 'prefixField', '_groupBy', + '_groupBy', '_pageSize', '_pageSizeOptions', '_searchableFields', '_filterableFields', '_hiddenFields', '_quickFilters', @@ -442,13 +443,14 @@ describe('buildViewConfigSchema', () => { 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, + // Spec order: striped, bordered, color, wrapHeaders, // showDescription, resizable, rowHeight, conditionalFormatting, emptyState + // NOTE: collapseAllByDefault removed — not consumed by runtime + // NOTE: fieldTextColor removed — not consumed by runtime // NOTE: densityMode removed (redundant with rowHeight) expect(fieldKeys).toEqual([ 'striped', 'bordered', 'color', - 'wrapHeaders', 'collapseAllByDefault', - 'fieldTextColor', 'showDescription', + 'wrapHeaders', 'showDescription', 'resizable', 'rowHeight', '_conditionalFormatting', '_emptyState', ]); @@ -470,18 +472,19 @@ describe('buildViewConfigSchema', () => { 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 + // Spec order: inlineEdit, addDeleteRecordsInline, rowActions, bulkActions + // NOTE: clickIntoRecordDetails removed — controlled via navigation mode expect(fieldKeys).toEqual([ - 'inlineEdit', 'clickIntoRecordDetails', 'addDeleteRecordsInline', + 'inlineEdit', 'addDeleteRecordsInline', '_rowActions', '_bulkActions', ]); }); - it('inlineEdit comes before clickIntoRecordDetails per spec', () => { + it('inlineEdit comes before addDeleteRecordsInline per spec', () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'userActions')!; const fieldKeys = section.fields.map(f => f.key); - expect(fieldKeys.indexOf('inlineEdit')).toBeLessThan(fieldKeys.indexOf('clickIntoRecordDetails')); + expect(fieldKeys.indexOf('inlineEdit')).toBeLessThan(fieldKeys.indexOf('addDeleteRecordsInline')); }); }); @@ -637,12 +640,12 @@ describe('spec alignment', () => { color: 'color', inlineEdit: 'inlineEdit', wrapHeaders: 'wrapHeaders', - clickIntoRecordDetails: 'clickIntoRecordDetails', + // clickIntoRecordDetails removed — controlled via navigation mode addRecordViaForm: '_addRecord', // compound field addDeleteRecordsInline: 'addDeleteRecordsInline', - collapseAllByDefault: 'collapseAllByDefault', - fieldTextColor: 'fieldTextColor', - prefixField: 'prefixField', + // collapseAllByDefault removed — not consumed by runtime + // fieldTextColor removed — not consumed by runtime + // prefixField removed — not consumed by runtime showDescription: 'showDescription', navigation: '_navigationMode', // compound: mode/width/openNewTab selection: '_selectionType', @@ -693,9 +696,9 @@ describe('spec alignment', () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'userActions')!; const keys = section.fields.map(f => f.key); - // Spec order: inlineEdit → clickIntoRecordDetails → addDeleteRecordsInline - expect(keys.indexOf('inlineEdit')).toBeLessThan(keys.indexOf('clickIntoRecordDetails')); - expect(keys.indexOf('clickIntoRecordDetails')).toBeLessThan(keys.indexOf('addDeleteRecordsInline')); + // Spec order: inlineEdit → addDeleteRecordsInline + // NOTE: clickIntoRecordDetails removed — controlled via navigation mode + expect(keys.indexOf('inlineEdit')).toBeLessThan(keys.indexOf('addDeleteRecordsInline')); }); // Protocol suggestions: UI fields not in NamedListView spec @@ -744,14 +747,6 @@ describe('spec alignment', () => { return section.fields.find(f => f.key === fieldKey)!; } - it('collapseAllByDefault is an explicitTrue switch field', () => { - // explicitTrue fields only show checked when value === true - const field = findField('appearance', 'collapseAllByDefault'); - expect(field.render).toBeDefined(); - expect(field.type).toBe('custom'); - expect(field.key).toBe('collapseAllByDefault'); - }); - it('showDescription is a defaultOn switch field', () => { const field = findField('appearance', 'showDescription'); expect(field.render).toBeDefined(); @@ -814,7 +809,7 @@ describe('spec alignment', () => { }); // ── Toolbar toggle visibility by view type ────────────────────── - it('showGroup visible for grid (default) and kanban, hidden for others', () => { + it('showGroup visible for grid (default), kanban, and gallery, hidden for others', () => { const schema = buildSpecSchema(); const section = schema.sections.find(s => s.key === 'pageConfig')!; const field = section.fields.find(f => f.key === 'showGroup')!; @@ -822,22 +817,26 @@ describe('spec alignment', () => { expect(field.visibleWhen!({})).toBe(true); // default = grid expect(field.visibleWhen!({ type: 'grid' })).toBe(true); expect(field.visibleWhen!({ type: 'kanban' })).toBe(true); + expect(field.visibleWhen!({ type: 'gallery' })).toBe(true); expect(field.visibleWhen!({ type: 'calendar' })).toBe(false); - expect(field.visibleWhen!({ type: 'gallery' })).toBe(false); expect(field.visibleWhen!({ type: 'timeline' })).toBe(false); + expect(field.visibleWhen!({ type: 'gantt' })).toBe(false); expect(field.visibleWhen!({ type: 'map' })).toBe(false); }); - it('showColor visible only for grid (default), hidden for others', () => { + it('showColor visible for grid (default), calendar, timeline, gantt, hidden for others', () => { const schema = buildSpecSchema(); const section = schema.sections.find(s => s.key === 'pageConfig')!; const field = section.fields.find(f => f.key === 'showColor')!; expect(field.visibleWhen).toBeDefined(); expect(field.visibleWhen!({})).toBe(true); // default = grid expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'calendar' })).toBe(true); + expect(field.visibleWhen!({ type: 'timeline' })).toBe(true); + expect(field.visibleWhen!({ type: 'gantt' })).toBe(true); expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); - expect(field.visibleWhen!({ type: 'calendar' })).toBe(false); expect(field.visibleWhen!({ type: 'gallery' })).toBe(false); + expect(field.visibleWhen!({ type: 'map' })).toBe(false); }); it('showDensity visible only for grid (default), hidden for others', () => { @@ -852,57 +851,40 @@ describe('spec alignment', () => { }); // ── Data section visibility by view type ──────────────────────── - it('prefixField visible only for grid (default), hidden for others', () => { - const schema = buildSpecSchema(); - const section = schema.sections.find(s => s.key === 'data')!; - const field = section.fields.find(f => f.key === 'prefixField')!; - expect(field.visibleWhen).toBeDefined(); - expect(field.visibleWhen!({})).toBe(true); // default = grid - expect(field.visibleWhen!({ type: 'grid' })).toBe(true); - expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); - expect(field.visibleWhen!({ type: 'calendar' })).toBe(false); - }); - - it('_groupBy hidden for kanban (uses type-specific kanban.groupByField)', () => { + it('_groupBy visible for grid (default) and gallery, hidden for others', () => { const schema = buildSpecSchema(); const section = schema.sections.find(s => s.key === 'data')!; const field = section.fields.find(f => f.key === '_groupBy')!; expect(field.visibleWhen).toBeDefined(); expect(field.visibleWhen!({})).toBe(true); // default = grid expect(field.visibleWhen!({ type: 'grid' })).toBe(true); - expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); // kanban uses dedicated groupByField - expect(field.visibleWhen!({ type: 'calendar' })).toBe(true); expect(field.visibleWhen!({ type: 'gallery' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); // kanban uses dedicated groupByField + expect(field.visibleWhen!({ type: 'calendar' })).toBe(false); + expect(field.visibleWhen!({ type: 'timeline' })).toBe(false); + expect(field.visibleWhen!({ type: 'gantt' })).toBe(false); + expect(field.visibleWhen!({ type: 'map' })).toBe(false); }); - it('searchableFields visible only for grid', () => { + it('searchableFields is universal (no visibleWhen)', () => { const schema = buildSpecSchema(); const section = schema.sections.find(s => s.key === 'data')!; const field = section.fields.find(f => f.key === '_searchableFields')!; - expect(field.visibleWhen).toBeDefined(); - expect(field.visibleWhen!({})).toBe(true); - expect(field.visibleWhen!({ type: 'grid' })).toBe(true); - expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + expect(field.visibleWhen).toBeUndefined(); }); - it('filterableFields visible only for grid', () => { + it('filterableFields is universal (no visibleWhen)', () => { const schema = buildSpecSchema(); const section = schema.sections.find(s => s.key === 'data')!; const field = section.fields.find(f => f.key === '_filterableFields')!; - expect(field.visibleWhen).toBeDefined(); - expect(field.visibleWhen!({})).toBe(true); - expect(field.visibleWhen!({ type: 'grid' })).toBe(true); - expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + expect(field.visibleWhen).toBeUndefined(); }); - it('quickFilters visible only for grid', () => { + it('quickFilters is universal (no visibleWhen)', () => { const schema = buildSpecSchema(); const section = schema.sections.find(s => s.key === 'data')!; const field = section.fields.find(f => f.key === '_quickFilters')!; - expect(field.visibleWhen).toBeDefined(); - expect(field.visibleWhen!({})).toBe(true); - expect(field.visibleWhen!({ type: 'grid' })).toBe(true); - expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + expect(field.visibleWhen).toBeUndefined(); }); it('virtualScroll visible only for grid', () => { @@ -916,34 +898,26 @@ describe('spec alignment', () => { }); // ── Appearance section visibility by view type / state ────────── - it('collapseAllByDefault visible only when groupBy is set', () => { + it('color visible for grid (default), calendar, timeline, gantt, hidden for others', () => { const schema = buildSpecSchema(); const section = schema.sections.find(s => s.key === 'appearance')!; - const field = section.fields.find(f => f.key === 'collapseAllByDefault')!; + const field = section.fields.find(f => f.key === 'color')!; expect(field.visibleWhen).toBeDefined(); - expect(field.visibleWhen!({})).toBe(false); // no groupBy → hidden - expect(field.visibleWhen!({ groupBy: '' })).toBe(false); // empty groupBy → hidden - expect(field.visibleWhen!({ groupBy: 'status' })).toBe(true); // groupBy set → visible - }); - - it('fieldTextColor visible only for grid', () => { - const schema = buildSpecSchema(); - const section = schema.sections.find(s => s.key === 'appearance')!; - const field = section.fields.find(f => f.key === 'fieldTextColor')!; - expect(field.visibleWhen).toBeDefined(); - expect(field.visibleWhen!({})).toBe(true); + expect(field.visibleWhen!({})).toBe(true); // default = grid expect(field.visibleWhen!({ type: 'grid' })).toBe(true); + expect(field.visibleWhen!({ type: 'calendar' })).toBe(true); + expect(field.visibleWhen!({ type: 'timeline' })).toBe(true); + expect(field.visibleWhen!({ type: 'gantt' })).toBe(true); expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + expect(field.visibleWhen!({ type: 'gallery' })).toBe(false); + expect(field.visibleWhen!({ type: 'map' })).toBe(false); }); - it('showDescription visible only for grid', () => { + it('showDescription is universal (no visibleWhen)', () => { const schema = buildSpecSchema(); const section = schema.sections.find(s => s.key === 'appearance')!; const field = section.fields.find(f => f.key === 'showDescription')!; - expect(field.visibleWhen).toBeDefined(); - expect(field.visibleWhen!({})).toBe(true); - expect(field.visibleWhen!({ type: 'grid' })).toBe(true); - expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + expect(field.visibleWhen).toBeUndefined(); }); it('rowHeight visible only for grid', () => { @@ -956,19 +930,23 @@ describe('spec alignment', () => { expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); }); - it('conditionalFormatting visible only for grid', () => { + it('conditionalFormatting visible for grid (default) and kanban, hidden for others', () => { const schema = buildSpecSchema(); const section = schema.sections.find(s => s.key === 'appearance')!; const field = section.fields.find(f => f.key === '_conditionalFormatting')!; expect(field.visibleWhen).toBeDefined(); expect(field.visibleWhen!({})).toBe(true); expect(field.visibleWhen!({ type: 'grid' })).toBe(true); - expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(true); + expect(field.visibleWhen!({ type: 'calendar' })).toBe(false); + expect(field.visibleWhen!({ type: 'gallery' })).toBe(false); + expect(field.visibleWhen!({ type: 'timeline' })).toBe(false); + expect(field.visibleWhen!({ type: 'map' })).toBe(false); }); }); - // ── disabledWhen predicates for grid-only fields ───────────────────── - describe('disabledWhen predicates for grid-only fields', () => { + // ── visibleWhen predicates for grid-only appearance fields ────────── + describe('visibleWhen predicates for grid-only appearance fields', () => { function buildSchema() { return buildViewConfigSchema({ t: mockT, @@ -983,32 +961,32 @@ describe('spec alignment', () => { const gridOnlyFields = ['striped', 'bordered', 'wrapHeaders', 'resizable']; for (const fieldKey of gridOnlyFields) { - it(`${fieldKey} should have disabledWhen predicate`, () => { + it(`${fieldKey} should have visibleWhen predicate`, () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'appearance')!; const field = section.fields.find(f => f.key === fieldKey)!; - expect(field.disabledWhen).toBeDefined(); + expect(field.visibleWhen).toBeDefined(); }); - it(`${fieldKey} should be disabled when view type is kanban`, () => { + it(`${fieldKey} should be hidden when view type is kanban`, () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'appearance')!; const field = section.fields.find(f => f.key === fieldKey)!; - expect(field.disabledWhen!({ type: 'kanban' })).toBe(true); + expect(field.visibleWhen!({ type: 'kanban' })).toBe(false); }); - it(`${fieldKey} should not be disabled when view type is grid`, () => { + it(`${fieldKey} should be visible when view type is grid`, () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'appearance')!; const field = section.fields.find(f => f.key === fieldKey)!; - expect(field.disabledWhen!({ type: 'grid' })).toBe(false); + expect(field.visibleWhen!({ type: 'grid' })).toBe(true); }); - it(`${fieldKey} should not be disabled when view type is undefined (default grid)`, () => { + it(`${fieldKey} should be visible when view type is undefined (default grid)`, () => { const schema = buildSchema(); const section = schema.sections.find(s => s.key === 'appearance')!; const field = section.fields.find(f => f.key === fieldKey)!; - expect(field.disabledWhen!({})).toBe(false); + expect(field.visibleWhen!({})).toBe(true); }); } }); From 23825217fa65ac887d5324fe4b15e84c2ad37076 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:47:18 +0000 Subject: [PATCH 4/6] fix: remove tests for removed schema fields in ViewConfigPanel - Remove fieldTextColor and collapseAllByDefault from appearance fields test - Remove prefixField assertion from Data section test - Delete collapseAllByDefault toggle test (field removed from schema) - Delete clickIntoRecordDetails toggle test (field removed from schema) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/__tests__/ViewConfigPanel.test.tsx | 37 +---- apps/console/src/utils/view-config-schema.tsx | 143 +++++++----------- 2 files changed, 57 insertions(+), 123 deletions(-) diff --git a/apps/console/src/__tests__/ViewConfigPanel.test.tsx b/apps/console/src/__tests__/ViewConfigPanel.test.tsx index 65ad0d9d4..36b7b048a 100644 --- a/apps/console/src/__tests__/ViewConfigPanel.test.tsx +++ b/apps/console/src/__tests__/ViewConfigPanel.test.tsx @@ -1250,7 +1250,7 @@ describe('ViewConfigPanel', () => { // ── Appearance fields tests ── - it('renders new appearance fields: color, fieldTextColor, rowHeight, wrapHeaders, collapseAllByDefault', () => { + it('renders new appearance fields: color, rowHeight, wrapHeaders', () => { render( { ); expect(screen.getByTestId('appearance-color')).toBeInTheDocument(); - expect(screen.getByTestId('appearance-fieldTextColor')).toBeInTheDocument(); expect(screen.getByTestId('appearance-rowHeight')).toBeInTheDocument(); expect(screen.getByTestId('toggle-wrapHeaders')).toBeInTheDocument(); - expect(screen.getByTestId('toggle-collapseAllByDefault')).toBeInTheDocument(); }); it('changes row height via icon buttons', () => { @@ -1346,7 +1344,6 @@ describe('ViewConfigPanel', () => { ); expect(screen.getByTestId('data-groupBy')).toBeInTheDocument(); - expect(screen.getByTestId('data-prefixField')).toBeInTheDocument(); }); it('changes groupBy for grid view', () => { @@ -2408,22 +2405,6 @@ describe('ViewConfigPanel', () => { // ── Spec alignment: toggle interaction tests for all switch fields ── - it('toggles collapseAllByDefault and calls onViewUpdate', () => { - const onViewUpdate = vi.fn(); - render( - - ); - - fireEvent.click(screen.getByTestId('toggle-collapseAllByDefault')); - expect(onViewUpdate).toHaveBeenCalledWith('collapseAllByDefault', true); - }); - it('toggles showDescription and calls onViewUpdate', () => { const onViewUpdate = vi.fn(); render( @@ -2440,22 +2421,6 @@ describe('ViewConfigPanel', () => { expect(onViewUpdate).toHaveBeenCalledWith('showDescription', false); }); - it('toggles clickIntoRecordDetails and calls onViewUpdate', () => { - const onViewUpdate = vi.fn(); - render( - - ); - - fireEvent.click(screen.getByTestId('toggle-clickIntoRecordDetails')); - expect(onViewUpdate).toHaveBeenCalledWith('clickIntoRecordDetails', false); - }); - it('toggles addDeleteRecordsInline and calls onViewUpdate', () => { const onViewUpdate = vi.fn(); render( diff --git a/apps/console/src/utils/view-config-schema.tsx b/apps/console/src/utils/view-config-schema.tsx index 96a04aa53..3e62deac4 100644 --- a/apps/console/src/utils/view-config-schema.tsx +++ b/apps/console/src/utils/view-config-schema.tsx @@ -66,11 +66,20 @@ export interface ViewSchemaFactoryOptions { /** True when the view type is grid (or unset, since grid is the default) */ const isGridView = (draft: Record) => draft.type == null || draft.type === 'grid'; -/** True when the view type is grid or kanban (both support grouping) */ -const isGridOrKanbanView = (draft: Record) => draft.type == null || draft.type === 'grid' || draft.type === 'kanban'; +/** True for views that support the Group toolbar button (grid/kanban/gallery have grouping support) */ +const supportsGrouping = (draft: Record) => draft.type == null || ['grid', 'kanban', 'gallery'].includes(draft.type); -/** True when the view type is NOT kanban (kanban has dedicated groupByField in type-specific options) */ -const isNotKanbanView = (draft: Record) => draft.type !== 'kanban'; +/** True for views where the color field is consumed at runtime (grid, calendar, timeline, gantt) */ +const supportsColorField = (draft: Record) => draft.type == null || ['grid', 'calendar', 'timeline', 'gantt'].includes(draft.type); + +/** True for views that support conditional formatting (grid, kanban) */ +const supportsConditionalFormatting = (draft: Record) => draft.type == null || ['grid', 'kanban'].includes(draft.type); + +/** True for views that support row/bulk actions (grid, kanban) */ +const supportsRowActions = (draft: Record) => draft.type == null || ['grid', 'kanban'].includes(draft.type); + +/** True for views that support generic groupBy (grid, gallery — kanban has dedicated groupByField) */ +const supportsGenericGroupBy = (draft: Record) => draft.type == null || ['grid', 'gallery'].includes(draft.type); // --------------------------------------------------------------------------- // Schema factory @@ -179,9 +188,9 @@ function buildPageConfigSection( 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 - false, undefined, isGridOrKanbanView), + false, undefined, supportsGrouping), buildSwitchField('showColor', t('console.objectView.enableColor'), 'toggle-showColor', true, // spec: NamedListView.showColor - false, undefined, isGridView), + false, undefined, supportsColorField), buildSwitchField('showDensity', t('console.objectView.enableDensity'), 'toggle-showDensity', true, // spec: NamedListView.showDensity false, undefined, isGridView), // spec: NamedListView.allowExport + NamedListView.exportOptions — export toggle + sub-config @@ -428,10 +437,12 @@ function buildPageConfigSection( ); }, }, - // spec: NamedListView.showRecordCount - buildSwitchField('showRecordCount', t('console.objectView.showRecordCount'), 'toggle-showRecordCount', false, true), - // spec: NamedListView.allowPrinting - buildSwitchField('allowPrinting', t('console.objectView.allowPrinting'), 'toggle-allowPrinting', false, true), + // spec: NamedListView.showRecordCount (grid-only: record count bar is a grid feature) + buildSwitchField('showRecordCount', t('console.objectView.showRecordCount'), 'toggle-showRecordCount', false, true, + undefined, isGridView), + // spec: NamedListView.allowPrinting (grid-only: print renders grid table) + buildSwitchField('allowPrinting', t('console.objectView.allowPrinting'), 'toggle-allowPrinting', false, true, + undefined, isGridView), ], }; } @@ -637,34 +648,13 @@ function buildDataSection( ); }, }, - // spec: NamedListView.prefixField (grid-only: prefix column is a grid concept) - { - key: 'prefixField', - label: t('console.objectView.prefixField'), - type: 'custom', - visibleWhen: isGridView, - render: (value, onChange) => ( - - - - ), - }, - // UI extension: groupBy — not in NamedListView spec. Hidden for kanban (uses dedicated kanban.groupByField in type-specific options). + // NOTE: prefixField removed — not consumed by any runtime renderer + // UI extension: groupBy — visible for grid/gallery (kanban has dedicated type-specific option). { key: '_groupBy', label: t('console.objectView.groupBy'), type: 'custom', - visibleWhen: isNotKanbanView, + visibleWhen: supportsGenericGroupBy, render: (_value, _onChange, draft) => { const viewType = draft.type || 'grid'; const groupByValue = draft.kanban?.groupByField || draft.kanban?.groupField || draft.groupBy || ''; @@ -732,18 +722,17 @@ function buildDataSection( ), }, - // spec: NamedListView.searchableFields (grid-only: search field whitelisting is a grid concept) - buildFieldMultiSelect('searchableFields', t('console.objectView.searchableFields'), 'searchable-fields-selector', 'searchable-field', fieldOptions, updateField, 'selected', isGridView), - // spec: NamedListView.filterableFields (grid-only: filter field whitelisting is a grid concept) - buildFieldMultiSelect('filterableFields', t('console.objectView.filterableFields'), 'filterable-fields-selector', 'filterable-field', fieldOptions, updateField, 'selected', isGridView), + // spec: NamedListView.searchableFields (universal: search applies at data fetch level for all views) + buildFieldMultiSelect('searchableFields', t('console.objectView.searchableFields'), 'searchable-fields-selector', 'searchable-field', fieldOptions, updateField, 'selected'), + // spec: NamedListView.filterableFields (universal: filter field whitelisting applies to all views) + buildFieldMultiSelect('filterableFields', t('console.objectView.filterableFields'), 'filterable-fields-selector', 'filterable-field', fieldOptions, updateField, 'selected'), // spec: NamedListView.hiddenFields buildFieldMultiSelect('hiddenFields', t('console.objectView.hiddenFields'), 'hidden-fields-selector', 'hidden-field', fieldOptions, updateField, 'hidden'), - // spec: NamedListView.quickFilters (grid-only: quick filter buttons are a grid toolbar feature) + // spec: NamedListView.quickFilters (universal: quick filter buttons render in toolbar for all views) { key: '_quickFilters', label: t('console.objectView.quickFilters'), type: 'custom', - visibleWhen: isGridView, render: (_value, _onChange, draft) => { return ( draft.type != null && draft.type !== 'grid'), - // spec: NamedListView.bordered (grid-only) + undefined, isGridView), + // spec: NamedListView.bordered (grid-only: borders are a grid concept) buildSwitchField('bordered', t('console.objectView.bordered'), 'toggle-bordered', false, true, - (draft) => draft.type != null && draft.type !== 'grid'), - // spec: NamedListView.color — field for row/card coloring + undefined, isGridView), + // spec: NamedListView.color — field for row/card coloring (grid, calendar, timeline, gantt) { key: 'color', label: t('console.objectView.color'), type: 'custom', + visibleWhen: supportsColorField, render: (value, onChange) => ( ) => onChange(e.target.value)} - > - - {fieldOptions.map(f => ( - - ))} - - - ), - }, - // spec: NamedListView.showDescription (grid-only: field descriptions only shown in grid headers) - buildSwitchField('showDescription', t('console.objectView.showFieldDescriptions'), 'toggle-showDescription', true, - false, undefined, isGridView), - // spec: NamedListView.resizable (grid-only) + undefined, isGridView), + // NOTE: collapseAllByDefault removed — not consumed by any runtime renderer + // NOTE: fieldTextColor removed — not consumed by any runtime renderer + // spec: NamedListView.showDescription (universal: view description shown below toolbar for all views) + buildSwitchField('showDescription', t('console.objectView.showFieldDescriptions'), 'toggle-showDescription', true), + // spec: NamedListView.resizable (grid-only: column resizing is a grid concept) buildSwitchField('resizable', t('console.objectView.resizableColumns'), 'toggle-resizable', false, true, - (draft) => draft.type != null && draft.type !== 'grid'), + undefined, isGridView), // NOTE: densityMode removed — redundant with rowHeight which provides finer 5-value granularity // spec: NamedListView.rowHeight — 5-value enum: compact/short/medium/tall/extra_tall (grid-only) { @@ -1091,12 +1057,12 @@ function buildAppearanceSection( ), }, - // spec: NamedListView.conditionalFormatting (grid-only: conditional formatting applies to grid cells/rows) + // spec: NamedListView.conditionalFormatting (grid/kanban: both process conditional formatting rules) { key: '_conditionalFormatting', label: t('console.objectView.conditionalFormatting'), type: 'custom', - visibleWhen: isGridView, + visibleWhen: supportsConditionalFormatting, render: (_value, _onChange, draft) => { return ( { return ( { return ( Date: Tue, 24 Feb 2026 02:50:20 +0000 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20comprehensive=20view=20config=20pane?= =?UTF-8?q?l=20cleanup=20=E2=80=94=20remove=20unused=20fields,=20correct?= =?UTF-8?q?=20view-type=20visibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed fields not consumed by runtime: - prefixField (data section) - collapseAllByDefault (appearance section) - fieldTextColor (appearance section) - clickIntoRecordDetails (userActions section) - densityMode (appearance section, previous commit) Added semantic view-type visibility predicates: - supportsGrouping (grid/kanban/gallery) - supportsColorField (grid/calendar/timeline/gantt) - supportsConditionalFormatting (grid/kanban) - supportsRowActions (grid/kanban) - supportsGenericGroupBy (grid/gallery) Changed grid-only fields from disabledWhen to visibleWhen: - striped, bordered, wrapHeaders, resizable Added visibleWhen to previously universal fields: - showRecordCount, allowPrinting (grid only) - inlineEdit, addDeleteRecordsInline (grid only) - rowActions, bulkActions (grid/kanban) Corrected visibility for toolbar toggles: - showGroup: grid/kanban/gallery (was grid/kanban) - showColor: grid/calendar/timeline/gantt (was grid) - searchableFields/filterableFields/quickFilters/showDescription: universal (was grid) Updated 239 tests (103 schema + 136 panel), 10 integration tests pass. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- ROADMAP.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b1561a4d6..cce82c546 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -402,9 +402,9 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind **Phase 2 — Schema Factory (All Sections):** - [x] Page Config section: label, description, viewType, toolbar toggles (7 switches), navigation mode/width/openNewTab, selection, addRecord sub-editor, export + sub-config, showRecordCount, allowPrinting -- [x] Data section: source, sortBy (expandable), groupBy, prefixField, columns selector (expandable w/ reorder), filterBy (expandable), pagination, searchable/filterable/hidden fields (expandable), quickFilters (expandable), virtualScroll, type-specific options (kanban/calendar/map/gallery/timeline/gantt) -- [x] Appearance section: color, fieldTextColor (grid only), rowHeight (icon group, grid only), wrapHeaders, showDescription (grid only), collapseAllByDefault (groupBy-dependent), striped, bordered, resizable, conditionalFormatting (expandable, grid only), emptyState (title/message/icon) -- [x] User Actions section: inlineEdit, addDeleteRecordsInline, rowActions (expandable), bulkActions (expandable) +- [x] Data section: source, sortBy (expandable), groupBy (grid/gallery), columns selector (expandable w/ reorder), filterBy (expandable), pagination, searchable/filterable/hidden fields (expandable), quickFilters (expandable), virtualScroll (grid only), type-specific options (kanban/calendar/map/gallery/timeline/gantt) +- [x] Appearance section: color (grid/calendar/timeline/gantt), rowHeight (icon group, grid only), wrapHeaders (grid only), showDescription, striped/bordered (grid only), resizable (grid only), conditionalFormatting (expandable, grid/kanban), emptyState (title/message/icon) +- [x] User Actions section: inlineEdit (grid only), addDeleteRecordsInline (grid only), rowActions/bulkActions (expandable, grid/kanban) - [x] Sharing section: sharingEnabled, sharingVisibility (visibleWhen: sharing.enabled) - [x] Accessibility section: ariaLabel, ariaDescribedBy, ariaLive - [x] `ExpandableWidget` component for hook-safe expandable sub-sections within custom render functions @@ -435,14 +435,20 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind **Phase 6 — Config Panel Cleanup (Invalid Items Fix):** - [x] Remove `densityMode` field from appearance section (redundant with `rowHeight` which provides finer 5-value granularity) -- [x] Add `visibleWhen` to toolbar toggles: `showGroup` (grid/kanban only), `showColor` (grid only), `showDensity` (grid only) -- [x] Add `visibleWhen` to data fields: `prefixField` (grid only), `_groupBy` (hidden for kanban — uses dedicated type-specific `kanban.groupByField`), `searchableFields`/`filterableFields`/`quickFilters`/`virtualScroll` (grid only) -- [x] Add `visibleWhen` to appearance fields: `collapseAllByDefault` (only when `groupBy` is set), `fieldTextColor`/`showDescription`/`rowHeight`/`conditionalFormatting` (grid only) +- [x] Remove `prefixField` from data section (not consumed by any runtime renderer) +- [x] Remove `collapseAllByDefault` from appearance section (not consumed by any runtime renderer) +- [x] Remove `fieldTextColor` from appearance section (not consumed by any runtime renderer) +- [x] Remove `clickIntoRecordDetails` from userActions section (controlled implicitly via navigation mode, not directly consumed) +- [x] Add view-type-aware `visibleWhen` to toolbar toggles: `showGroup` (grid/kanban/gallery), `showColor` (grid/calendar/timeline/gantt), `showDensity` (grid only), `showRecordCount` (grid only), `allowPrinting` (grid only) +- [x] Add view-type-aware `visibleWhen` to data fields: `_groupBy` (grid/gallery — kanban uses dedicated type-specific option), `virtualScroll` (grid only) +- [x] Add view-type-aware `visibleWhen` to appearance fields: `striped`/`bordered`/`wrapHeaders`/`resizable`/`rowHeight` (grid only, changed from disabledWhen to visibleWhen), `color` (grid/calendar/timeline/gantt), `conditionalFormatting` (grid/kanban) +- [x] Add view-type-aware `visibleWhen` to userActions fields: `inlineEdit`/`addDeleteRecordsInline` (grid only), `rowActions`/`bulkActions` (grid/kanban) +- [x] Correct `searchableFields`/`filterableFields`/`quickFilters`/`showDescription` to universal (all view types) — data fetch/toolbar features not view-specific - [x] Extend `buildSwitchField` and `buildFieldMultiSelect` helpers to accept `visibleWhen` parameter -- [x] 15 new visibleWhen predicate tests covering all new conditional visibility rules -- [x] 1 new test verifying `densityMode` removal -- [x] Updated ViewConfigPanel interaction tests to account for new visibility conditions -- [x] All 244 schema + panel tests pass, 10 config-sync integration tests pass +- [x] Define semantic predicates: `supportsGrouping`, `supportsColorField`, `supportsConditionalFormatting`, `supportsRowActions`, `supportsGenericGroupBy` +- [x] 103 schema tests pass (updated field key lists, visibleWhen predicates for all view types, removed field verification) +- [x] 136 ViewConfigPanel interaction tests pass (removed tests for deleted fields) +- [x] 10 config-sync integration tests pass **Code Reduction:** ~1655 lines imperative → ~170 lines declarative wrapper + ~1100 lines schema factory + ~180 lines shared utils = **>50% net reduction in component code** with significantly improved maintainability From f43a480dec687967118fd17ba3f551549fdd1007 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:52:11 +0000 Subject: [PATCH 6/6] fix: clarify clickIntoRecordDetails removal comment Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/console/src/utils/view-config-schema.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/console/src/utils/view-config-schema.tsx b/apps/console/src/utils/view-config-schema.tsx index 3e62deac4..de4b38b35 100644 --- a/apps/console/src/utils/view-config-schema.tsx +++ b/apps/console/src/utils/view-config-schema.tsx @@ -1206,7 +1206,7 @@ function buildUserActionsSection( // spec: NamedListView.inlineEdit (grid-only: only ObjectGrid supports inline editing) buildSwitchField('inlineEdit', t('console.objectView.inlineEdit'), 'toggle-inlineEdit', true, false, undefined, isGridView), - // NOTE: clickIntoRecordDetails removed — controlled implicitly via navigation mode, not directly consumed + // NOTE: clickIntoRecordDetails removed — behavior is handled by navigation mode config, not consumed as a standalone field by runtime // spec: NamedListView.addDeleteRecordsInline (grid-only: inline add/delete is a grid feature) buildSwitchField('addDeleteRecordsInline', t('console.objectView.addDeleteRecordsInline'), 'toggle-addDeleteRecordsInline', true, false, undefined, isGridView),