@asteby/metacore-runtime-react 22.0.0 → 23.1.0
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 +62 -0
- package/dist/dynamic-kanban.d.ts +20 -8
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +103 -102
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +60 -7
- package/dist/filter-chips.d.ts +60 -0
- package/dist/filter-chips.d.ts.map +1 -0
- package/dist/filter-chips.js +93 -0
- package/dist/use-dynamic-filters.d.ts.map +1 -1
- package/dist/use-dynamic-filters.js +25 -1
- package/dist/use-facet-loaders.d.ts +23 -7
- package/dist/use-facet-loaders.d.ts.map +1 -1
- package/dist/use-facet-loaders.js +49 -8
- package/package.json +3 -3
- package/src/__tests__/dynamic-kanban.test.tsx +141 -0
- package/src/__tests__/dynamic-table-filters.test.tsx +102 -0
- package/src/__tests__/filter-chips.test.tsx +119 -0
- package/src/__tests__/use-dynamic-filters.test.tsx +65 -4
- package/src/dynamic-kanban.tsx +189 -199
- package/src/dynamic-table.tsx +69 -6
- package/src/filter-chips.tsx +175 -0
- package/src/use-dynamic-filters.ts +26 -1
- package/src/use-facet-loaders.ts +76 -9
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// FilterChipsRow — the removable active-filter chip row shared by DynamicTable
|
|
4
|
+
// and DynamicKanban: one chip per active field (label + summarized value + a
|
|
5
|
+
// value-color dot when the option carries a color), an X that clears that field,
|
|
6
|
+
// and a trailing "Limpiar todo".
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
import { cleanup, render, screen, fireEvent } from '@testing-library/react'
|
|
9
|
+
|
|
10
|
+
vi.mock('react-i18next', () => ({
|
|
11
|
+
useTranslation: () => ({
|
|
12
|
+
t: (_k: string, opts?: { defaultValue?: string }) => opts?.defaultValue ?? _k,
|
|
13
|
+
}),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
FilterChipsRow,
|
|
18
|
+
summarizeFilterValues,
|
|
19
|
+
chipValueColor,
|
|
20
|
+
type FilterChipField,
|
|
21
|
+
} from '../filter-chips'
|
|
22
|
+
|
|
23
|
+
afterEach(cleanup)
|
|
24
|
+
|
|
25
|
+
function field(over: Partial<FilterChipField> & { key: string }): FilterChipField {
|
|
26
|
+
return {
|
|
27
|
+
label: over.key,
|
|
28
|
+
config: {
|
|
29
|
+
selectedValues: [],
|
|
30
|
+
options: [],
|
|
31
|
+
filterKey: over.key,
|
|
32
|
+
onFilterChange: vi.fn(),
|
|
33
|
+
...(over.config as any),
|
|
34
|
+
},
|
|
35
|
+
...over,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('summarizeFilterValues / chipValueColor', () => {
|
|
40
|
+
const opts = [
|
|
41
|
+
{ value: 'backlog', label: 'Pendiente', color: 'slate' },
|
|
42
|
+
{ value: 'done', label: 'Hecho', color: 'green' },
|
|
43
|
+
]
|
|
44
|
+
it('summarizes a single value to its (translated) option label', () => {
|
|
45
|
+
expect(summarizeFilterValues(['done'], opts)).toBe('Hecho')
|
|
46
|
+
})
|
|
47
|
+
it('resolves the option color for the chip dot', () => {
|
|
48
|
+
expect(chipValueColor({ selectedValues: ['done'], options: opts })).toBeTruthy()
|
|
49
|
+
})
|
|
50
|
+
it('has no dot for a free-text (ILIKE) value', () => {
|
|
51
|
+
expect(
|
|
52
|
+
chipValueColor({ selectedValues: ['ILIKE:foo'], options: opts }),
|
|
53
|
+
).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('FilterChipsRow', () => {
|
|
58
|
+
const stageOptions = [
|
|
59
|
+
{ value: 'backlog', label: 'Pendiente', color: 'slate' },
|
|
60
|
+
{ value: 'done', label: 'Hecho', color: 'green' },
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
it('renders nothing when there are no active fields', () => {
|
|
64
|
+
const { container } = render(
|
|
65
|
+
<FilterChipsRow fields={[]} onClearAll={() => {}} />,
|
|
66
|
+
)
|
|
67
|
+
expect(container.firstChild).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('renders a chip per active field with its translated value summary', () => {
|
|
71
|
+
render(
|
|
72
|
+
<FilterChipsRow
|
|
73
|
+
fields={[
|
|
74
|
+
field({
|
|
75
|
+
key: 'stage',
|
|
76
|
+
label: 'Etapa',
|
|
77
|
+
config: {
|
|
78
|
+
selectedValues: ['done'],
|
|
79
|
+
options: stageOptions,
|
|
80
|
+
filterKey: 'stage',
|
|
81
|
+
onFilterChange: vi.fn(),
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
]}
|
|
85
|
+
onClearAll={() => {}}
|
|
86
|
+
data-testid="chips"
|
|
87
|
+
/>,
|
|
88
|
+
)
|
|
89
|
+
expect(screen.getByText('Etapa:')).toBeTruthy()
|
|
90
|
+
// the value shows the TRANSLATED option label, not the raw key
|
|
91
|
+
expect(screen.getByText('Hecho')).toBeTruthy()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('clears one field via its X and everything via "Limpiar todo"', () => {
|
|
95
|
+
const onFilterChange = vi.fn()
|
|
96
|
+
const onClearAll = vi.fn()
|
|
97
|
+
render(
|
|
98
|
+
<FilterChipsRow
|
|
99
|
+
fields={[
|
|
100
|
+
field({
|
|
101
|
+
key: 'stage',
|
|
102
|
+
label: 'Etapa',
|
|
103
|
+
config: {
|
|
104
|
+
selectedValues: ['done'],
|
|
105
|
+
options: stageOptions,
|
|
106
|
+
filterKey: 'stage',
|
|
107
|
+
onFilterChange,
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
]}
|
|
111
|
+
onClearAll={onClearAll}
|
|
112
|
+
/>,
|
|
113
|
+
)
|
|
114
|
+
fireEvent.click(screen.getByLabelText('Quitar filtro'))
|
|
115
|
+
expect(onFilterChange).toHaveBeenCalledWith('stage', [])
|
|
116
|
+
fireEvent.click(screen.getByText('Limpiar todo'))
|
|
117
|
+
expect(onClearAll).toHaveBeenCalled()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -169,14 +169,75 @@ describe('useDynamicFilters — facet upgrade + fallback (B)', () => {
|
|
|
169
169
|
() => useDynamicFilters(meta(), { model: 'issue' }),
|
|
170
170
|
{ wrapper: wrapper(api) },
|
|
171
171
|
)
|
|
172
|
+
const titleFacetCalls = () =>
|
|
173
|
+
(api.get as any).mock.calls.filter(
|
|
174
|
+
(c: any[]) =>
|
|
175
|
+
String(c[0]).endsWith('/facets') &&
|
|
176
|
+
c[1]?.params?.field === 'title' &&
|
|
177
|
+
!c[1]?.params?.q,
|
|
178
|
+
).length
|
|
179
|
+
// Let the prewarm settle first, then reopening (empty query) is cached.
|
|
180
|
+
await waitFor(() => expect(titleFacetCalls()).toBe(1))
|
|
172
181
|
const load = result.current.columnFilterConfigs.get('title')!.loadOptions!
|
|
173
182
|
await load('')
|
|
174
183
|
await load('')
|
|
175
|
-
|
|
184
|
+
expect(titleFacetCalls()).toBe(1)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('useDynamicFilters — facet prefetch', () => {
|
|
189
|
+
it('prewarms facet options on mount so the config carries them (no lazy open)', async () => {
|
|
190
|
+
const api = fakeApi()
|
|
191
|
+
const { result } = renderHook(
|
|
192
|
+
() => useDynamicFilters(meta(), { model: 'issue' }),
|
|
193
|
+
{ wrapper: wrapper(api) },
|
|
194
|
+
)
|
|
195
|
+
// The prefetch burst fires an empty-query request per facet field.
|
|
196
|
+
await waitFor(() =>
|
|
197
|
+
expect(
|
|
198
|
+
(api.get as any).mock.calls.some(
|
|
199
|
+
(c: any[]) =>
|
|
200
|
+
String(c[0]) === '/data/issue/facets' &&
|
|
201
|
+
c[1]?.params?.field === 'title' &&
|
|
202
|
+
c[1]?.params?.q === undefined,
|
|
203
|
+
),
|
|
204
|
+
).toBe(true),
|
|
205
|
+
)
|
|
206
|
+
// …and those values land directly on the config (popover opens instantly).
|
|
207
|
+
await waitFor(() =>
|
|
208
|
+
expect(
|
|
209
|
+
result.current.columnFilterConfigs.get('title')?.options,
|
|
210
|
+
).toEqual([
|
|
211
|
+
{ value: 'ana', label: 'Ana', count: 3 },
|
|
212
|
+
{ value: 'beto', label: 'Beto', count: 1 },
|
|
213
|
+
]),
|
|
214
|
+
)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('opening a prewarmed facet is a cache hit (no extra request)', async () => {
|
|
218
|
+
const api = fakeApi()
|
|
219
|
+
const { result } = renderHook(
|
|
220
|
+
() => useDynamicFilters(meta(), { model: 'issue' }),
|
|
221
|
+
{ wrapper: wrapper(api) },
|
|
222
|
+
)
|
|
223
|
+
// wait for prefetch to settle
|
|
176
224
|
await waitFor(() =>
|
|
177
|
-
expect(
|
|
178
|
-
|
|
179
|
-
).
|
|
225
|
+
expect(
|
|
226
|
+
result.current.columnFilterConfigs.get('title')?.options?.length,
|
|
227
|
+
).toBe(2),
|
|
180
228
|
)
|
|
229
|
+
const before = (api.get as any).mock.calls.filter(
|
|
230
|
+
(c: any[]) =>
|
|
231
|
+
String(c[0]).endsWith('/facets') &&
|
|
232
|
+
c[1]?.params?.field === 'title',
|
|
233
|
+
).length
|
|
234
|
+
// the popover's empty-query load reuses the prewarmed cache
|
|
235
|
+
await result.current.columnFilterConfigs.get('title')!.loadOptions!('')
|
|
236
|
+
const after = (api.get as any).mock.calls.filter(
|
|
237
|
+
(c: any[]) =>
|
|
238
|
+
String(c[0]).endsWith('/facets') &&
|
|
239
|
+
c[1]?.params?.field === 'title',
|
|
240
|
+
).length
|
|
241
|
+
expect(after).toBe(before)
|
|
181
242
|
})
|
|
182
243
|
})
|