Skip to content

Commit fd054a9

Browse files
authored
Merge pull request #690 from objectstack-ai/copilot/optimize-view-config-panel
2 parents 40381fd + a4bec69 commit fd054a9

13 files changed

Lines changed: 1028 additions & 251 deletions

File tree

ROADMAP.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,14 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
123123
- [x] Unified create/edit mode (`mode="create"|"edit"`) — single panel entry point
124124
- [x] Unified data model (`UnifiedViewConfig`) for view configuration
125125
- [x] ViewDesigner retained as "Advanced Editor" with weaker entry point
126-
- [ ] View appearance settings (density, row color, conditional formatting)
126+
- [x] Panel header breadcrumb navigation (Page > List/Kanban/Gallery)
127+
- [x] Collapsible/expandable sections with chevron toggle
128+
- [x] Data section: Sort by (summary), Group by, Prefix field, Fields (count visible)
129+
- [x] Appearance section: Color, Field text color, Row height (icon toggle), Wrap headers, Show field descriptions, Collapse all by default
130+
- [x] User actions section: Edit records inline, Add/delete records inline, Click into record details
131+
- [x] Calendar endDateField support
132+
- [x] i18n for all 11 locales (en, zh, ja, de, fr, es, ar, ru, pt, ko)
133+
- [ ] Conditional formatting rules
127134

128135
---
129136

apps/console/src/__tests__/ViewConfigPanel.test.tsx

Lines changed: 275 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ describe('ViewConfigPanel', () => {
127127

128128
expect(screen.getByTestId('view-config-panel')).toBeInTheDocument();
129129

130-
// Check section headers
131-
expect(screen.getByText('console.objectView.page')).toBeInTheDocument();
130+
// Check section headers (page appears in both breadcrumb and section)
131+
expect(screen.getAllByText('console.objectView.page').length).toBeGreaterThanOrEqual(1);
132132
expect(screen.getByText('console.objectView.data')).toBeInTheDocument();
133133
expect(screen.getByText('console.objectView.appearance')).toBeInTheDocument();
134-
expect(screen.getByText('console.objectView.userFilters')).toBeInTheDocument();
135134
expect(screen.getByText('console.objectView.userActions')).toBeInTheDocument();
136-
expect(screen.getByText('console.objectView.advanced')).toBeInTheDocument();
135+
// Breadcrumb shows view type label
136+
expect(screen.getByTestId('panel-breadcrumb')).toBeInTheDocument();
137137
});
138138

139139
it('displays view title in editable input', () => {
@@ -186,6 +186,10 @@ describe('ViewConfigPanel', () => {
186186
/>
187187
);
188188

189+
// Expand the Fields sub-section by clicking the summary row
190+
const fieldsRow = screen.getByText('console.objectView.fields');
191+
fireEvent.click(fieldsRow);
192+
189193
// 3 fields → 3 checkboxes
190194
expect(screen.getByTestId('column-selector')).toBeInTheDocument();
191195
expect(screen.getByTestId('col-checkbox-name')).toBeInTheDocument();
@@ -278,6 +282,10 @@ describe('ViewConfigPanel', () => {
278282
/>
279283
);
280284

285+
// Expand sort and filter sub-sections
286+
fireEvent.click(screen.getByText('console.objectView.sortBy'));
287+
fireEvent.click(screen.getByText('console.objectView.filterBy'));
288+
281289
// FilterBuilder should have 0 conditions
282290
expect(screen.getByTestId('mock-filter-builder')).toHaveAttribute('data-condition-count', '0');
283291
// SortBuilder should have 0 items
@@ -457,6 +465,9 @@ describe('ViewConfigPanel', () => {
457465
/>
458466
);
459467

468+
// Expand filter sub-section
469+
fireEvent.click(screen.getByText('console.objectView.filterBy'));
470+
460471
const fb = screen.getByTestId('mock-filter-builder');
461472
expect(fb).toHaveAttribute('data-condition-count', '1');
462473
expect(fb).toHaveAttribute('data-field-count', '3');
@@ -473,6 +484,9 @@ describe('ViewConfigPanel', () => {
473484
/>
474485
);
475486

487+
// Expand sort sub-section
488+
fireEvent.click(screen.getByText('console.objectView.sortBy'));
489+
476490
const sb = screen.getByTestId('mock-sort-builder');
477491
expect(sb).toHaveAttribute('data-sort-count', '1');
478492
expect(sb).toHaveAttribute('data-field-count', '3');
@@ -491,6 +505,9 @@ describe('ViewConfigPanel', () => {
491505
/>
492506
);
493507

508+
// Expand filter sub-section
509+
fireEvent.click(screen.getByText('console.objectView.filterBy'));
510+
494511
fireEvent.click(screen.getByTestId('filter-builder-add'));
495512
expect(onViewUpdate).toHaveBeenCalledWith('filter', expect.any(Array));
496513
});
@@ -507,6 +524,9 @@ describe('ViewConfigPanel', () => {
507524
/>
508525
);
509526

527+
// Expand sort sub-section
528+
fireEvent.click(screen.getByText('console.objectView.sortBy'));
529+
510530
fireEvent.click(screen.getByTestId('sort-builder-add'));
511531
expect(onViewUpdate).toHaveBeenCalledWith('sort', expect.any(Array));
512532
});
@@ -523,6 +543,9 @@ describe('ViewConfigPanel', () => {
523543
/>
524544
);
525545

546+
// Expand the Fields sub-section
547+
fireEvent.click(screen.getByText('console.objectView.fields'));
548+
526549
// Uncheck the 'stage' column
527550
fireEvent.click(screen.getByTestId('col-checkbox-stage'));
528551
expect(onViewUpdate).toHaveBeenCalledWith('columns', ['name', 'amount']);
@@ -717,6 +740,9 @@ describe('ViewConfigPanel', () => {
717740
/>
718741
);
719742

743+
// Expand filter sub-section
744+
fireEvent.click(screen.getByText('console.objectView.filterBy'));
745+
720746
const fb = screen.getByTestId('mock-filter-builder');
721747
expect(fb).toHaveAttribute('data-condition-count', '2');
722748
expect(screen.getByTestId('filter-condition-0')).toHaveTextContent('stage equals active');
@@ -736,6 +762,9 @@ describe('ViewConfigPanel', () => {
736762
/>
737763
);
738764

765+
// Expand filter sub-section
766+
fireEvent.click(screen.getByText('console.objectView.filterBy'));
767+
739768
const fb = screen.getByTestId('mock-filter-builder');
740769
expect(fb).toHaveAttribute('data-condition-count', '2');
741770
});
@@ -759,6 +788,9 @@ describe('ViewConfigPanel', () => {
759788
/>
760789
);
761790

791+
// Expand filter sub-section
792+
fireEvent.click(screen.getByText('console.objectView.filterBy'));
793+
762794
// The mock FilterBuilder receives normalized fields via data-field-count
763795
const fb = screen.getByTestId('mock-filter-builder');
764796
expect(fb).toHaveAttribute('data-field-count', '5');
@@ -992,4 +1024,243 @@ describe('ViewConfigPanel', () => {
9921024
// Now kanban groupBy should appear
9931025
expect(screen.getByTestId('type-opt-kanban-groupByField')).toBeInTheDocument();
9941026
});
1027+
1028+
// ── Breadcrumb header tests ──
1029+
1030+
it('renders breadcrumb header with Page > ViewType', () => {
1031+
render(
1032+
<ViewConfigPanel
1033+
open={true}
1034+
onClose={vi.fn()}
1035+
activeView={mockActiveView}
1036+
objectDef={mockObjectDef}
1037+
/>
1038+
);
1039+
1040+
const breadcrumb = screen.getByTestId('panel-breadcrumb');
1041+
expect(breadcrumb).toBeInTheDocument();
1042+
expect(breadcrumb).toHaveTextContent('Grid');
1043+
});
1044+
1045+
it('breadcrumb updates when view type changes', () => {
1046+
render(
1047+
<ViewConfigPanel
1048+
open={true}
1049+
onClose={vi.fn()}
1050+
activeView={{ ...mockActiveView, type: 'kanban' }}
1051+
objectDef={mockObjectDef}
1052+
/>
1053+
);
1054+
1055+
const breadcrumb = screen.getByTestId('panel-breadcrumb');
1056+
expect(breadcrumb).toHaveTextContent('Kanban');
1057+
});
1058+
1059+
// ── Collapsible section tests ──
1060+
1061+
it('collapses and expands Data section', () => {
1062+
render(
1063+
<ViewConfigPanel
1064+
open={true}
1065+
onClose={vi.fn()}
1066+
activeView={mockActiveView}
1067+
objectDef={mockObjectDef}
1068+
/>
1069+
);
1070+
1071+
// Data section is expanded by default — source row visible
1072+
expect(screen.getByText('Opportunity')).toBeInTheDocument();
1073+
1074+
// Click section header to collapse
1075+
const sectionBtn = screen.getByTestId('section-data');
1076+
fireEvent.click(sectionBtn);
1077+
1078+
// Source row should be hidden
1079+
expect(screen.queryByText('Opportunity')).not.toBeInTheDocument();
1080+
1081+
// Click again to expand
1082+
fireEvent.click(sectionBtn);
1083+
expect(screen.getByText('Opportunity')).toBeInTheDocument();
1084+
});
1085+
1086+
it('collapses and expands Appearance section', () => {
1087+
render(
1088+
<ViewConfigPanel
1089+
open={true}
1090+
onClose={vi.fn()}
1091+
activeView={mockActiveView}
1092+
objectDef={mockObjectDef}
1093+
/>
1094+
);
1095+
1096+
// Appearance section is expanded by default
1097+
expect(screen.getByTestId('toggle-showDescription')).toBeInTheDocument();
1098+
1099+
// Click section header to collapse
1100+
fireEvent.click(screen.getByTestId('section-appearance'));
1101+
1102+
// Toggle should be hidden
1103+
expect(screen.queryByTestId('toggle-showDescription')).not.toBeInTheDocument();
1104+
});
1105+
1106+
// ── Appearance fields tests ──
1107+
1108+
it('renders new appearance fields: color, fieldTextColor, rowHeight, wrapHeaders, collapseAllByDefault', () => {
1109+
render(
1110+
<ViewConfigPanel
1111+
open={true}
1112+
onClose={vi.fn()}
1113+
activeView={mockActiveView}
1114+
objectDef={mockObjectDef}
1115+
/>
1116+
);
1117+
1118+
expect(screen.getByTestId('appearance-color')).toBeInTheDocument();
1119+
expect(screen.getByTestId('appearance-fieldTextColor')).toBeInTheDocument();
1120+
expect(screen.getByTestId('appearance-rowHeight')).toBeInTheDocument();
1121+
expect(screen.getByTestId('toggle-wrapHeaders')).toBeInTheDocument();
1122+
expect(screen.getByTestId('toggle-collapseAllByDefault')).toBeInTheDocument();
1123+
});
1124+
1125+
it('changes row height via icon buttons', () => {
1126+
const onViewUpdate = vi.fn();
1127+
render(
1128+
<ViewConfigPanel
1129+
open={true}
1130+
onClose={vi.fn()}
1131+
activeView={mockActiveView}
1132+
objectDef={mockObjectDef}
1133+
onViewUpdate={onViewUpdate}
1134+
/>
1135+
);
1136+
1137+
const mediumBtn = screen.getByTestId('row-height-medium');
1138+
fireEvent.click(mediumBtn);
1139+
expect(onViewUpdate).toHaveBeenCalledWith('rowHeight', 'medium');
1140+
});
1141+
1142+
it('toggles wrapHeaders via Switch', () => {
1143+
const onViewUpdate = vi.fn();
1144+
render(
1145+
<ViewConfigPanel
1146+
open={true}
1147+
onClose={vi.fn()}
1148+
activeView={mockActiveView}
1149+
objectDef={mockObjectDef}
1150+
onViewUpdate={onViewUpdate}
1151+
/>
1152+
);
1153+
1154+
fireEvent.click(screen.getByTestId('toggle-wrapHeaders'));
1155+
expect(onViewUpdate).toHaveBeenCalledWith('wrapHeaders', true);
1156+
});
1157+
1158+
// ── User actions fields tests ──
1159+
1160+
it('renders new user action fields: editRecordsInline, addDeleteRecordsInline, clickIntoRecordDetails', () => {
1161+
render(
1162+
<ViewConfigPanel
1163+
open={true}
1164+
onClose={vi.fn()}
1165+
activeView={mockActiveView}
1166+
objectDef={mockObjectDef}
1167+
/>
1168+
);
1169+
1170+
expect(screen.getByTestId('toggle-editRecordsInline')).toBeInTheDocument();
1171+
expect(screen.getByTestId('toggle-addDeleteRecordsInline')).toBeInTheDocument();
1172+
expect(screen.getByTestId('toggle-clickIntoRecordDetails')).toBeInTheDocument();
1173+
});
1174+
1175+
it('toggles editRecordsInline via Switch', () => {
1176+
const onViewUpdate = vi.fn();
1177+
render(
1178+
<ViewConfigPanel
1179+
open={true}
1180+
onClose={vi.fn()}
1181+
activeView={mockActiveView}
1182+
objectDef={mockObjectDef}
1183+
onViewUpdate={onViewUpdate}
1184+
/>
1185+
);
1186+
1187+
fireEvent.click(screen.getByTestId('toggle-editRecordsInline'));
1188+
expect(onViewUpdate).toHaveBeenCalledWith('editRecordsInline', false);
1189+
});
1190+
1191+
// ── Data section: Group by and Prefix field tests ──
1192+
1193+
it('renders Group by and Prefix field selectors in Data section', () => {
1194+
render(
1195+
<ViewConfigPanel
1196+
open={true}
1197+
onClose={vi.fn()}
1198+
activeView={mockActiveView}
1199+
objectDef={mockObjectDef}
1200+
/>
1201+
);
1202+
1203+
expect(screen.getByTestId('data-groupBy')).toBeInTheDocument();
1204+
expect(screen.getByTestId('data-prefixField')).toBeInTheDocument();
1205+
});
1206+
1207+
it('changes groupBy and propagates to kanban type option', () => {
1208+
const onViewUpdate = vi.fn();
1209+
render(
1210+
<ViewConfigPanel
1211+
open={true}
1212+
onClose={vi.fn()}
1213+
activeView={{ ...mockActiveView, type: 'kanban' }}
1214+
objectDef={mockObjectDef}
1215+
onViewUpdate={onViewUpdate}
1216+
/>
1217+
);
1218+
1219+
const groupBySelect = screen.getByTestId('data-groupBy');
1220+
fireEvent.change(groupBySelect, { target: { value: 'stage' } });
1221+
1222+
expect(onViewUpdate).toHaveBeenCalledWith('groupBy', 'stage');
1223+
expect(onViewUpdate).toHaveBeenCalledWith('kanban', expect.objectContaining({ groupByField: 'stage' }));
1224+
});
1225+
1226+
// ── Calendar endDateField test ──
1227+
1228+
it('shows calendar endDateField when view type is calendar', () => {
1229+
render(
1230+
<ViewConfigPanel
1231+
open={true}
1232+
onClose={vi.fn()}
1233+
activeView={{ ...mockActiveView, type: 'calendar' }}
1234+
objectDef={mockObjectDef}
1235+
/>
1236+
);
1237+
1238+
expect(screen.getByTestId('type-opt-calendar-startDateField')).toBeInTheDocument();
1239+
expect(screen.getByTestId('type-opt-calendar-endDateField')).toBeInTheDocument();
1240+
expect(screen.getByTestId('type-opt-calendar-titleField')).toBeInTheDocument();
1241+
});
1242+
1243+
// ── Data sub-section expand/collapse tests ──
1244+
1245+
it('expands and collapses sort/filter/fields sub-sections in Data', () => {
1246+
render(
1247+
<ViewConfigPanel
1248+
open={true}
1249+
onClose={vi.fn()}
1250+
activeView={mockActiveView}
1251+
objectDef={mockObjectDef}
1252+
/>
1253+
);
1254+
1255+
// Sort sub-section starts collapsed
1256+
expect(screen.queryByTestId('inline-sort-builder')).not.toBeInTheDocument();
1257+
1258+
// Click to expand
1259+
fireEvent.click(screen.getByText('console.objectView.sortBy'));
1260+
expect(screen.getByTestId('inline-sort-builder')).toBeInTheDocument();
1261+
1262+
// Click again to collapse
1263+
fireEvent.click(screen.getByText('console.objectView.sortBy'));
1264+
expect(screen.queryByTestId('inline-sort-builder')).not.toBeInTheDocument();
1265+
});
9951266
});

0 commit comments

Comments
 (0)