@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.
- package/CHANGELOG.md +34 -0
- package/README.md +21 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30492 -38346
- package/dist/index.umd.cjs +30 -38
- package/dist/{src → packages/plugin-list/src}/ListView.d.ts +17 -1
- package/dist/packages/plugin-list/src/ListView.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/ListView.stories.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/ObjectGallery.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/UserFilters.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/ViewSwitcher.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/components/TabBar.d.ts.map +1 -0
- package/dist/{src → packages/plugin-list/src}/index.d.ts +1 -1
- package/dist/packages/plugin-list/src/index.d.ts.map +1 -0
- package/dist/plugin-list.css +1 -2
- package/package.json +35 -13
- package/.turbo/turbo-build.log +0 -24
- package/dist/src/ListView.d.ts.map +0 -1
- package/dist/src/ListView.stories.d.ts.map +0 -1
- package/dist/src/ObjectGallery.d.ts.map +0 -1
- package/dist/src/UserFilters.d.ts.map +0 -1
- package/dist/src/ViewSwitcher.d.ts.map +0 -1
- package/dist/src/components/TabBar.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/src/ListView.stories.tsx +0 -64
- package/src/ListView.tsx +0 -1688
- package/src/ObjectGallery.tsx +0 -308
- package/src/UserFilters.tsx +0 -453
- package/src/ViewSwitcher.tsx +0 -113
- package/src/__tests__/ConditionalFormatting.test.ts +0 -285
- package/src/__tests__/DataFetch.test.tsx +0 -253
- package/src/__tests__/Export.test.tsx +0 -175
- package/src/__tests__/FilterNormalization.test.ts +0 -162
- package/src/__tests__/GalleryGrouping.test.tsx +0 -237
- package/src/__tests__/GalleryTimelineSpecConfig.test.tsx +0 -203
- package/src/__tests__/ListView.test.tsx +0 -2151
- package/src/__tests__/ListViewGroupingPropagation.test.tsx +0 -250
- package/src/__tests__/ListViewPersistence.test.tsx +0 -129
- package/src/__tests__/ObjectGallery.test.tsx +0 -208
- package/src/__tests__/TabBar.test.tsx +0 -199
- package/src/__tests__/UserFilters.test.tsx +0 -486
- package/src/components/TabBar.tsx +0 -120
- package/src/index.tsx +0 -78
- package/tsconfig.json +0 -18
- package/vite.config.ts +0 -56
- package/vitest.config.ts +0 -12
- package/vitest.setup.ts +0 -1
- /package/dist/{src → packages/plugin-list/src}/ListView.stories.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/ObjectGallery.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/UserFilters.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/ViewSwitcher.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/components/TabBar.d.ts +0 -0
|
@@ -1,199 +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 } from 'vitest';
|
|
10
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
11
|
-
import { TabBar } from '../components/TabBar';
|
|
12
|
-
import type { ViewTab } from '../components/TabBar';
|
|
13
|
-
|
|
14
|
-
describe('TabBar', () => {
|
|
15
|
-
const baseTabs: ViewTab[] = [
|
|
16
|
-
{ name: 'all', label: 'All Records', isDefault: true },
|
|
17
|
-
{ name: 'active', label: 'Active' },
|
|
18
|
-
{ name: 'archived', label: 'Archived' },
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
it('should render tab bar with tabs', () => {
|
|
22
|
-
render(<TabBar tabs={baseTabs} />);
|
|
23
|
-
expect(screen.getByTestId('view-tabs')).toBeInTheDocument();
|
|
24
|
-
expect(screen.getByTestId('view-tab-all')).toBeInTheDocument();
|
|
25
|
-
expect(screen.getByTestId('view-tab-active')).toBeInTheDocument();
|
|
26
|
-
expect(screen.getByTestId('view-tab-archived')).toBeInTheDocument();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should render tab labels', () => {
|
|
30
|
-
render(<TabBar tabs={baseTabs} />);
|
|
31
|
-
expect(screen.getByText('All Records')).toBeInTheDocument();
|
|
32
|
-
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
33
|
-
expect(screen.getByText('Archived')).toBeInTheDocument();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should not render when tabs array is empty', () => {
|
|
37
|
-
const { container } = render(<TabBar tabs={[]} />);
|
|
38
|
-
expect(container.innerHTML).toBe('');
|
|
39
|
-
expect(screen.queryByTestId('view-tabs')).not.toBeInTheDocument();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// isDefault tab should be selected by default
|
|
43
|
-
it('should select isDefault tab initially', () => {
|
|
44
|
-
render(<TabBar tabs={baseTabs} />);
|
|
45
|
-
const defaultTab = screen.getByTestId('view-tab-all');
|
|
46
|
-
expect(defaultTab).toHaveAttribute('aria-selected', 'true');
|
|
47
|
-
const otherTab = screen.getByTestId('view-tab-active');
|
|
48
|
-
expect(otherTab).toHaveAttribute('aria-selected', 'false');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should select first tab when no isDefault is set', () => {
|
|
52
|
-
const tabs: ViewTab[] = [
|
|
53
|
-
{ name: 'alpha', label: 'Alpha' },
|
|
54
|
-
{ name: 'beta', label: 'Beta' },
|
|
55
|
-
];
|
|
56
|
-
render(<TabBar tabs={tabs} />);
|
|
57
|
-
expect(screen.getByTestId('view-tab-alpha')).toHaveAttribute('aria-selected', 'true');
|
|
58
|
-
expect(screen.getByTestId('view-tab-beta')).toHaveAttribute('aria-selected', 'false');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Tab click changes active tab
|
|
62
|
-
it('should switch active tab on click', () => {
|
|
63
|
-
render(<TabBar tabs={baseTabs} />);
|
|
64
|
-
const activeTab = screen.getByTestId('view-tab-active');
|
|
65
|
-
fireEvent.click(activeTab);
|
|
66
|
-
expect(activeTab).toHaveAttribute('aria-selected', 'true');
|
|
67
|
-
expect(screen.getByTestId('view-tab-all')).toHaveAttribute('aria-selected', 'false');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should call onTabChange callback on click', () => {
|
|
71
|
-
const onTabChange = vi.fn();
|
|
72
|
-
render(<TabBar tabs={baseTabs} onTabChange={onTabChange} />);
|
|
73
|
-
fireEvent.click(screen.getByTestId('view-tab-active'));
|
|
74
|
-
expect(onTabChange).toHaveBeenCalledTimes(1);
|
|
75
|
-
expect(onTabChange).toHaveBeenCalledWith(expect.objectContaining({ name: 'active', label: 'Active' }));
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Pinned tabs always visible
|
|
79
|
-
it('should always show pinned tabs even if visible is false', () => {
|
|
80
|
-
const tabs: ViewTab[] = [
|
|
81
|
-
{ name: 'all', label: 'All', isDefault: true },
|
|
82
|
-
{ name: 'pinned-tab', label: 'Pinned', pinned: true, visible: 'false' },
|
|
83
|
-
{ name: 'hidden', label: 'Hidden', visible: 'false' },
|
|
84
|
-
];
|
|
85
|
-
render(<TabBar tabs={tabs} />);
|
|
86
|
-
expect(screen.getByTestId('view-tab-pinned-tab')).toBeInTheDocument();
|
|
87
|
-
expect(screen.queryByTestId('view-tab-hidden')).not.toBeInTheDocument();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Hidden tabs filtered out
|
|
91
|
-
it('should filter out hidden tabs (visible: "false")', () => {
|
|
92
|
-
const tabs: ViewTab[] = [
|
|
93
|
-
{ name: 'all', label: 'All Records' },
|
|
94
|
-
{ name: 'hidden', label: 'Hidden Tab', visible: 'false' },
|
|
95
|
-
];
|
|
96
|
-
render(<TabBar tabs={tabs} />);
|
|
97
|
-
expect(screen.getByText('All Records')).toBeInTheDocument();
|
|
98
|
-
expect(screen.queryByText('Hidden Tab')).not.toBeInTheDocument();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should filter out tabs with visible: boolean false', () => {
|
|
102
|
-
const tabs: ViewTab[] = [
|
|
103
|
-
{ name: 'all', label: 'All Records' },
|
|
104
|
-
{ name: 'hidden', label: 'Hidden Tab', visible: false as any },
|
|
105
|
-
];
|
|
106
|
-
render(<TabBar tabs={tabs} />);
|
|
107
|
-
expect(screen.getByText('All Records')).toBeInTheDocument();
|
|
108
|
-
expect(screen.queryByText('Hidden Tab')).not.toBeInTheDocument();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Order sorting
|
|
112
|
-
it('should sort tabs by order property', () => {
|
|
113
|
-
const tabs: ViewTab[] = [
|
|
114
|
-
{ name: 'c', label: 'Third', order: 3 },
|
|
115
|
-
{ name: 'a', label: 'First', order: 1 },
|
|
116
|
-
{ name: 'b', label: 'Second', order: 2 },
|
|
117
|
-
];
|
|
118
|
-
render(<TabBar tabs={tabs} />);
|
|
119
|
-
const tabContainer = screen.getByTestId('view-tabs');
|
|
120
|
-
const buttons = tabContainer.querySelectorAll('button');
|
|
121
|
-
expect(buttons[0]).toHaveTextContent('First');
|
|
122
|
-
expect(buttons[1]).toHaveTextContent('Second');
|
|
123
|
-
expect(buttons[2]).toHaveTextContent('Third');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Icon rendering
|
|
127
|
-
it('should render Lucide icon when icon prop is provided', () => {
|
|
128
|
-
const tabs: ViewTab[] = [
|
|
129
|
-
{ name: 'starred', label: 'Starred', icon: 'star' },
|
|
130
|
-
];
|
|
131
|
-
render(<TabBar tabs={tabs} />);
|
|
132
|
-
const tab = screen.getByTestId('view-tab-starred');
|
|
133
|
-
// Lucide icons render as SVG elements
|
|
134
|
-
const svg = tab.querySelector('svg');
|
|
135
|
-
expect(svg).toBeTruthy();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should handle kebab-case icon names', () => {
|
|
139
|
-
const tabs: ViewTab[] = [
|
|
140
|
-
{ name: 'test', label: 'Test', icon: 'arrow-right' },
|
|
141
|
-
];
|
|
142
|
-
render(<TabBar tabs={tabs} />);
|
|
143
|
-
const tab = screen.getByTestId('view-tab-test');
|
|
144
|
-
const svg = tab.querySelector('svg');
|
|
145
|
-
expect(svg).toBeTruthy();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should not render icon when icon is not provided', () => {
|
|
149
|
-
const tabs: ViewTab[] = [
|
|
150
|
-
{ name: 'noicon', label: 'No Icon' },
|
|
151
|
-
];
|
|
152
|
-
render(<TabBar tabs={tabs} />);
|
|
153
|
-
const tab = screen.getByTestId('view-tab-noicon');
|
|
154
|
-
const svg = tab.querySelector('svg');
|
|
155
|
-
expect(svg).toBeFalsy();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Controlled activeTab
|
|
159
|
-
it('should respect controlled activeTab prop', () => {
|
|
160
|
-
render(<TabBar tabs={baseTabs} activeTab="archived" />);
|
|
161
|
-
expect(screen.getByTestId('view-tab-archived')).toHaveAttribute('aria-selected', 'true');
|
|
162
|
-
expect(screen.getByTestId('view-tab-all')).toHaveAttribute('aria-selected', 'false');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Tab role attributes
|
|
166
|
-
it('should have proper ARIA attributes', () => {
|
|
167
|
-
render(<TabBar tabs={baseTabs} />);
|
|
168
|
-
const tabBar = screen.getByTestId('view-tabs');
|
|
169
|
-
expect(tabBar).toHaveAttribute('role', 'tablist');
|
|
170
|
-
const tab = screen.getByTestId('view-tab-all');
|
|
171
|
-
expect(tab).toHaveAttribute('role', 'tab');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Tab with filter config
|
|
175
|
-
it('should pass tab with filter to onTabChange', () => {
|
|
176
|
-
const onTabChange = vi.fn();
|
|
177
|
-
const tabs: ViewTab[] = [
|
|
178
|
-
{ name: 'all', label: 'All', isDefault: true },
|
|
179
|
-
{
|
|
180
|
-
name: 'active',
|
|
181
|
-
label: 'Active',
|
|
182
|
-
filter: { logic: 'and', conditions: [{ field: 'status', operator: 'eq', value: 'active' }] },
|
|
183
|
-
},
|
|
184
|
-
];
|
|
185
|
-
render(<TabBar tabs={tabs} onTabChange={onTabChange} />);
|
|
186
|
-
fireEvent.click(screen.getByTestId('view-tab-active'));
|
|
187
|
-
expect(onTabChange).toHaveBeenCalledWith(
|
|
188
|
-
expect.objectContaining({
|
|
189
|
-
name: 'active',
|
|
190
|
-
filter: expect.objectContaining({
|
|
191
|
-
logic: 'and',
|
|
192
|
-
conditions: expect.arrayContaining([
|
|
193
|
-
expect.objectContaining({ field: 'status' }),
|
|
194
|
-
]),
|
|
195
|
-
}),
|
|
196
|
-
}),
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
@@ -1,486 +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 } from 'vitest';
|
|
10
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
11
|
-
import { UserFilters } from '../UserFilters';
|
|
12
|
-
|
|
13
|
-
describe('UserFilters', () => {
|
|
14
|
-
// ============================================
|
|
15
|
-
// Dropdown Mode
|
|
16
|
-
// ============================================
|
|
17
|
-
describe('Dropdown mode', () => {
|
|
18
|
-
const dropdownConfig = {
|
|
19
|
-
element: 'dropdown' as const,
|
|
20
|
-
fields: [
|
|
21
|
-
{
|
|
22
|
-
field: 'status',
|
|
23
|
-
label: 'Status',
|
|
24
|
-
type: 'multi-select' as const,
|
|
25
|
-
options: [
|
|
26
|
-
{ label: 'Active', value: 'active' },
|
|
27
|
-
{ label: 'Inactive', value: 'inactive' },
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
field: 'priority',
|
|
32
|
-
label: 'Priority',
|
|
33
|
-
type: 'multi-select' as const,
|
|
34
|
-
options: [
|
|
35
|
-
{ label: 'High', value: 'high', color: '#dc2626' },
|
|
36
|
-
{ label: 'Low', value: 'low', color: '#2563eb' },
|
|
37
|
-
],
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
it('renders field badges with labels', () => {
|
|
43
|
-
const onChange = vi.fn();
|
|
44
|
-
render(<UserFilters config={dropdownConfig} onFilterChange={onChange} />);
|
|
45
|
-
|
|
46
|
-
expect(screen.getByTestId('user-filters-dropdown')).toBeInTheDocument();
|
|
47
|
-
expect(screen.getByTestId('filter-badge-status')).toBeInTheDocument();
|
|
48
|
-
expect(screen.getByTestId('filter-badge-priority')).toBeInTheDocument();
|
|
49
|
-
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
50
|
-
expect(screen.getByText('Priority')).toBeInTheDocument();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('opens popover and shows options on click', () => {
|
|
54
|
-
const onChange = vi.fn();
|
|
55
|
-
render(<UserFilters config={dropdownConfig} onFilterChange={onChange} />);
|
|
56
|
-
|
|
57
|
-
fireEvent.click(screen.getByTestId('filter-badge-status'));
|
|
58
|
-
expect(screen.getByTestId('filter-options-status')).toBeInTheDocument();
|
|
59
|
-
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
60
|
-
expect(screen.getByText('Inactive')).toBeInTheDocument();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('selects option and emits filter change', () => {
|
|
64
|
-
const onChange = vi.fn();
|
|
65
|
-
render(<UserFilters config={dropdownConfig} onFilterChange={onChange} />);
|
|
66
|
-
|
|
67
|
-
fireEvent.click(screen.getByTestId('filter-badge-status'));
|
|
68
|
-
fireEvent.click(screen.getByText('Active'));
|
|
69
|
-
|
|
70
|
-
expect(onChange).toHaveBeenCalledWith([['status', 'in', ['active']]]);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('supports multi-select — selecting multiple options', () => {
|
|
74
|
-
const onChange = vi.fn();
|
|
75
|
-
render(<UserFilters config={dropdownConfig} onFilterChange={onChange} />);
|
|
76
|
-
|
|
77
|
-
fireEvent.click(screen.getByTestId('filter-badge-status'));
|
|
78
|
-
fireEvent.click(screen.getByText('Active'));
|
|
79
|
-
fireEvent.click(screen.getByText('Inactive'));
|
|
80
|
-
|
|
81
|
-
expect(onChange).toHaveBeenLastCalledWith([['status', 'in', ['active', 'inactive']]]);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('shows count badge when options are selected', () => {
|
|
85
|
-
const onChange = vi.fn();
|
|
86
|
-
render(<UserFilters config={dropdownConfig} onFilterChange={onChange} />);
|
|
87
|
-
|
|
88
|
-
fireEvent.click(screen.getByTestId('filter-badge-status'));
|
|
89
|
-
fireEvent.click(screen.getByText('Active'));
|
|
90
|
-
|
|
91
|
-
// Count badge should show "1"
|
|
92
|
-
const badge = screen.getByTestId('filter-badge-status');
|
|
93
|
-
expect(badge.textContent).toContain('1');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('clears filter when X is clicked', () => {
|
|
97
|
-
const onChange = vi.fn();
|
|
98
|
-
render(<UserFilters config={dropdownConfig} onFilterChange={onChange} />);
|
|
99
|
-
|
|
100
|
-
// Select an option first
|
|
101
|
-
fireEvent.click(screen.getByTestId('filter-badge-status'));
|
|
102
|
-
fireEvent.click(screen.getByText('Active'));
|
|
103
|
-
|
|
104
|
-
// Click the clear button
|
|
105
|
-
fireEvent.click(screen.getByTestId('filter-clear-status'));
|
|
106
|
-
expect(onChange).toHaveBeenLastCalledWith([]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('shows record count per option when showCount is true', () => {
|
|
110
|
-
const config = {
|
|
111
|
-
element: 'dropdown' as const,
|
|
112
|
-
fields: [{
|
|
113
|
-
field: 'status',
|
|
114
|
-
label: 'Status',
|
|
115
|
-
showCount: true,
|
|
116
|
-
options: [
|
|
117
|
-
{ label: 'Active', value: 'active' },
|
|
118
|
-
{ label: 'Inactive', value: 'inactive' },
|
|
119
|
-
],
|
|
120
|
-
}],
|
|
121
|
-
};
|
|
122
|
-
const data = [
|
|
123
|
-
{ status: 'active' },
|
|
124
|
-
{ status: 'active' },
|
|
125
|
-
{ status: 'inactive' },
|
|
126
|
-
];
|
|
127
|
-
const onChange = vi.fn();
|
|
128
|
-
render(<UserFilters config={config} data={data} onFilterChange={onChange} />);
|
|
129
|
-
|
|
130
|
-
fireEvent.click(screen.getByTestId('filter-badge-status'));
|
|
131
|
-
// The options list should show counts
|
|
132
|
-
const optionsContainer = screen.getByTestId('filter-options-status');
|
|
133
|
-
expect(optionsContainer.textContent).toContain('2');
|
|
134
|
-
expect(optionsContainer.textContent).toContain('1');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('renders color dots for options with color', () => {
|
|
138
|
-
const onChange = vi.fn();
|
|
139
|
-
render(<UserFilters config={dropdownConfig} onFilterChange={onChange} />);
|
|
140
|
-
|
|
141
|
-
fireEvent.click(screen.getByTestId('filter-badge-priority'));
|
|
142
|
-
// Color dots should be rendered as span elements
|
|
143
|
-
const optionsContainer = screen.getByTestId('filter-options-priority');
|
|
144
|
-
const colorDots = optionsContainer.querySelectorAll('span[style]');
|
|
145
|
-
expect(colorDots.length).toBeGreaterThanOrEqual(2);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('auto-derives options from objectDef when not provided', () => {
|
|
149
|
-
const config = {
|
|
150
|
-
element: 'dropdown' as const,
|
|
151
|
-
fields: [{ field: 'status', label: 'Status' }],
|
|
152
|
-
};
|
|
153
|
-
const objectDef = {
|
|
154
|
-
fields: [
|
|
155
|
-
{
|
|
156
|
-
name: 'status',
|
|
157
|
-
options: [
|
|
158
|
-
{ label: 'Open', value: 'open' },
|
|
159
|
-
{ label: 'Closed', value: 'closed' },
|
|
160
|
-
],
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
};
|
|
164
|
-
const onChange = vi.fn();
|
|
165
|
-
render(<UserFilters config={config} objectDef={objectDef} onFilterChange={onChange} />);
|
|
166
|
-
|
|
167
|
-
fireEvent.click(screen.getByTestId('filter-badge-status'));
|
|
168
|
-
expect(screen.getByText('Open')).toBeInTheDocument();
|
|
169
|
-
expect(screen.getByText('Closed')).toBeInTheDocument();
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('applies defaultValues on mount', () => {
|
|
173
|
-
const config = {
|
|
174
|
-
element: 'dropdown' as const,
|
|
175
|
-
fields: [{
|
|
176
|
-
field: 'status',
|
|
177
|
-
label: 'Status',
|
|
178
|
-
options: [
|
|
179
|
-
{ label: 'Active', value: 'active' },
|
|
180
|
-
{ label: 'Inactive', value: 'inactive' },
|
|
181
|
-
],
|
|
182
|
-
defaultValues: ['active'] as (string | number | boolean)[],
|
|
183
|
-
}],
|
|
184
|
-
};
|
|
185
|
-
const onChange = vi.fn();
|
|
186
|
-
render(<UserFilters config={config} onFilterChange={onChange} />);
|
|
187
|
-
|
|
188
|
-
// Should emit default filter on mount
|
|
189
|
-
expect(onChange).toHaveBeenCalledWith([['status', 'in', ['active']]]);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// ============================================
|
|
194
|
-
// Tabs Mode
|
|
195
|
-
// ============================================
|
|
196
|
-
describe('Tabs mode', () => {
|
|
197
|
-
const tabsConfig = {
|
|
198
|
-
element: 'tabs' as const,
|
|
199
|
-
showAllRecords: true,
|
|
200
|
-
allowAddTab: true,
|
|
201
|
-
tabs: [
|
|
202
|
-
{ id: 'tab-1', label: 'Active', filters: [['status', '=', 'active']], default: true },
|
|
203
|
-
{ id: 'tab-2', label: 'My Items', filters: [['owner', '=', '$currentUser']] },
|
|
204
|
-
],
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
it('renders tab bar with tab labels', () => {
|
|
208
|
-
const onChange = vi.fn();
|
|
209
|
-
render(<UserFilters config={tabsConfig} onFilterChange={onChange} />);
|
|
210
|
-
|
|
211
|
-
expect(screen.getByTestId('user-filters-tabs')).toBeInTheDocument();
|
|
212
|
-
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
213
|
-
expect(screen.getByText('My Items')).toBeInTheDocument();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('renders "All records" tab when showAllRecords is true', () => {
|
|
217
|
-
const onChange = vi.fn();
|
|
218
|
-
render(<UserFilters config={tabsConfig} onFilterChange={onChange} />);
|
|
219
|
-
|
|
220
|
-
expect(screen.getByText('All records')).toBeInTheDocument();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('renders add tab button when allowAddTab is true', () => {
|
|
224
|
-
const onChange = vi.fn();
|
|
225
|
-
render(<UserFilters config={tabsConfig} onFilterChange={onChange} />);
|
|
226
|
-
|
|
227
|
-
expect(screen.getByTestId('filter-tab-add')).toBeInTheDocument();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('switches filters on tab click', () => {
|
|
231
|
-
const onChange = vi.fn();
|
|
232
|
-
render(<UserFilters config={tabsConfig} onFilterChange={onChange} />);
|
|
233
|
-
|
|
234
|
-
fireEvent.click(screen.getByText('My Items'));
|
|
235
|
-
expect(onChange).toHaveBeenCalledWith([['owner', '=', '$currentUser']]);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('clears filters when "All records" tab is clicked', () => {
|
|
239
|
-
const onChange = vi.fn();
|
|
240
|
-
render(<UserFilters config={tabsConfig} onFilterChange={onChange} />);
|
|
241
|
-
|
|
242
|
-
fireEvent.click(screen.getByText('All records'));
|
|
243
|
-
expect(onChange).toHaveBeenCalledWith([]);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('emits default tab filters on mount', () => {
|
|
247
|
-
const onChange = vi.fn();
|
|
248
|
-
render(<UserFilters config={tabsConfig} onFilterChange={onChange} />);
|
|
249
|
-
|
|
250
|
-
// Default tab (tab-1) should emit its filters on mount
|
|
251
|
-
expect(onChange).toHaveBeenCalledWith([['status', '=', 'active']]);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('hides "All records" tab when showAllRecords is false', () => {
|
|
255
|
-
const config = {
|
|
256
|
-
...tabsConfig,
|
|
257
|
-
showAllRecords: false,
|
|
258
|
-
};
|
|
259
|
-
const onChange = vi.fn();
|
|
260
|
-
render(<UserFilters config={config} onFilterChange={onChange} />);
|
|
261
|
-
|
|
262
|
-
expect(screen.queryByText('All records')).not.toBeInTheDocument();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('hides add button when allowAddTab is not set', () => {
|
|
266
|
-
const config = {
|
|
267
|
-
...tabsConfig,
|
|
268
|
-
allowAddTab: false,
|
|
269
|
-
};
|
|
270
|
-
const onChange = vi.fn();
|
|
271
|
-
render(<UserFilters config={config} onFilterChange={onChange} />);
|
|
272
|
-
|
|
273
|
-
expect(screen.queryByTestId('filter-tab-add')).not.toBeInTheDocument();
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// ============================================
|
|
278
|
-
// Toggle Mode
|
|
279
|
-
// ============================================
|
|
280
|
-
describe('Toggle mode', () => {
|
|
281
|
-
const toggleConfig = {
|
|
282
|
-
element: 'toggle' as const,
|
|
283
|
-
fields: [
|
|
284
|
-
{ field: 'is_active', label: 'Active Only' },
|
|
285
|
-
{ field: 'is_vip', label: 'VIP' },
|
|
286
|
-
],
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
it('renders toggle buttons with labels', () => {
|
|
290
|
-
const onChange = vi.fn();
|
|
291
|
-
render(<UserFilters config={toggleConfig} onFilterChange={onChange} />);
|
|
292
|
-
|
|
293
|
-
expect(screen.getByTestId('user-filters-toggle')).toBeInTheDocument();
|
|
294
|
-
expect(screen.getByTestId('filter-toggle-is_active')).toBeInTheDocument();
|
|
295
|
-
expect(screen.getByTestId('filter-toggle-is_vip')).toBeInTheDocument();
|
|
296
|
-
expect(screen.getByText('Active Only')).toBeInTheDocument();
|
|
297
|
-
expect(screen.getByText('VIP')).toBeInTheDocument();
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('toggles button active state on click', () => {
|
|
301
|
-
const onChange = vi.fn();
|
|
302
|
-
render(<UserFilters config={toggleConfig} onFilterChange={onChange} />);
|
|
303
|
-
|
|
304
|
-
fireEvent.click(screen.getByText('Active Only'));
|
|
305
|
-
expect(onChange).toHaveBeenCalledWith([['is_active', '!=', null]]);
|
|
306
|
-
|
|
307
|
-
// Click again to deactivate
|
|
308
|
-
fireEvent.click(screen.getByText('Active Only'));
|
|
309
|
-
expect(onChange).toHaveBeenLastCalledWith([]);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('supports multiple active toggles', () => {
|
|
313
|
-
const onChange = vi.fn();
|
|
314
|
-
render(<UserFilters config={toggleConfig} onFilterChange={onChange} />);
|
|
315
|
-
|
|
316
|
-
fireEvent.click(screen.getByText('Active Only'));
|
|
317
|
-
fireEvent.click(screen.getByText('VIP'));
|
|
318
|
-
|
|
319
|
-
// Both should be active, producing two filter conditions
|
|
320
|
-
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
|
|
321
|
-
expect(lastCall).toHaveLength(2);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('uses defaultValues for filter condition when provided', () => {
|
|
325
|
-
const config = {
|
|
326
|
-
element: 'toggle' as const,
|
|
327
|
-
fields: [
|
|
328
|
-
{
|
|
329
|
-
field: 'status',
|
|
330
|
-
label: 'Active',
|
|
331
|
-
defaultValues: ['active', 'pending'] as (string | number | boolean)[],
|
|
332
|
-
},
|
|
333
|
-
],
|
|
334
|
-
};
|
|
335
|
-
const onChange = vi.fn();
|
|
336
|
-
render(<UserFilters config={config} onFilterChange={onChange} />);
|
|
337
|
-
|
|
338
|
-
// Should emit default filter on mount
|
|
339
|
-
expect(onChange).toHaveBeenCalledWith([['status', 'in', ['active', 'pending']]]);
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// ============================================
|
|
344
|
-
// Edge Cases
|
|
345
|
-
// ============================================
|
|
346
|
-
describe('Edge cases', () => {
|
|
347
|
-
it('returns null for unknown element type', () => {
|
|
348
|
-
const config = { element: 'unknown' as any };
|
|
349
|
-
const onChange = vi.fn();
|
|
350
|
-
const { container } = render(<UserFilters config={config} onFilterChange={onChange} />);
|
|
351
|
-
expect(container.innerHTML).toBe('');
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('renders empty dropdown with placeholder when no fields provided', () => {
|
|
355
|
-
const config = { element: 'dropdown' as const };
|
|
356
|
-
const onChange = vi.fn();
|
|
357
|
-
render(<UserFilters config={config} onFilterChange={onChange} />);
|
|
358
|
-
expect(screen.getByTestId('user-filters-dropdown')).toBeInTheDocument();
|
|
359
|
-
expect(screen.getByTestId('user-filters-empty')).toBeInTheDocument();
|
|
360
|
-
expect(screen.getByText('No filter fields')).toBeInTheDocument();
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it('renders empty tabs when no tabs provided', () => {
|
|
364
|
-
const config = { element: 'tabs' as const, showAllRecords: false };
|
|
365
|
-
const onChange = vi.fn();
|
|
366
|
-
render(<UserFilters config={config} onFilterChange={onChange} />);
|
|
367
|
-
expect(screen.getByTestId('user-filters-tabs')).toBeInTheDocument();
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// ============================================
|
|
372
|
-
// Add Filter Entry Point (removed)
|
|
373
|
-
// ============================================
|
|
374
|
-
describe('Add filter entry', () => {
|
|
375
|
-
it('does not render "Add filter" button (removed from UI)', () => {
|
|
376
|
-
const config = {
|
|
377
|
-
element: 'dropdown' as const,
|
|
378
|
-
fields: [
|
|
379
|
-
{
|
|
380
|
-
field: 'status',
|
|
381
|
-
label: 'Status',
|
|
382
|
-
options: [
|
|
383
|
-
{ label: 'Active', value: 'active' },
|
|
384
|
-
],
|
|
385
|
-
},
|
|
386
|
-
],
|
|
387
|
-
};
|
|
388
|
-
const onChange = vi.fn();
|
|
389
|
-
render(<UserFilters config={config} onFilterChange={onChange} />);
|
|
390
|
-
expect(screen.queryByTestId('user-filters-add')).not.toBeInTheDocument();
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
// ============================================
|
|
395
|
-
// maxVisible Collapse Behavior (Dropdown)
|
|
396
|
-
// ============================================
|
|
397
|
-
describe('maxVisible collapse behavior', () => {
|
|
398
|
-
const manyFieldsConfig = {
|
|
399
|
-
element: 'dropdown' as const,
|
|
400
|
-
fields: [
|
|
401
|
-
{ field: 'status', label: 'Status', options: [{ label: 'Active', value: 'active' }] },
|
|
402
|
-
{ field: 'priority', label: 'Priority', options: [{ label: 'High', value: 'high' }] },
|
|
403
|
-
{ field: 'region', label: 'Region', options: [{ label: 'US', value: 'us' }] },
|
|
404
|
-
{ field: 'owner', label: 'Owner', options: [{ label: 'Alice', value: 'alice' }] },
|
|
405
|
-
{ field: 'type', label: 'Type', options: [{ label: 'Bug', value: 'bug' }] },
|
|
406
|
-
],
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
it('shows all badges when maxVisible is not set', () => {
|
|
410
|
-
const onChange = vi.fn();
|
|
411
|
-
render(<UserFilters config={manyFieldsConfig} onFilterChange={onChange} />);
|
|
412
|
-
|
|
413
|
-
expect(screen.getByTestId('filter-badge-status')).toBeInTheDocument();
|
|
414
|
-
expect(screen.getByTestId('filter-badge-priority')).toBeInTheDocument();
|
|
415
|
-
expect(screen.getByTestId('filter-badge-region')).toBeInTheDocument();
|
|
416
|
-
expect(screen.getByTestId('filter-badge-owner')).toBeInTheDocument();
|
|
417
|
-
expect(screen.getByTestId('filter-badge-type')).toBeInTheDocument();
|
|
418
|
-
expect(screen.queryByTestId('user-filters-more')).not.toBeInTheDocument();
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
it('shows only maxVisible badges and a "More" button when maxVisible < total fields', () => {
|
|
422
|
-
const onChange = vi.fn();
|
|
423
|
-
render(<UserFilters config={manyFieldsConfig} onFilterChange={onChange} maxVisible={2} />);
|
|
424
|
-
|
|
425
|
-
// First 2 should be visible
|
|
426
|
-
expect(screen.getByTestId('filter-badge-status')).toBeInTheDocument();
|
|
427
|
-
expect(screen.getByTestId('filter-badge-priority')).toBeInTheDocument();
|
|
428
|
-
// Remaining 3 should NOT be directly visible
|
|
429
|
-
expect(screen.queryByTestId('filter-badge-region')).not.toBeInTheDocument();
|
|
430
|
-
expect(screen.queryByTestId('filter-badge-owner')).not.toBeInTheDocument();
|
|
431
|
-
expect(screen.queryByTestId('filter-badge-type')).not.toBeInTheDocument();
|
|
432
|
-
// "More" button should show with count
|
|
433
|
-
const moreBtn = screen.getByTestId('user-filters-more');
|
|
434
|
-
expect(moreBtn).toBeInTheDocument();
|
|
435
|
-
expect(moreBtn.textContent).toContain('3');
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('shows overflow badges inside "More" popover when clicked', () => {
|
|
439
|
-
const onChange = vi.fn();
|
|
440
|
-
render(<UserFilters config={manyFieldsConfig} onFilterChange={onChange} maxVisible={2} />);
|
|
441
|
-
|
|
442
|
-
// Click "More" button
|
|
443
|
-
fireEvent.click(screen.getByTestId('user-filters-more'));
|
|
444
|
-
|
|
445
|
-
// Overflow badges should now be visible in the popover
|
|
446
|
-
expect(screen.getByTestId('user-filters-more-content')).toBeInTheDocument();
|
|
447
|
-
expect(screen.getByTestId('filter-badge-region')).toBeInTheDocument();
|
|
448
|
-
expect(screen.getByTestId('filter-badge-owner')).toBeInTheDocument();
|
|
449
|
-
expect(screen.getByTestId('filter-badge-type')).toBeInTheDocument();
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it('shows all badges when maxVisible >= total fields', () => {
|
|
453
|
-
const onChange = vi.fn();
|
|
454
|
-
render(<UserFilters config={manyFieldsConfig} onFilterChange={onChange} maxVisible={10} />);
|
|
455
|
-
|
|
456
|
-
expect(screen.getByTestId('filter-badge-status')).toBeInTheDocument();
|
|
457
|
-
expect(screen.getByTestId('filter-badge-type')).toBeInTheDocument();
|
|
458
|
-
expect(screen.queryByTestId('user-filters-more')).not.toBeInTheDocument();
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it('shows "More" button with maxVisible=0 and all fields overflow', () => {
|
|
462
|
-
const onChange = vi.fn();
|
|
463
|
-
render(<UserFilters config={manyFieldsConfig} onFilterChange={onChange} maxVisible={0} />);
|
|
464
|
-
|
|
465
|
-
// No direct badges
|
|
466
|
-
expect(screen.queryByTestId('filter-badge-status')).not.toBeInTheDocument();
|
|
467
|
-
// "More" button with all 5 overflow
|
|
468
|
-
const moreBtn = screen.getByTestId('user-filters-more');
|
|
469
|
-
expect(moreBtn.textContent).toContain('5');
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it('does not show "More" button for tabs mode even when maxVisible is set', () => {
|
|
473
|
-
const tabsConfig = {
|
|
474
|
-
element: 'tabs' as const,
|
|
475
|
-
tabs: [
|
|
476
|
-
{ id: 'tab-1', label: 'Active', filters: [] },
|
|
477
|
-
{ id: 'tab-2', label: 'Closed', filters: [] },
|
|
478
|
-
],
|
|
479
|
-
};
|
|
480
|
-
const onChange = vi.fn();
|
|
481
|
-
render(<UserFilters config={tabsConfig} onFilterChange={onChange} maxVisible={1} />);
|
|
482
|
-
|
|
483
|
-
expect(screen.queryByTestId('user-filters-more')).not.toBeInTheDocument();
|
|
484
|
-
});
|
|
485
|
-
});
|
|
486
|
-
});
|