Skip to content

Commit 0dd7272

Browse files
authored
Merge pull request #1031 from objectstack-ai/copilot/fix-user-filters-panel-configuration
2 parents ec6d203 + cac2d48 commit 0dd7272

File tree

11 files changed

+504
-0
lines changed

11 files changed

+504
-0
lines changed

ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
10681068
- [x] Merged UserFilters row and tool buttons row into single toolbar line — left: field filter badges, right: tool buttons with separator
10691069
- [x] Search changed from inline input to icon button + Popover — saves toolbar space, matches Airtable pattern
10701070
- [x] UserFilters `maxVisible` prop added — overflow badges collapse into "More" dropdown with Popover
1071+
- [x] UserFilters config panel: `_userFilters` integrated into `buildDataSection` in ViewConfigPanel — element type selector (dropdown/tabs/toggle), field picker, live preview sync, i18n (en/zh), `NamedListView.userFilters` type parity with `ListViewSchema.userFilters`
10711072
- [x] FilterBuilder: multi-select support for `in`/`notIn` operators — checkbox list UI replaces text input for select/lookup/master_detail fields
10721073
- [x] FilterBuilder: lookup/master_detail field types now show dropdown selector instead of manual ID input
10731074
- [x] FilterBuilder: all field types mapped — `currency`/`percent`/`rating` → number operators, `datetime`/`time` → date operators with proper input types, `status` → select operators, `user`/`owner` → lookup operators

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ vi.mock('@object-ui/plugin-list', () => ({
3333
{props.schema?.selection?.type && <div data-testid="schema-selection-type">{props.schema.selection.type}</div>}
3434
{props.schema?.addRecord?.enabled && <div data-testid="schema-addRecord-enabled">addRecord</div>}
3535
{props.schema?.addRecordViaForm && <div data-testid="schema-addRecordViaForm">addRecordViaForm</div>}
36+
{props.schema?.userFilters && <div data-testid="schema-userFilters">{props.schema.userFilters.element}</div>}
3637
<button data-testid="list-row-click" onClick={() => props.onRowClick?.({ _id: 'rec-1', id: 'rec-1', name: 'Test Record' })}>Click Row</button>
3738
</div>
3839
);
@@ -632,6 +633,37 @@ describe('ObjectView Component', () => {
632633
});
633634
});
634635

636+
it('propagates userFilters config to ListView schema when viewDef has userFilters', async () => {
637+
mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
638+
mockUseParams.mockReturnValue({ objectName: 'opportunity' });
639+
640+
// Set up a view definition that includes userFilters
641+
const objectsWithUserFilters = mockObjects.map((obj: any) => {
642+
if (obj.name === 'opportunity') {
643+
return {
644+
...obj,
645+
listViews: [{
646+
...obj.listViews?.[0],
647+
label: 'All Opportunities',
648+
userFilters: {
649+
element: 'dropdown',
650+
fields: [{ field: 'status' }],
651+
},
652+
}],
653+
};
654+
}
655+
return obj;
656+
});
657+
658+
render(<ObjectView dataSource={mockDataSource} objects={objectsWithUserFilters} onEdit={vi.fn()} />);
659+
660+
// Verify userFilters propagated to ListView
661+
await vi.waitFor(() => {
662+
expect(screen.getByTestId('schema-userFilters')).toBeInTheDocument();
663+
expect(screen.getByTestId('schema-userFilters')).toHaveTextContent('dropdown');
664+
});
665+
});
666+
635667
it('propagates multiple config changes without requiring save', async () => {
636668
mockAuthUser = { id: 'u1', name: 'Admin', role: 'admin' };
637669
mockUseParams.mockReturnValue({ objectName: 'opportunity' });

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

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2222,6 +2222,191 @@ describe('ViewConfigPanel', () => {
22222222
]));
22232223
});
22242224

2225+
// ── User Filters editor tests ──
2226+
2227+
it('renders user filters editor when expanded', () => {
2228+
render(
2229+
<ViewConfigPanel
2230+
open={true}
2231+
onClose={vi.fn()}
2232+
activeView={mockActiveView}
2233+
objectDef={mockObjectDef}
2234+
/>
2235+
);
2236+
2237+
fireEvent.click(screen.getByText('console.objectView.userFilters'));
2238+
expect(screen.getByTestId('user-filters-editor')).toBeInTheDocument();
2239+
expect(screen.getByTestId('uf-element-dropdown')).toBeInTheDocument();
2240+
expect(screen.getByTestId('uf-element-tabs')).toBeInTheDocument();
2241+
expect(screen.getByTestId('uf-element-toggle')).toBeInTheDocument();
2242+
});
2243+
2244+
it('updates draft element type when clicking element buttons', () => {
2245+
const onViewUpdate = vi.fn();
2246+
render(
2247+
<ViewConfigPanel
2248+
open={true}
2249+
onClose={vi.fn()}
2250+
activeView={mockActiveView}
2251+
objectDef={mockObjectDef}
2252+
onViewUpdate={onViewUpdate}
2253+
/>
2254+
);
2255+
2256+
fireEvent.click(screen.getByText('console.objectView.userFilters'));
2257+
fireEvent.click(screen.getByTestId('uf-element-tabs'));
2258+
expect(onViewUpdate).toHaveBeenCalledWith('userFilters', expect.objectContaining({ element: 'tabs' }));
2259+
});
2260+
2261+
it('adds a filter field via the field selector', () => {
2262+
const onViewUpdate = vi.fn();
2263+
render(
2264+
<ViewConfigPanel
2265+
open={true}
2266+
onClose={vi.fn()}
2267+
activeView={mockActiveView}
2268+
objectDef={mockObjectDef}
2269+
onViewUpdate={onViewUpdate}
2270+
/>
2271+
);
2272+
2273+
fireEvent.click(screen.getByText('console.objectView.userFilters'));
2274+
const addField = screen.getByTestId('uf-add-field');
2275+
fireEvent.change(addField, { target: { value: 'stage' } });
2276+
expect(onViewUpdate).toHaveBeenCalledWith('userFilters', expect.objectContaining({
2277+
fields: expect.arrayContaining([{ field: 'stage' }]),
2278+
}));
2279+
});
2280+
2281+
it('removes a filter field when clicking remove button', () => {
2282+
const onViewUpdate = vi.fn();
2283+
render(
2284+
<ViewConfigPanel
2285+
open={true}
2286+
onClose={vi.fn()}
2287+
activeView={{
2288+
...mockActiveView,
2289+
userFilters: {
2290+
element: 'dropdown',
2291+
fields: [{ field: 'name' }, { field: 'stage' }],
2292+
},
2293+
}}
2294+
objectDef={mockObjectDef}
2295+
onViewUpdate={onViewUpdate}
2296+
/>
2297+
);
2298+
2299+
fireEvent.click(screen.getByText('console.objectView.userFilters'));
2300+
expect(screen.getByTestId('uf-field-0')).toBeInTheDocument();
2301+
expect(screen.getByTestId('uf-field-1')).toBeInTheDocument();
2302+
fireEvent.click(screen.getByTestId('uf-remove-field-0'));
2303+
expect(onViewUpdate).toHaveBeenCalledWith('userFilters', expect.objectContaining({
2304+
fields: [{ field: 'stage' }],
2305+
}));
2306+
});
2307+
2308+
it('shows tabs editor when element is tabs', () => {
2309+
render(
2310+
<ViewConfigPanel
2311+
open={true}
2312+
onClose={vi.fn()}
2313+
activeView={{
2314+
...mockActiveView,
2315+
userFilters: {
2316+
element: 'tabs',
2317+
tabs: [{ id: 't1', label: 'Active', filters: [], default: true }],
2318+
},
2319+
}}
2320+
objectDef={mockObjectDef}
2321+
/>
2322+
);
2323+
2324+
fireEvent.click(screen.getByText('console.objectView.userFilters'));
2325+
expect(screen.getByTestId('uf-tabs-section')).toBeInTheDocument();
2326+
expect(screen.getByTestId('uf-tab-0')).toBeInTheDocument();
2327+
expect(screen.getByTestId('uf-add-tab')).toBeInTheDocument();
2328+
expect(screen.getByTestId('uf-show-all-records')).toBeInTheDocument();
2329+
expect(screen.getByTestId('uf-allow-add-tab')).toBeInTheDocument();
2330+
});
2331+
2332+
it('adds a tab in tabs mode', () => {
2333+
const onViewUpdate = vi.fn();
2334+
render(
2335+
<ViewConfigPanel
2336+
open={true}
2337+
onClose={vi.fn()}
2338+
activeView={{
2339+
...mockActiveView,
2340+
userFilters: { element: 'tabs', tabs: [] },
2341+
}}
2342+
objectDef={mockObjectDef}
2343+
onViewUpdate={onViewUpdate}
2344+
/>
2345+
);
2346+
2347+
fireEvent.click(screen.getByText('console.objectView.userFilters'));
2348+
fireEvent.click(screen.getByTestId('uf-add-tab'));
2349+
expect(onViewUpdate).toHaveBeenCalledWith('userFilters', expect.objectContaining({
2350+
tabs: expect.arrayContaining([
2351+
expect.objectContaining({ label: '', filters: [], default: false }),
2352+
]),
2353+
}));
2354+
});
2355+
2356+
it('removes a tab in tabs mode', () => {
2357+
const onViewUpdate = vi.fn();
2358+
render(
2359+
<ViewConfigPanel
2360+
open={true}
2361+
onClose={vi.fn()}
2362+
activeView={{
2363+
...mockActiveView,
2364+
userFilters: {
2365+
element: 'tabs',
2366+
tabs: [
2367+
{ id: 't1', label: 'Active', filters: [], default: true },
2368+
{ id: 't2', label: 'My Items', filters: [] },
2369+
],
2370+
},
2371+
}}
2372+
objectDef={mockObjectDef}
2373+
onViewUpdate={onViewUpdate}
2374+
/>
2375+
);
2376+
2377+
fireEvent.click(screen.getByText('console.objectView.userFilters'));
2378+
fireEvent.click(screen.getByTestId('uf-remove-tab-0'));
2379+
expect(onViewUpdate).toHaveBeenCalledWith('userFilters', expect.objectContaining({
2380+
tabs: [expect.objectContaining({ id: 't2', label: 'My Items' })],
2381+
}));
2382+
});
2383+
2384+
it('edits a tab label in tabs mode', () => {
2385+
const onViewUpdate = vi.fn();
2386+
render(
2387+
<ViewConfigPanel
2388+
open={true}
2389+
onClose={vi.fn()}
2390+
activeView={{
2391+
...mockActiveView,
2392+
userFilters: {
2393+
element: 'tabs',
2394+
tabs: [{ id: 't1', label: 'Active', filters: [] }],
2395+
},
2396+
}}
2397+
objectDef={mockObjectDef}
2398+
onViewUpdate={onViewUpdate}
2399+
/>
2400+
);
2401+
2402+
fireEvent.click(screen.getByText('console.objectView.userFilters'));
2403+
const labelInput = screen.getByTestId('uf-tab-label-0');
2404+
fireEvent.change(labelInput, { target: { value: 'My Customers' } });
2405+
expect(onViewUpdate).toHaveBeenCalledWith('userFilters', expect.objectContaining({
2406+
tabs: [expect.objectContaining({ id: 't1', label: 'My Customers' })],
2407+
}));
2408+
});
2409+
22252410
it('renders empty state inputs in appearance section', () => {
22262411
render(
22272412
<ViewConfigPanel

apps/console/src/__tests__/view-config-schema.test.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ describe('buildViewConfigSchema', () => {
457457
'_pageSize', '_pageSizeOptions',
458458
'_searchableFields', '_filterableFields', '_hiddenFields',
459459
'_quickFilters',
460+
'_userFilters',
460461
'virtualScroll',
461462
'_typeOptions',
462463
]);
@@ -469,6 +470,22 @@ describe('buildViewConfigSchema', () => {
469470
expect(fieldKeys.indexOf('_columns')).toBeLessThan(fieldKeys.indexOf('_filterBy'));
470471
expect(fieldKeys.indexOf('_filterBy')).toBeLessThan(fieldKeys.indexOf('_sortBy'));
471472
});
473+
474+
it('_userFilters field exists and is custom type', () => {
475+
const schema = buildSchema();
476+
const section = schema.sections.find((s: any) => s.key === 'data')!;
477+
const field = section.fields.find((f: any) => f.key === '_userFilters');
478+
expect(field).toBeDefined();
479+
expect(field!.type).toBe('custom');
480+
expect(field!.label).toBe('console.objectView.userFilters');
481+
});
482+
483+
it('_userFilters comes after _quickFilters', () => {
484+
const schema = buildSchema();
485+
const section = schema.sections.find((s: any) => s.key === 'data')!;
486+
const fieldKeys = section.fields.map((f: any) => f.key);
487+
expect(fieldKeys.indexOf('_userFilters')).toBeGreaterThan(fieldKeys.indexOf('_quickFilters'));
488+
});
472489
});
473490

474491
// ── Appearance Section ──────────────────────────────────────────────

apps/console/src/components/ObjectView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ export function ObjectView({ dataSource, objects, onEdit }: any) {
560560
addRecord: viewDef.addRecord ?? listSchema.addRecord,
561561
conditionalFormatting: viewDef.conditionalFormatting ?? listSchema.conditionalFormatting,
562562
quickFilters: viewDef.quickFilters ?? listSchema.quickFilters,
563+
userFilters: viewDef.userFilters ?? listSchema.userFilters,
563564
showRecordCount: viewDef.showRecordCount ?? listSchema.showRecordCount,
564565
allowPrinting: viewDef.allowPrinting ?? listSchema.allowPrinting,
565566
virtualScroll: viewDef.virtualScroll ?? listSchema.virtualScroll,

0 commit comments

Comments
 (0)