From bc45bcd7ef4773bd7607d22173cb608803b1e4d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 09:39:03 +0000 Subject: [PATCH 1/5] Initial plan From 246c160cb7536fd367c18bbe6ed8f1133494d503 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 10:04:37 +0000 Subject: [PATCH 2/5] fix: expand defaultCollapsed sections before accessing their elements in tests Update ViewConfigPanel tests to account for toolbar, navigation, records, and appearance sections now being defaultCollapsed: true. Add fireEvent.click() calls to expand these sections before accessing child elements. Also update the 'collapses and expands Appearance section' test to reflect the new default collapsed state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/__tests__/ViewConfigPanel.test.tsx | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/apps/console/src/__tests__/ViewConfigPanel.test.tsx b/apps/console/src/__tests__/ViewConfigPanel.test.tsx index b3a92e1d4..9a2df2ed0 100644 --- a/apps/console/src/__tests__/ViewConfigPanel.test.tsx +++ b/apps/console/src/__tests__/ViewConfigPanel.test.tsx @@ -520,6 +520,9 @@ describe('ViewConfigPanel', () => { // Initially no footer (not dirty) expect(screen.queryByTestId('view-config-footer')).not.toBeInTheDocument(); + // Expand toolbar section (defaultCollapsed) + fireEvent.click(screen.getByTestId('section-toolbar')); + // Toggle showSearch switch (default is on → turn off) const searchSwitch = screen.getByTestId('toggle-showSearch'); fireEvent.click(searchSwitch); @@ -541,6 +544,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-toolbar')); const filterSwitch = screen.getByTestId('toggle-showFilters'); fireEvent.click(filterSwitch); expect(onViewUpdate).toHaveBeenCalledWith('showFilters', false); @@ -558,6 +562,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-toolbar')); const sortSwitch = screen.getByTestId('toggle-showSort'); fireEvent.click(sortSwitch); expect(onViewUpdate).toHaveBeenCalledWith('showSort', false); @@ -593,6 +598,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-records')); const formSwitch = screen.getByTestId('toggle-addRecord-enabled'); fireEvent.click(formSwitch); expect(onViewUpdate).toHaveBeenCalledWith('addRecordViaForm', true); @@ -815,6 +821,8 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-toolbar')); + fireEvent.click(screen.getByTestId('section-records')); expect(screen.getByTestId('toggle-showSearch')).toHaveAttribute('aria-checked', 'false'); expect(screen.getByTestId('toggle-showFilters')).toHaveAttribute('aria-checked', 'true'); expect(screen.getByTestId('toggle-showSort')).toHaveAttribute('aria-checked', 'false'); @@ -838,6 +846,7 @@ describe('ViewConfigPanel', () => { ); // Toggle showSearch — panel becomes dirty + fireEvent.click(screen.getByTestId('section-toolbar')); fireEvent.click(screen.getByTestId('toggle-showSearch')); expect(screen.getByTestId('view-config-footer')).toBeInTheDocument(); @@ -868,6 +877,7 @@ describe('ViewConfigPanel', () => { ); // Make the panel dirty + fireEvent.click(screen.getByTestId('section-toolbar')); fireEvent.click(screen.getByTestId('toggle-showSearch')); expect(screen.getByTestId('view-config-footer')).toBeInTheDocument(); @@ -898,6 +908,7 @@ describe('ViewConfigPanel', () => { ); // Toggle multiple switches + fireEvent.click(screen.getByTestId('section-toolbar')); fireEvent.click(screen.getByTestId('toggle-showSearch')); fireEvent.click(screen.getByTestId('toggle-showFilters')); @@ -1274,13 +1285,19 @@ describe('ViewConfigPanel', () => { /> ); - // Appearance section is expanded by default + // Appearance section starts collapsed by default + expect(screen.queryByTestId('toggle-showDescription')).not.toBeInTheDocument(); + + // Click section header to expand + fireEvent.click(screen.getByTestId('section-appearance')); + + // Toggle should now be visible expect(screen.getByTestId('toggle-showDescription')).toBeInTheDocument(); - // Click section header to collapse + // Click again to collapse fireEvent.click(screen.getByTestId('section-appearance')); - // Toggle should be hidden + // Toggle should be hidden again expect(screen.queryByTestId('toggle-showDescription')).not.toBeInTheDocument(); }); @@ -1296,6 +1313,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); expect(screen.getByTestId('appearance-color')).toBeInTheDocument(); expect(screen.getByTestId('appearance-rowHeight')).toBeInTheDocument(); expect(screen.getByTestId('toggle-wrapHeaders')).toBeInTheDocument(); @@ -1313,6 +1331,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); const mediumBtn = screen.getByTestId('row-height-medium'); fireEvent.click(mediumBtn); expect(onViewUpdate).toHaveBeenCalledWith('rowHeight', 'medium'); @@ -1330,6 +1349,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); fireEvent.click(screen.getByTestId('toggle-wrapHeaders')); expect(onViewUpdate).toHaveBeenCalledWith('wrapHeaders', true); }); @@ -1350,6 +1370,7 @@ describe('ViewConfigPanel', () => { fireEvent.click(screen.getByTestId('section-userActions')); expect(screen.getByTestId('toggle-inlineEdit')).toBeInTheDocument(); expect(screen.getByTestId('toggle-addDeleteRecordsInline')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('section-navigation')); expect(screen.getByTestId('select-navigation-mode')).toBeInTheDocument(); }); @@ -1688,7 +1709,12 @@ describe('ViewConfigPanel', () => { const panel = screen.getByTestId('view-config-panel'); expect(panel).toBeInTheDocument(); - // These toggles should be rendered in the page section (always visible, not behind a collapsible) + // Expand collapsed sections + fireEvent.click(screen.getByTestId('section-toolbar')); + fireEvent.click(screen.getByTestId('section-navigation')); + fireEvent.click(screen.getByTestId('section-records')); + + // These toggles should be rendered in the page section expect(screen.getByTestId('toggle-showSearch')).toBeInTheDocument(); expect(screen.getByTestId('toggle-showFilters')).toBeInTheDocument(); expect(screen.getByTestId('toggle-showSort')).toBeInTheDocument(); @@ -1741,6 +1767,11 @@ describe('ViewConfigPanel', () => { /> ); + // Expand collapsed sections + fireEvent.click(screen.getByTestId('section-toolbar')); + fireEvent.click(screen.getByTestId('section-navigation')); + fireEvent.click(screen.getByTestId('section-records')); + // Toggle showSearch off fireEvent.click(screen.getByTestId('toggle-showSearch')); expect(onViewUpdate).toHaveBeenCalledWith('showSearch', false); @@ -1779,6 +1810,8 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-toolbar')); + // Toggle showHideFields off fireEvent.click(screen.getByTestId('toggle-showHideFields')); expect(onViewUpdate).toHaveBeenCalledWith('showHideFields', false); @@ -1853,6 +1886,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-navigation')); const navSelect = screen.getByTestId('select-navigation-mode'); expect(navSelect).toBeInTheDocument(); expect(navSelect).toHaveValue('page'); @@ -1870,6 +1904,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-navigation')); expect(screen.getByTestId('input-navigation-width')).toBeInTheDocument(); }); @@ -1883,6 +1918,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-navigation')); expect(screen.getByTestId('toggle-navigation-openNewTab')).toBeInTheDocument(); }); @@ -1898,6 +1934,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-navigation')); fireEvent.change(screen.getByTestId('select-navigation-mode'), { target: { value: 'none' } }); expect(onViewUpdate).toHaveBeenCalledWith('navigation', expect.objectContaining({ mode: 'none' })); expect(onViewUpdate).toHaveBeenCalledWith('clickIntoRecordDetails', false); @@ -1913,6 +1950,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-records')); expect(screen.getByTestId('select-selection-type')).toBeInTheDocument(); }); @@ -1928,6 +1966,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-records')); fireEvent.change(screen.getByTestId('select-selection-type'), { target: { value: 'single' } }); expect(onViewUpdate).toHaveBeenCalledWith('selection', { type: 'single' }); }); @@ -2090,6 +2129,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); expect(screen.getByTestId('toggle-resizable')).toBeInTheDocument(); }); @@ -2105,6 +2145,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); fireEvent.click(screen.getByTestId('toggle-resizable')); expect(onViewUpdate).toHaveBeenCalledWith('resizable', true); }); @@ -2121,6 +2162,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); fireEvent.click(screen.getByText('console.objectView.conditionalFormatting')); expect(screen.getByTestId('conditional-formatting-editor')).toBeInTheDocument(); expect(screen.getByTestId('add-conditional-rule')).toBeInTheDocument(); @@ -2138,6 +2180,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); fireEvent.click(screen.getByText('console.objectView.conditionalFormatting')); fireEvent.click(screen.getByTestId('add-conditional-rule')); expect(onViewUpdate).toHaveBeenCalledWith('conditionalFormatting', expect.arrayContaining([ @@ -2189,6 +2232,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); expect(screen.getByTestId('input-emptyState-title')).toBeInTheDocument(); expect(screen.getByTestId('input-emptyState-message')).toBeInTheDocument(); expect(screen.getByTestId('input-emptyState-icon')).toBeInTheDocument(); @@ -2266,6 +2310,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-records')); expect(screen.getByTestId('select-addRecord-position')).toBeInTheDocument(); expect(screen.getByTestId('select-addRecord-mode')).toBeInTheDocument(); expect(screen.getByTestId('input-addRecord-formView')).toBeInTheDocument(); @@ -2313,6 +2358,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); expect(screen.getByTestId('row-height-compact')).toBeInTheDocument(); expect(screen.getByTestId('row-height-short')).toBeInTheDocument(); expect(screen.getByTestId('row-height-medium')).toBeInTheDocument(); @@ -2342,6 +2388,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); fireEvent.change(screen.getByTestId('input-emptyState-title'), { target: { value: 'No data' } }); expect(onViewUpdate).toHaveBeenCalledWith('emptyState', expect.objectContaining({ title: 'No data' })); }); @@ -2358,6 +2405,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); fireEvent.change(screen.getByTestId('input-emptyState-message'), { target: { value: 'Try adding records' } }); expect(onViewUpdate).toHaveBeenCalledWith('emptyState', expect.objectContaining({ message: 'Try adding records' })); }); @@ -2374,6 +2422,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); fireEvent.change(screen.getByTestId('input-emptyState-icon'), { target: { value: 'inbox' } }); expect(onViewUpdate).toHaveBeenCalledWith('emptyState', expect.objectContaining({ icon: 'inbox' })); }); @@ -2529,6 +2578,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); fireEvent.click(screen.getByTestId('toggle-showDescription')); expect(onViewUpdate).toHaveBeenCalledWith('showDescription', false); }); @@ -2623,6 +2673,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); const heights = ['compact', 'short', 'medium', 'tall', 'extra_tall']; heights.forEach((h) => { fireEvent.click(screen.getByTestId(`row-height-${h}`)); @@ -2701,6 +2752,7 @@ describe('ViewConfigPanel', () => { /> ); + fireEvent.click(screen.getByTestId('section-appearance')); fireEvent.change(screen.getByTestId('input-emptyState-title'), { target: { value: '' } }); expect(onViewUpdate).toHaveBeenCalledWith('emptyState', expect.objectContaining({ title: '', From 5ffc82f27436e39113caefc4a0c4934858a90d7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 10:28:43 +0000 Subject: [PATCH 3/5] feat: upgrade config panel UI with progressive disclosure, summary control, section icons, subsections, and improved spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'summary' ControlType with summaryText and onSummaryClick support - Add icon and subsections support to ConfigSection - Update SectionHeader to render icons alongside titles - Add summary field renderer with gear button in ConfigFieldRenderer - Increase section spacing (space-y-0.5 → space-y-1, separator my-1 → my-3) - Add subsection rendering in ConfigPanelRenderer - Set defaultCollapsed: true for toolbar, navigation, records, appearance sections - Update ViewConfigPanel and ObjectView tests to expand collapsed sections - Add tests for summary control, section icons, subsections, spacing Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../console/src/__tests__/ObjectView.test.tsx | 6 + apps/console/src/utils/view-config-schema.tsx | 4 + .../__tests__/config-panel-renderer.test.tsx | 154 ++++++++++++++++++ .../src/__tests__/config-primitives.test.tsx | 12 ++ .../src/custom/config-field-renderer.tsx | 19 +++ .../src/custom/config-panel-renderer.tsx | 36 +++- .../components/src/custom/section-header.tsx | 14 +- packages/components/src/types/config-panel.ts | 9 + 8 files changed, 249 insertions(+), 5 deletions(-) diff --git a/apps/console/src/__tests__/ObjectView.test.tsx b/apps/console/src/__tests__/ObjectView.test.tsx index 29d75455c..ccd5196df 100644 --- a/apps/console/src/__tests__/ObjectView.test.tsx +++ b/apps/console/src/__tests__/ObjectView.test.tsx @@ -475,6 +475,7 @@ describe('ObjectView Component', () => { expect(screen.getByTestId('view-config-panel')).toBeInTheDocument(); // Toggle showSearch off — our mock Switch fires onCheckedChange with opposite of aria-checked + fireEvent.click(screen.getByTestId('section-header-toolbar')); // Expand toolbar (defaultCollapsed) const searchSwitch = screen.getByTestId('toggle-showSearch'); fireEvent.click(searchSwitch); @@ -500,6 +501,7 @@ describe('ObjectView Component', () => { fireEvent.click(screen.getByText('console.objectView.editView')); // Toggle showSort off + fireEvent.click(screen.getByTestId('section-header-toolbar')); // Expand toolbar (defaultCollapsed) const sortSwitch = screen.getByTestId('toggle-showSort'); fireEvent.click(sortSwitch); @@ -643,6 +645,7 @@ describe('ObjectView Component', () => { fireEvent.click(screen.getByText('console.objectView.editView')); // Toggle showSearch off + fireEvent.click(screen.getByTestId('section-header-toolbar')); // Expand toolbar (defaultCollapsed) const searchSwitch = screen.getByTestId('toggle-showSearch'); fireEvent.click(searchSwitch); @@ -699,6 +702,7 @@ describe('ObjectView Component', () => { fireEvent.click(screen.getByText('console.objectView.editView')); // Change selection mode to 'single' + fireEvent.click(screen.getByTestId('section-header-records')); // Expand records (defaultCollapsed) const selectionSelect = screen.getByTestId('select-selection-type'); fireEvent.change(selectionSelect, { target: { value: 'single' } }); @@ -722,6 +726,7 @@ describe('ObjectView Component', () => { fireEvent.click(screen.getByText('console.objectView.editView')); // Toggle addRecord on + fireEvent.click(screen.getByTestId('section-header-records')); // Expand records (defaultCollapsed) const addRecordSwitch = screen.getByTestId('toggle-addRecord-enabled'); fireEvent.click(addRecordSwitch); @@ -746,6 +751,7 @@ describe('ObjectView Component', () => { fireEvent.click(screen.getByText('console.objectView.editView')); // Change navigation mode to 'modal' + fireEvent.click(screen.getByTestId('section-header-navigation')); // Expand navigation (defaultCollapsed) const navSelect = screen.getByTestId('select-navigation-mode'); fireEvent.change(navSelect, { target: { value: 'modal' } }); diff --git a/apps/console/src/utils/view-config-schema.tsx b/apps/console/src/utils/view-config-schema.tsx index 08d252d06..e5cfadea9 100644 --- a/apps/console/src/utils/view-config-schema.tsx +++ b/apps/console/src/utils/view-config-schema.tsx @@ -345,6 +345,7 @@ function buildToolbarSection( title: t('console.objectView.toolbar'), hint: t('console.objectView.toolbarHint'), collapsible: true, + defaultCollapsed: true, fields: [ // Toolbar toggles — ordered per spec: showSearch, showSort, showFilters, showHideFields, showGroup, showColor, showDensity buildSwitchField('showSearch', t('console.objectView.enableSearch'), 'toggle-showSearch', true), // spec: NamedListView.showSearch @@ -374,6 +375,7 @@ function buildNavigationSection( title: t('console.objectView.navigationSection'), hint: t('console.objectView.navigationHint'), collapsible: true, + defaultCollapsed: true, fields: [ // spec: NamedListView.navigation — navigation mode/width/openNewTab { @@ -470,6 +472,7 @@ function buildRecordsSection( title: t('console.objectView.records'), hint: t('console.objectView.recordsHint'), collapsible: true, + defaultCollapsed: true, fields: [ // spec: NamedListView.selection — row selection mode { @@ -1224,6 +1227,7 @@ function buildAppearanceSection( key: 'appearance', title: t('console.objectView.appearance'), collapsible: true, + defaultCollapsed: true, fields: [ // spec: NamedListView.striped (grid-only: row striping is a grid concept) buildSwitchField('striped', t('console.objectView.striped'), 'toggle-striped', false, true, diff --git a/packages/components/src/__tests__/config-panel-renderer.test.tsx b/packages/components/src/__tests__/config-panel-renderer.test.tsx index 5558e51de..8056ff47f 100644 --- a/packages/components/src/__tests__/config-panel-renderer.test.tsx +++ b/packages/components/src/__tests__/config-panel-renderer.test.tsx @@ -423,4 +423,158 @@ describe('ConfigPanelRenderer', () => { expect((input as HTMLInputElement).disabled).toBe(false); }); }); + + describe('section icons', () => { + it('should render section icon when provided', () => { + const React = require('react'); + const schemaWithIcon: ConfigPanelSchema = { + breadcrumb: ['Test'], + sections: [ + { + key: 'sec', + title: 'Section With Icon', + icon: React.createElement('span', { 'data-testid': 'section-icon' }, '⚙'), + fields: [{ key: 'x', label: 'X', type: 'input' }], + }, + ], + }; + render(); + expect(screen.getByTestId('section-icon')).toBeDefined(); + expect(screen.getByText('Section With Icon')).toBeDefined(); + }); + }); + + describe('subsections', () => { + it('should render subsections within a section', () => { + const schemaWithSub: ConfigPanelSchema = { + breadcrumb: ['Test'], + sections: [ + { + key: 'parent', + title: 'Parent', + fields: [{ key: 'a', label: 'Field A', type: 'input' }], + subsections: [ + { + key: 'child', + title: 'Child Section', + fields: [{ key: 'b', label: 'Field B', type: 'input' }], + }, + ], + }, + ], + }; + render(); + expect(screen.getByText('Parent')).toBeDefined(); + expect(screen.getByText('Child Section')).toBeDefined(); + expect(screen.getByText('Field A')).toBeDefined(); + expect(screen.getByText('Field B')).toBeDefined(); + expect(screen.getByTestId('config-subsection-child')).toBeDefined(); + }); + + it('should support collapsible subsections', () => { + const schemaWithCollapsibleSub: ConfigPanelSchema = { + breadcrumb: ['Test'], + sections: [ + { + key: 'parent', + title: 'Parent', + fields: [{ key: 'a', label: 'Field A', type: 'input' }], + subsections: [ + { + key: 'child', + title: 'Child', + collapsible: true, + defaultCollapsed: true, + fields: [{ key: 'b', label: 'Field B', type: 'input' }], + }, + ], + }, + ], + }; + render(); + // Child is defaultCollapsed, so Field B should not be visible + expect(screen.queryByText('Field B')).toBeNull(); + // Click to expand + fireEvent.click(screen.getByTestId('section-header-child')); + expect(screen.getByText('Field B')).toBeDefined(); + }); + }); + + describe('summary control type', () => { + it('should render summary field with text and gear icon', () => { + const onSummaryClick = vi.fn(); + const schemaWithSummary: ConfigPanelSchema = { + breadcrumb: ['Test'], + sections: [ + { + key: 'sec', + title: 'Section', + fields: [ + { + key: 'viz', + label: 'Visualizations', + type: 'summary', + summaryText: 'List, Gallery, Kanban', + onSummaryClick, + }, + ], + }, + ], + }; + render(); + expect(screen.getByText('Visualizations')).toBeDefined(); + expect(screen.getByTestId('config-field-viz-text')).toBeDefined(); + expect(screen.getByText('List, Gallery, Kanban')).toBeDefined(); + expect(screen.getByTestId('config-field-viz-gear')).toBeDefined(); + }); + + it('should call onSummaryClick when summary row is clicked', () => { + const onSummaryClick = vi.fn(); + const schemaWithSummary: ConfigPanelSchema = { + breadcrumb: ['Test'], + sections: [ + { + key: 'sec', + title: 'Section', + fields: [ + { + key: 'viz', + label: 'Viz', + type: 'summary', + summaryText: 'Items', + onSummaryClick, + }, + ], + }, + ], + }; + render(); + // The ConfigRow wraps in a button when onClick is provided + const row = screen.getByText('Viz').closest('button'); + if (row) fireEvent.click(row); + expect(onSummaryClick).toHaveBeenCalledTimes(1); + }); + }); + + describe('increased section spacing', () => { + it('should use space-y-1 for field spacing within sections', () => { + render(); + const section = screen.getByTestId('config-section-basic'); + const fieldContainer = section.querySelector('.space-y-1'); + expect(fieldContainer).not.toBeNull(); + }); + + it('should use my-3 separator between sections', () => { + render( + , + ); + const panel = screen.getByTestId('config-panel'); + const separators = panel.querySelectorAll('.my-3'); + expect(separators.length).toBeGreaterThan(0); + }); + }); }); diff --git a/packages/components/src/__tests__/config-primitives.test.tsx b/packages/components/src/__tests__/config-primitives.test.tsx index 91740e0e1..6813867ff 100644 --- a/packages/components/src/__tests__/config-primitives.test.tsx +++ b/packages/components/src/__tests__/config-primitives.test.tsx @@ -91,4 +91,16 @@ describe('SectionHeader', () => { const element = screen.getByTestId('section'); expect(element.className).toContain('custom-class'); }); + + it('should render icon when provided', () => { + render(📊} testId="section" />); + expect(screen.getByTestId('icon')).toBeDefined(); + expect(screen.getByText('Data')).toBeDefined(); + }); + + it('should render icon alongside collapsible title', () => { + render(📊} collapsible testId="section" />); + expect(screen.getByTestId('icon')).toBeDefined(); + expect(screen.getByTestId('section').tagName).toBe('BUTTON'); + }); }); diff --git a/packages/components/src/custom/config-field-renderer.tsx b/packages/components/src/custom/config-field-renderer.tsx index c737b4e44..af35784f7 100644 --- a/packages/components/src/custom/config-field-renderer.tsx +++ b/packages/components/src/custom/config-field-renderer.tsx @@ -7,6 +7,7 @@ */ import * as React from 'react'; +import { Settings } from 'lucide-react'; import { Input } from '../ui/input'; import { Switch } from '../ui/switch'; import { Checkbox } from '../ui/checkbox'; @@ -237,6 +238,24 @@ export function ConfigFieldRenderer({ } break; + case 'summary': + content = ( + +
+ + {field.summaryText ?? effectiveValue ?? ''} + + {field.onSummaryClick && ( + + )} +
+
+ ); + break; + default: break; } diff --git a/packages/components/src/custom/config-panel-renderer.tsx b/packages/components/src/custom/config-panel-renderer.tsx index 5d390904d..50feaeb5d 100644 --- a/packages/components/src/custom/config-panel-renderer.tsx +++ b/packages/components/src/custom/config-panel-renderer.tsx @@ -219,9 +219,10 @@ export function ConfigPanelRenderer({ return (
- {sectionIdx > 0 && } + {sectionIdx > 0 && } toggleCollapse(section.key, section.defaultCollapsed)} @@ -233,7 +234,7 @@ export function ConfigPanelRenderer({

)} {!sectionCollapsed && ( -
+
{section.fields.map((field) => ( ))} + {section.subsections?.map((sub) => { + if (sub.visibleWhen && !sub.visibleWhen(draft)) return null; + const subCollapsed = isCollapsed(sub.key, sub.defaultCollapsed); + return ( +
+ toggleCollapse(sub.key, sub.defaultCollapsed)} + testId={`section-header-${sub.key}`} + className="pt-2 pb-1" + /> + {!subCollapsed && ( +
+ {sub.fields.map((field) => ( + onFieldChange(field.key, v)} + draft={draft} + objectDef={objectDef} + /> + ))} +
+ )} +
+ ); + })}
)}
diff --git a/packages/components/src/custom/section-header.tsx b/packages/components/src/custom/section-header.tsx index 842bcc0fc..774bf24a7 100644 --- a/packages/components/src/custom/section-header.tsx +++ b/packages/components/src/custom/section-header.tsx @@ -13,6 +13,8 @@ import { cn } from "../lib/utils" export interface SectionHeaderProps { /** Section heading text */ title: string + /** Icon rendered before the title */ + icon?: React.ReactNode /** Enable collapse/expand toggle */ collapsible?: boolean /** Current collapsed state */ @@ -31,7 +33,13 @@ export interface SectionHeaderProps { * Renders as a `
diff --git a/packages/components/src/custom/config-panel-renderer.tsx b/packages/components/src/custom/config-panel-renderer.tsx index 50feaeb5d..2f72f767f 100644 --- a/packages/components/src/custom/config-panel-renderer.tsx +++ b/packages/components/src/custom/config-panel-renderer.tsx @@ -249,7 +249,7 @@ export function ConfigPanelRenderer({ if (sub.visibleWhen && !sub.visibleWhen(draft)) return null; const subCollapsed = isCollapsed(sub.key, sub.defaultCollapsed); return ( -
+
- {icon && {icon}} + {icon && } {title} ) From 33d65a52a58124af5bc7d0ae946b6da03c56c7dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 10:54:53 +0000 Subject: [PATCH 5/5] feat: implement ConfigRow layout optimization (#4) and toolbar summary chip (#7) - ConfigRow: add label maxWidth (45%), text truncation with title tooltip for both label and value - Toolbar section: add summary chip showing "X of Y enabled" at top of expanded section - Add toolbarEnabledCount i18n key to all 10 locales - Update view-config-schema tests for new _toolbarSummary field Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../src/__tests__/view-config-schema.test.tsx | 2 +- apps/console/src/utils/view-config-schema.tsx | 20 +++++++++++++++++++ packages/components/src/custom/config-row.tsx | 4 ++-- packages/i18n/src/locales/ar.ts | 1 + packages/i18n/src/locales/de.ts | 1 + packages/i18n/src/locales/en.ts | 1 + packages/i18n/src/locales/es.ts | 1 + packages/i18n/src/locales/fr.ts | 1 + packages/i18n/src/locales/ja.ts | 1 + packages/i18n/src/locales/ko.ts | 1 + packages/i18n/src/locales/pt.ts | 1 + packages/i18n/src/locales/ru.ts | 1 + packages/i18n/src/locales/zh.ts | 1 + 13 files changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/console/src/__tests__/view-config-schema.test.tsx b/apps/console/src/__tests__/view-config-schema.test.tsx index 184d2109c..34f358f6d 100644 --- a/apps/console/src/__tests__/view-config-schema.test.tsx +++ b/apps/console/src/__tests__/view-config-schema.test.tsx @@ -394,7 +394,7 @@ describe('buildViewConfigSchema', () => { const section = schema.sections.find((s: any) => s.key === 'toolbar')!; const fieldKeys = section.fields.map((f: any) => f.key); expect(fieldKeys).toEqual([ - 'showSearch', 'showSort', 'showFilters', 'showHideFields', 'showGroup', 'showColor', 'showDensity', + '_toolbarSummary', 'showSearch', 'showSort', 'showFilters', 'showHideFields', 'showGroup', 'showColor', 'showDensity', ]); }); diff --git a/apps/console/src/utils/view-config-schema.tsx b/apps/console/src/utils/view-config-schema.tsx index e5cfadea9..93c5b94be 100644 --- a/apps/console/src/utils/view-config-schema.tsx +++ b/apps/console/src/utils/view-config-schema.tsx @@ -340,6 +340,9 @@ function buildGeneralSection( function buildToolbarSection( t: ViewSchemaFactoryOptions['t'], ): ConfigPanelSchema['sections'][number] { + /** Keys of all toolbar toggles (used for enabled-count summary) */ + const toolbarKeys = ['showSearch', 'showSort', 'showFilters', 'showHideFields', 'showGroup', 'showColor', 'showDensity']; + return { key: 'toolbar', title: t('console.objectView.toolbar'), @@ -347,6 +350,23 @@ function buildToolbarSection( collapsible: true, defaultCollapsed: true, fields: [ + // Summary chip — "X of Y enabled" at-a-glance overview at the top of expanded section + { + key: '_toolbarSummary', + label: t('console.objectView.toolbar'), + type: 'custom' as const, + render: (_value: any, _onChange: any, draft: Record) => { + const total = toolbarKeys.length; + const count = toolbarKeys.filter(k => draft[k] !== false).length; + return ( +
+ + {t('console.objectView.toolbarEnabledCount', { count, total })} + +
+ ); + }, + }, // 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 diff --git a/packages/components/src/custom/config-row.tsx b/packages/components/src/custom/config-row.tsx index 1592d0852..c36f5134e 100644 --- a/packages/components/src/custom/config-row.tsx +++ b/packages/components/src/custom/config-row.tsx @@ -39,9 +39,9 @@ function ConfigRow({ label, value, onClick, children, className }: ConfigRowProp onClick={onClick} type={onClick ? 'button' : undefined} > - {label} + {label} {children || ( - {value} + {value} )} ) diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index b09945784..7b44db63e 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -368,6 +368,7 @@ const ar = { generalHint: 'عنوان العرض والوصف والنوع', toolbar: 'شريط الأدوات', toolbarHint: 'البحث والتصفية والفرز والتجميع ومفاتيح الكثافة', + toolbarEnabledCount: '{{count}} of {{total}} enabled', navigationSection: 'التنقل', navigationHint: 'سلوك النقر على الصف وإعدادات العرض التفصيلي', records: 'السجلات', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index ebb6c8b20..0ae67cfbc 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -372,6 +372,7 @@ const de = { generalHint: 'Ansichtstitel, Beschreibung und Typ', toolbar: 'Symbolleiste', toolbarHint: 'Suche, Filter, Sortierung, Gruppierung und Dichteumschaltung', + toolbarEnabledCount: '{{count}} of {{total}} enabled', navigationSection: 'Navigation', navigationHint: 'Zeilenklickverhalten und Detailansichtseinstellungen', records: 'Datensätze', diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 3aea702d4..86ee002fe 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -372,6 +372,7 @@ const en = { generalHint: 'View title, description, and type', toolbar: 'Toolbar', toolbarHint: 'Search, filter, sort, group, and density toggles', + toolbarEnabledCount: '{{count}} of {{total}} enabled', navigationSection: 'Navigation', navigationHint: 'Row click behavior and detail view settings', records: 'Records', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 6201fdb56..3dc49365b 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -367,6 +367,7 @@ const es = { generalHint: 'Título, descripción y tipo de la vista', toolbar: 'Barra de herramientas', toolbarHint: 'Búsqueda, filtro, orden, agrupación y alternancia de densidad', + toolbarEnabledCount: '{{count}} of {{total}} enabled', navigationSection: 'Navegación', navigationHint: 'Comportamiento al hacer clic en la fila y configuración de la vista detallada', records: 'Registros', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index a2995db14..5e9a08cb4 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -372,6 +372,7 @@ const fr = { generalHint: 'Titre, description et type de la vue', toolbar: 'Barre d\'outils', toolbarHint: 'Recherche, filtre, tri, regroupement et bascule de densité', + toolbarEnabledCount: '{{count}} of {{total}} enabled', navigationSection: 'Navigation', navigationHint: 'Comportement au clic sur la ligne et paramètres de la vue détaillée', records: 'Enregistrements', diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 1247aa9c9..7d7e79438 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -367,6 +367,7 @@ const ja = { generalHint: 'ビューのタイトル、説明、タイプ', toolbar: 'ツールバー', toolbarHint: '検索、フィルター、ソート、グループ、密度の切り替え', + toolbarEnabledCount: '{{count}} of {{total}} enabled', navigationSection: 'ナビゲーション', navigationHint: '行クリック時の動作と詳細ビューの設定', records: 'レコード', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index cd79e2a02..cc0f504d8 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -367,6 +367,7 @@ const ko = { generalHint: '보기 제목, 설명 및 유형', toolbar: '도구 모음', toolbarHint: '검색, 필터, 정렬, 그룹화 및 밀도 전환', + toolbarEnabledCount: '{{count}} of {{total}} enabled', navigationSection: '탐색', navigationHint: '행 클릭 동작 및 상세 보기 설정', records: '레코드', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 1a3790bfd..c00b1cbd3 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -367,6 +367,7 @@ const pt = { generalHint: 'Título, descrição e tipo da visualização', toolbar: 'Barra de ferramentas', toolbarHint: 'Pesquisa, filtro, ordenação, agrupamento e alternância de densidade', + toolbarEnabledCount: '{{count}} of {{total}} enabled', navigationSection: 'Navegação', navigationHint: 'Comportamento ao clicar na linha e configurações da visualização detalhada', records: 'Registros', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 13731ce88..67519326c 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -367,6 +367,7 @@ const ru = { generalHint: 'Название, описание и тип представления', toolbar: 'Панель инструментов', toolbarHint: 'Поиск, фильтр, сортировка, группировка и переключение плотности', + toolbarEnabledCount: '{{count}} of {{total}} enabled', navigationSection: 'Навигация', navigationHint: 'Поведение при нажатии на строку и настройки детального представления', records: 'Записи', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 0dbf94e3d..4fafcd026 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -372,6 +372,7 @@ const zh = { generalHint: '视图标题、描述和类型', toolbar: '工具栏', toolbarHint: '搜索、筛选、排序、分组和密度切换', + toolbarEnabledCount: '{{count}} of {{total}} enabled', navigationSection: '导航', navigationHint: '行点击行为和详情视图设置', records: '记录',