@object-ui/plugin-list 3.1.5 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +21 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +30492 -38346
  5. package/dist/index.umd.cjs +30 -38
  6. package/dist/{src → packages/plugin-list/src}/ListView.d.ts +17 -1
  7. package/dist/packages/plugin-list/src/ListView.d.ts.map +1 -0
  8. package/dist/packages/plugin-list/src/ListView.stories.d.ts.map +1 -0
  9. package/dist/packages/plugin-list/src/ObjectGallery.d.ts.map +1 -0
  10. package/dist/packages/plugin-list/src/UserFilters.d.ts.map +1 -0
  11. package/dist/packages/plugin-list/src/ViewSwitcher.d.ts.map +1 -0
  12. package/dist/packages/plugin-list/src/components/TabBar.d.ts.map +1 -0
  13. package/dist/{src → packages/plugin-list/src}/index.d.ts +1 -1
  14. package/dist/packages/plugin-list/src/index.d.ts.map +1 -0
  15. package/dist/plugin-list.css +1 -2
  16. package/package.json +35 -13
  17. package/.turbo/turbo-build.log +0 -24
  18. package/dist/src/ListView.d.ts.map +0 -1
  19. package/dist/src/ListView.stories.d.ts.map +0 -1
  20. package/dist/src/ObjectGallery.d.ts.map +0 -1
  21. package/dist/src/UserFilters.d.ts.map +0 -1
  22. package/dist/src/ViewSwitcher.d.ts.map +0 -1
  23. package/dist/src/components/TabBar.d.ts.map +0 -1
  24. package/dist/src/index.d.ts.map +0 -1
  25. package/src/ListView.stories.tsx +0 -64
  26. package/src/ListView.tsx +0 -1688
  27. package/src/ObjectGallery.tsx +0 -308
  28. package/src/UserFilters.tsx +0 -453
  29. package/src/ViewSwitcher.tsx +0 -113
  30. package/src/__tests__/ConditionalFormatting.test.ts +0 -285
  31. package/src/__tests__/DataFetch.test.tsx +0 -253
  32. package/src/__tests__/Export.test.tsx +0 -175
  33. package/src/__tests__/FilterNormalization.test.ts +0 -162
  34. package/src/__tests__/GalleryGrouping.test.tsx +0 -237
  35. package/src/__tests__/GalleryTimelineSpecConfig.test.tsx +0 -203
  36. package/src/__tests__/ListView.test.tsx +0 -2151
  37. package/src/__tests__/ListViewGroupingPropagation.test.tsx +0 -250
  38. package/src/__tests__/ListViewPersistence.test.tsx +0 -129
  39. package/src/__tests__/ObjectGallery.test.tsx +0 -208
  40. package/src/__tests__/TabBar.test.tsx +0 -199
  41. package/src/__tests__/UserFilters.test.tsx +0 -486
  42. package/src/components/TabBar.tsx +0 -120
  43. package/src/index.tsx +0 -78
  44. package/tsconfig.json +0 -18
  45. package/vite.config.ts +0 -56
  46. package/vitest.config.ts +0 -12
  47. package/vitest.setup.ts +0 -1
  48. /package/dist/{src → packages/plugin-list/src}/ListView.stories.d.ts +0 -0
  49. /package/dist/{src → packages/plugin-list/src}/ObjectGallery.d.ts +0 -0
  50. /package/dist/{src → packages/plugin-list/src}/UserFilters.d.ts +0 -0
  51. /package/dist/{src → packages/plugin-list/src}/ViewSwitcher.d.ts +0 -0
  52. /package/dist/{src → packages/plugin-list/src}/components/TabBar.d.ts +0 -0
@@ -1,175 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, vi, beforeEach } from 'vitest';
10
- import { render, screen, fireEvent } from '@testing-library/react';
11
- import { ListView } from '../ListView';
12
- import type { ListViewSchema } from '@object-ui/types';
13
- import { SchemaRendererProvider } from '@object-ui/react';
14
-
15
- // Mock URL.createObjectURL and revokeObjectURL
16
- const mockCreateObjectURL = vi.fn().mockReturnValue('blob:test');
17
- const mockRevokeObjectURL = vi.fn();
18
- Object.defineProperty(URL, 'createObjectURL', { value: mockCreateObjectURL, writable: true });
19
- Object.defineProperty(URL, 'revokeObjectURL', { value: mockRevokeObjectURL, writable: true });
20
-
21
- const mockDataSource = {
22
- find: vi.fn().mockResolvedValue([]),
23
- findOne: vi.fn(),
24
- create: vi.fn(),
25
- update: vi.fn(),
26
- delete: vi.fn(),
27
- };
28
-
29
- const renderWithProvider = (component: React.ReactNode) => {
30
- return render(
31
- <SchemaRendererProvider dataSource={mockDataSource}>
32
- {component}
33
- </SchemaRendererProvider>
34
- );
35
- };
36
-
37
- describe('ListView Export', () => {
38
- beforeEach(() => {
39
- vi.clearAllMocks();
40
- });
41
-
42
- it('should render export button with configured formats', () => {
43
- const schema: ListViewSchema = {
44
- type: 'list-view',
45
- objectName: 'contacts',
46
- viewType: 'grid',
47
- fields: ['name', 'email'],
48
- exportOptions: {
49
- formats: ['csv', 'json'],
50
- },
51
- };
52
-
53
- renderWithProvider(<ListView schema={schema} />);
54
- const exportButton = screen.getByRole('button', { name: /export/i });
55
- expect(exportButton).toBeInTheDocument();
56
- });
57
-
58
- it('should render export button with spec string[] format', () => {
59
- const schema: ListViewSchema = {
60
- type: 'list-view',
61
- objectName: 'contacts',
62
- viewType: 'grid',
63
- fields: ['name', 'email'],
64
- exportOptions: ['csv', 'xlsx'],
65
- };
66
-
67
- renderWithProvider(<ListView schema={schema} />);
68
- const exportButton = screen.getByRole('button', { name: /export/i });
69
- expect(exportButton).toBeInTheDocument();
70
-
71
- // Click to open the popover and verify formats
72
- fireEvent.click(exportButton);
73
- expect(screen.getByRole('button', { name: /export as csv/i })).toBeInTheDocument();
74
- expect(screen.getByRole('button', { name: /export as xlsx/i })).toBeInTheDocument();
75
- });
76
-
77
- it('should handle export with complex object fields in CSV safely', async () => {
78
- const mockItems = [
79
- { id: '1', name: 'Alice', tags: ['admin', 'user'], metadata: { role: 'lead' } },
80
- { id: '2', name: 'Bob', tags: ['user'], metadata: null },
81
- ];
82
- mockDataSource.find.mockResolvedValue(mockItems);
83
-
84
- const schema: ListViewSchema = {
85
- type: 'list-view',
86
- objectName: 'contacts',
87
- viewType: 'grid',
88
- fields: ['name', 'tags', 'metadata'],
89
- exportOptions: {
90
- formats: ['csv'],
91
- },
92
- };
93
-
94
- renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
95
-
96
- // Wait for data to load
97
- await vi.waitFor(() => {
98
- expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
99
- });
100
-
101
- // Click export button
102
- const exportButton = screen.getByRole('button', { name: /export/i });
103
- fireEvent.click(exportButton);
104
-
105
- // Click CSV format
106
- const csvButton = screen.getByRole('button', { name: /export as csv/i });
107
-
108
- // Mock createElement and click
109
- const mockClick = vi.fn();
110
- const originalCreateElement = document.createElement.bind(document);
111
- vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
112
- const el = originalCreateElement(tag);
113
- if (tag === 'a') {
114
- el.click = mockClick;
115
- }
116
- return el;
117
- });
118
-
119
- fireEvent.click(csvButton);
120
- expect(mockCreateObjectURL).toHaveBeenCalled();
121
-
122
- // Verify the blob content includes safe serialization
123
- const blobArg = mockCreateObjectURL.mock.calls[0]?.[0];
124
- if (blobArg instanceof Blob) {
125
- const text = await blobArg.text();
126
- // Headers
127
- expect(text).toContain('name,tags,metadata');
128
- // Array should be serialized as semicolon-separated, not raw
129
- expect(text).toContain('admin; user');
130
- // Object should be JSON-serialized (CSV-escaped with doubled quotes)
131
- expect(text).toContain('{""role"":""lead""}');
132
- }
133
- });
134
-
135
- it('should handle export with JSON format', async () => {
136
- const mockItems = [
137
- { id: '1', name: 'Alice', email: 'alice@test.com' },
138
- ];
139
- mockDataSource.find.mockResolvedValue(mockItems);
140
-
141
- const schema: ListViewSchema = {
142
- type: 'list-view',
143
- objectName: 'contacts',
144
- viewType: 'grid',
145
- fields: ['name', 'email'],
146
- exportOptions: {
147
- formats: ['json'],
148
- },
149
- };
150
-
151
- renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
152
-
153
- await vi.waitFor(() => {
154
- expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
155
- });
156
-
157
- const exportButton = screen.getByRole('button', { name: /export/i });
158
- fireEvent.click(exportButton);
159
-
160
- const jsonButton = screen.getByRole('button', { name: /export as json/i });
161
-
162
- const mockClick = vi.fn();
163
- const originalCreateElement = document.createElement.bind(document);
164
- vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
165
- const el = originalCreateElement(tag);
166
- if (tag === 'a') {
167
- el.click = mockClick;
168
- }
169
- return el;
170
- });
171
-
172
- fireEvent.click(jsonButton);
173
- expect(mockCreateObjectURL).toHaveBeenCalled();
174
- });
175
- });
@@ -1,162 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect } from 'vitest';
10
- import { normalizeFilterCondition, normalizeFilters } from '../ListView';
11
-
12
- describe('normalizeFilterCondition', () => {
13
- // =========================================================================
14
- // `in` operator normalization
15
- // =========================================================================
16
- describe('in operator', () => {
17
- it('converts single-value `in` to `=`', () => {
18
- expect(normalizeFilterCondition(['status', 'in', ['active']])).toEqual(
19
- ['status', '=', 'active'],
20
- );
21
- });
22
-
23
- it('converts multi-value `in` to `or` of `=`', () => {
24
- expect(normalizeFilterCondition(['status', 'in', ['active', 'pending']])).toEqual(
25
- ['or', ['status', '=', 'active'], ['status', '=', 'pending']],
26
- );
27
- });
28
-
29
- it('returns empty array for empty `in` values', () => {
30
- expect(normalizeFilterCondition(['status', 'in', []])).toEqual([]);
31
- });
32
-
33
- it('handles numeric values in `in`', () => {
34
- expect(normalizeFilterCondition(['priority', 'in', [1, 2, 3]])).toEqual(
35
- ['or', ['priority', '=', 1], ['priority', '=', 2], ['priority', '=', 3]],
36
- );
37
- });
38
-
39
- it('handles boolean values in `in`', () => {
40
- expect(normalizeFilterCondition(['is_active', 'in', [true]])).toEqual(
41
- ['is_active', '=', true],
42
- );
43
- });
44
- });
45
-
46
- // =========================================================================
47
- // `not in` operator normalization
48
- // =========================================================================
49
- describe('not in operator', () => {
50
- it('converts single-value `not in` to `!=`', () => {
51
- expect(normalizeFilterCondition(['status', 'not in', ['closed']])).toEqual(
52
- ['status', '!=', 'closed'],
53
- );
54
- });
55
-
56
- it('converts multi-value `not in` to `and` of `!=`', () => {
57
- expect(normalizeFilterCondition(['status', 'not in', ['closed', 'archived']])).toEqual(
58
- ['and', ['status', '!=', 'closed'], ['status', '!=', 'archived']],
59
- );
60
- });
61
-
62
- it('returns empty array for empty `not in` values', () => {
63
- expect(normalizeFilterCondition(['status', 'not in', []])).toEqual([]);
64
- });
65
- });
66
-
67
- // =========================================================================
68
- // Passthrough for non-in operators
69
- // =========================================================================
70
- describe('passthrough', () => {
71
- it('passes through `=` operator unchanged', () => {
72
- expect(normalizeFilterCondition(['name', '=', 'Alice'])).toEqual(
73
- ['name', '=', 'Alice'],
74
- );
75
- });
76
-
77
- it('passes through `!=` operator unchanged', () => {
78
- expect(normalizeFilterCondition(['status', '!=', null])).toEqual(
79
- ['status', '!=', null],
80
- );
81
- });
82
-
83
- it('passes through `>` operator unchanged', () => {
84
- expect(normalizeFilterCondition(['amount', '>', 100])).toEqual(
85
- ['amount', '>', 100],
86
- );
87
- });
88
-
89
- it('passes through `contains` operator unchanged', () => {
90
- expect(normalizeFilterCondition(['name', 'contains', 'test'])).toEqual(
91
- ['name', 'contains', 'test'],
92
- );
93
- });
94
- });
95
-
96
- // =========================================================================
97
- // Logical group recursion
98
- // =========================================================================
99
- describe('logical groups', () => {
100
- it('recursively normalizes `and` groups', () => {
101
- const input = ['and', ['status', 'in', ['a', 'b']], ['name', '=', 'Alice']];
102
- expect(normalizeFilterCondition(input)).toEqual(
103
- ['and', ['or', ['status', '=', 'a'], ['status', '=', 'b']], ['name', '=', 'Alice']],
104
- );
105
- });
106
-
107
- it('recursively normalizes `or` groups', () => {
108
- const input = ['or', ['priority', 'in', [1, 2]], ['status', '=', 'active']];
109
- expect(normalizeFilterCondition(input)).toEqual(
110
- ['or', ['or', ['priority', '=', 1], ['priority', '=', 2]], ['status', '=', 'active']],
111
- );
112
- });
113
- });
114
-
115
- // =========================================================================
116
- // Edge cases
117
- // =========================================================================
118
- describe('edge cases', () => {
119
- it('handles non-array input gracefully', () => {
120
- expect(normalizeFilterCondition([] as any)).toEqual([]);
121
- });
122
-
123
- it('handles short array input gracefully', () => {
124
- expect(normalizeFilterCondition(['field'] as any)).toEqual(['field']);
125
- });
126
- });
127
- });
128
-
129
- describe('normalizeFilters', () => {
130
- it('normalizes an array of conditions', () => {
131
- const input = [
132
- ['status', 'in', ['active', 'pending']],
133
- ['name', '=', 'Alice'],
134
- ];
135
- const result = normalizeFilters(input);
136
- expect(result).toEqual([
137
- ['or', ['status', '=', 'active'], ['status', '=', 'pending']],
138
- ['name', '=', 'Alice'],
139
- ]);
140
- });
141
-
142
- it('filters out empty arrays from normalization', () => {
143
- const input = [
144
- ['status', 'in', []],
145
- ['name', '=', 'Alice'],
146
- ];
147
- const result = normalizeFilters(input);
148
- expect(result).toEqual([
149
- ['name', '=', 'Alice'],
150
- ]);
151
- });
152
-
153
- it('returns empty array for empty input', () => {
154
- expect(normalizeFilters([])).toEqual([]);
155
- });
156
-
157
- it('handles non-array items gracefully', () => {
158
- const input = [null, undefined, 'invalid', ['name', '=', 'test']];
159
- const result = normalizeFilters(input as any);
160
- expect(result).toEqual([['name', '=', 'test']]);
161
- });
162
- });
@@ -1,237 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, vi, beforeEach } from 'vitest';
10
- import { render, screen, fireEvent } from '@testing-library/react';
11
- import { ObjectGallery } from '../ObjectGallery';
12
-
13
- const mockHandleClick = vi.fn();
14
- const mockNavigationOverlay = {
15
- isOverlay: false,
16
- handleClick: mockHandleClick,
17
- selectedRecord: null,
18
- isOpen: false,
19
- close: vi.fn(),
20
- setIsOpen: vi.fn(),
21
- mode: 'page' as const,
22
- width: undefined,
23
- view: undefined,
24
- open: vi.fn(),
25
- };
26
-
27
- vi.mock('@object-ui/react', () => {
28
- const React = require('react');
29
- return {
30
- useDataScope: () => undefined,
31
- SchemaRendererContext: React.createContext(null),
32
- useNavigationOverlay: () => mockNavigationOverlay,
33
- };
34
- });
35
-
36
- vi.mock('@object-ui/components', () => ({
37
- cn: (...args: any[]) => args.filter(Boolean).join(' '),
38
- Card: ({ children, onClick, ...props }: any) => (
39
- <div data-testid="gallery-card" onClick={onClick} {...props}>{children}</div>
40
- ),
41
- CardContent: ({ children, ...props }: any) => <div {...props}>{children}</div>,
42
- NavigationOverlay: ({ children, selectedRecord }: any) => (
43
- selectedRecord ? <div data-testid="navigation-overlay">{children(selectedRecord)}</div> : null
44
- ),
45
- }));
46
-
47
- vi.mock('@object-ui/core', () => ({
48
- ComponentRegistry: { register: vi.fn() },
49
- }));
50
-
51
- vi.mock('lucide-react', () => ({
52
- ChevronRight: () => <span data-testid="chevron-right">▸</span>,
53
- ChevronDown: () => <span data-testid="chevron-down">▾</span>,
54
- }));
55
-
56
- const mockItems = [
57
- { id: '1', name: 'Alpha Widget', category: 'Electronics', image: 'https://example.com/1.jpg' },
58
- { id: '2', name: 'Beta Gadget', category: 'Electronics', image: 'https://example.com/2.jpg' },
59
- { id: '3', name: 'Gamma Tool', category: 'Tools', image: 'https://example.com/3.jpg' },
60
- { id: '4', name: 'Delta Supply', category: 'Office', image: 'https://example.com/4.jpg' },
61
- { id: '5', name: 'Epsilon Gear', category: 'Tools', image: 'https://example.com/5.jpg' },
62
- ];
63
-
64
- describe('ObjectGallery Grouping', () => {
65
- beforeEach(() => {
66
- vi.clearAllMocks();
67
- });
68
-
69
- it('renders without grouping (flat list) when no grouping config', () => {
70
- const schema = { objectName: 'products' };
71
- render(<ObjectGallery schema={schema} data={mockItems} />);
72
-
73
- // All items visible
74
- expect(screen.getByText('Alpha Widget')).toBeInTheDocument();
75
- expect(screen.getByText('Beta Gadget')).toBeInTheDocument();
76
- expect(screen.getByText('Gamma Tool')).toBeInTheDocument();
77
- expect(screen.getByText('Delta Supply')).toBeInTheDocument();
78
- expect(screen.getByText('Epsilon Gear')).toBeInTheDocument();
79
-
80
- // No group headers
81
- expect(screen.queryByText('Electronics')).not.toBeInTheDocument();
82
- expect(screen.queryByText('Tools')).not.toBeInTheDocument();
83
- });
84
-
85
- it('renders grouped sections when grouping config is provided', () => {
86
- const schema = {
87
- objectName: 'products',
88
- grouping: {
89
- fields: [{ field: 'category', order: 'asc' as const, collapsed: false }],
90
- },
91
- };
92
- render(<ObjectGallery schema={schema} data={mockItems} />);
93
-
94
- // Group headers should be visible
95
- expect(screen.getByText('Electronics')).toBeInTheDocument();
96
- expect(screen.getByText('Tools')).toBeInTheDocument();
97
- expect(screen.getByText('Office')).toBeInTheDocument();
98
-
99
- // All items should be visible (none collapsed)
100
- expect(screen.getByText('Alpha Widget')).toBeInTheDocument();
101
- expect(screen.getByText('Beta Gadget')).toBeInTheDocument();
102
- expect(screen.getByText('Gamma Tool')).toBeInTheDocument();
103
- expect(screen.getByText('Delta Supply')).toBeInTheDocument();
104
- expect(screen.getByText('Epsilon Gear')).toBeInTheDocument();
105
- });
106
-
107
- it('shows record count per group', () => {
108
- const schema = {
109
- objectName: 'products',
110
- grouping: {
111
- fields: [{ field: 'category', order: 'asc' as const, collapsed: false }],
112
- },
113
- };
114
- render(<ObjectGallery schema={schema} data={mockItems} />);
115
-
116
- // Electronics has 2 items, Tools has 2, Office has 1
117
- const buttons = screen.getAllByRole('button');
118
- const electronicsBtn = buttons.find(b => b.textContent?.includes('Electronics'));
119
- const toolsBtn = buttons.find(b => b.textContent?.includes('Tools'));
120
- const officeBtn = buttons.find(b => b.textContent?.includes('Office'));
121
-
122
- expect(electronicsBtn?.textContent).toContain('2');
123
- expect(toolsBtn?.textContent).toContain('2');
124
- expect(officeBtn?.textContent).toContain('1');
125
- });
126
-
127
- it('collapses a group when clicking the group header', () => {
128
- const schema = {
129
- objectName: 'products',
130
- grouping: {
131
- fields: [{ field: 'category', order: 'asc' as const, collapsed: false }],
132
- },
133
- };
134
- render(<ObjectGallery schema={schema} data={mockItems} />);
135
-
136
- // All items visible initially
137
- expect(screen.getByText('Alpha Widget')).toBeInTheDocument();
138
- expect(screen.getByText('Beta Gadget')).toBeInTheDocument();
139
-
140
- // Click Electronics group header to collapse
141
- const buttons = screen.getAllByRole('button');
142
- const electronicsBtn = buttons.find(b => b.textContent?.includes('Electronics'))!;
143
- fireEvent.click(electronicsBtn);
144
-
145
- // Electronics items should be hidden
146
- expect(screen.queryByText('Alpha Widget')).not.toBeInTheDocument();
147
- expect(screen.queryByText('Beta Gadget')).not.toBeInTheDocument();
148
-
149
- // Other items still visible
150
- expect(screen.getByText('Gamma Tool')).toBeInTheDocument();
151
- expect(screen.getByText('Delta Supply')).toBeInTheDocument();
152
- });
153
-
154
- it('expands a collapsed group when clicking again', () => {
155
- const schema = {
156
- objectName: 'products',
157
- grouping: {
158
- fields: [{ field: 'category', order: 'asc' as const, collapsed: false }],
159
- },
160
- };
161
- render(<ObjectGallery schema={schema} data={mockItems} />);
162
-
163
- const buttons = screen.getAllByRole('button');
164
- const electronicsBtn = buttons.find(b => b.textContent?.includes('Electronics'))!;
165
-
166
- // Collapse
167
- fireEvent.click(electronicsBtn);
168
- expect(screen.queryByText('Alpha Widget')).not.toBeInTheDocument();
169
-
170
- // Expand
171
- fireEvent.click(electronicsBtn);
172
- expect(screen.getByText('Alpha Widget')).toBeInTheDocument();
173
- expect(screen.getByText('Beta Gadget')).toBeInTheDocument();
174
- });
175
-
176
- it('respects initial collapsed state from grouping config', () => {
177
- const schema = {
178
- objectName: 'products',
179
- grouping: {
180
- fields: [{ field: 'category', order: 'asc' as const, collapsed: true }],
181
- },
182
- };
183
- render(<ObjectGallery schema={schema} data={mockItems} />);
184
-
185
- // Group headers should be visible
186
- expect(screen.getByText('Electronics')).toBeInTheDocument();
187
- expect(screen.getByText('Tools')).toBeInTheDocument();
188
- expect(screen.getByText('Office')).toBeInTheDocument();
189
-
190
- // All items should be hidden (all groups collapsed by default)
191
- expect(screen.queryByText('Alpha Widget')).not.toBeInTheDocument();
192
- expect(screen.queryByText('Beta Gadget')).not.toBeInTheDocument();
193
- expect(screen.queryByText('Gamma Tool')).not.toBeInTheDocument();
194
- expect(screen.queryByText('Delta Supply')).not.toBeInTheDocument();
195
- expect(screen.queryByText('Epsilon Gear')).not.toBeInTheDocument();
196
- });
197
-
198
- it('shows (empty) label for items with empty grouping field', () => {
199
- const items = [
200
- { id: '1', name: 'Item A', category: 'Cat1' },
201
- { id: '2', name: 'Item B', category: '' },
202
- { id: '3', name: 'Item C' }, // no category field
203
- ];
204
- const schema = {
205
- objectName: 'products',
206
- grouping: {
207
- fields: [{ field: 'category', order: 'asc' as const, collapsed: false }],
208
- },
209
- };
210
- render(<ObjectGallery schema={schema} data={items} />);
211
-
212
- expect(screen.getByText('Cat1')).toBeInTheDocument();
213
- expect(screen.getByText('(empty)')).toBeInTheDocument();
214
- });
215
-
216
- it('sorts groups by descending order when configured', () => {
217
- const schema = {
218
- objectName: 'products',
219
- grouping: {
220
- fields: [{ field: 'category', order: 'desc' as const, collapsed: false }],
221
- },
222
- };
223
- render(<ObjectGallery schema={schema} data={mockItems} />);
224
-
225
- const buttons = screen.getAllByRole('button');
226
- const labels = buttons.map(b => {
227
- // Extract the group label text (the <span> inside button)
228
- const spans = b.querySelectorAll('span');
229
- return spans[1]?.textContent; // label span
230
- }).filter(Boolean);
231
-
232
- // With desc order: Tools > Office > Electronics
233
- expect(labels[0]).toBe('Tools');
234
- expect(labels[1]).toBe('Office');
235
- expect(labels[2]).toBe('Electronics');
236
- });
237
- });