@asteby/metacore-runtime-react 21.0.0 → 22.0.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 +59 -0
- package/dist/dynamic-columns-shim.d.ts +9 -1
- package/dist/dynamic-columns-shim.d.ts.map +1 -1
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +1 -0
- package/dist/dynamic-kanban.d.ts +16 -0
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +265 -11
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +51 -4
- package/dist/use-dynamic-filters.d.ts +19 -0
- package/dist/use-dynamic-filters.d.ts.map +1 -1
- package/dist/use-dynamic-filters.js +75 -5
- package/dist/use-facet-loaders.d.ts +22 -0
- package/dist/use-facet-loaders.d.ts.map +1 -0
- package/dist/use-facet-loaders.js +67 -0
- package/package.json +3 -3
- package/src/__tests__/dynamic-kanban.test.tsx +106 -5
- package/src/__tests__/use-dynamic-filters.test.tsx +182 -0
- package/src/dynamic-columns-shim.ts +9 -1
- package/src/dynamic-columns.tsx +1 -0
- package/src/dynamic-kanban.tsx +676 -36
- package/src/dynamic-table.tsx +52 -4
- package/src/use-dynamic-filters.ts +100 -5
- package/src/use-facet-loaders.ts +79 -0
package/src/dynamic-table.tsx
CHANGED
|
@@ -66,6 +66,7 @@ import { useMetadataCache } from './metadata-cache'
|
|
|
66
66
|
import { useApi, useCurrentBranch } from './api-context'
|
|
67
67
|
import type { ColumnFilterConfig, GetDynamicColumns } from './dynamic-columns-shim'
|
|
68
68
|
import { defaultGetDynamicColumns, DATE_CELL_TYPES, aggregateOf, formatAggregateTotal } from './dynamic-columns'
|
|
69
|
+
import { useFacetLoaders, isLongTextColumn } from './use-facet-loaders'
|
|
69
70
|
import { OptionsContext } from './options-context'
|
|
70
71
|
import type { TableMetadata, ApiResponse } from './types'
|
|
71
72
|
import { getSearchableColumnKeys } from './column-visibility'
|
|
@@ -530,15 +531,28 @@ export function DynamicTable({
|
|
|
530
531
|
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: 0 }))
|
|
531
532
|
}, [])
|
|
532
533
|
|
|
534
|
+
// Same facet loader machinery the board uses, so a text column filters
|
|
535
|
+
// identically in the table header and the kanban Sheet (the host's
|
|
536
|
+
// getDynamicColumns forwards `loadOptions` into the column meta). Facets base
|
|
537
|
+
// derived off the list endpoint exactly like the aggregate endpoint below.
|
|
538
|
+
const facetsBase = endpoint ? `${endpoint}/facets` : model ? `/data/${model}/facets` : null
|
|
539
|
+
const getFacetLoader = useFacetLoaders(facetsBase)
|
|
540
|
+
|
|
533
541
|
const columnFilterConfigs = useMemo(() => {
|
|
534
542
|
const map = new Map<string, ColumnFilterConfig>()
|
|
535
543
|
if (!metadata) return map
|
|
544
|
+
const stageOptions = (metadata.stages ?? []).map((s) => ({
|
|
545
|
+
label: s.label,
|
|
546
|
+
value: s.key,
|
|
547
|
+
color: s.color,
|
|
548
|
+
}))
|
|
549
|
+
const groupBy = metadata.group_by
|
|
536
550
|
// Explicit `metadata.filters` wins. When the backend does not emit
|
|
537
551
|
// them, derive a filter chip from every column flagged
|
|
538
552
|
// `filterable: true` — keeps the kernel API minimal (one flag on the
|
|
539
553
|
// column) while still rendering the FilterableColumnHeader.
|
|
540
554
|
for (const f of metadata.filters ?? []) {
|
|
541
|
-
|
|
555
|
+
let fType = f.type as ColumnFilterConfig['filterType']
|
|
542
556
|
let options: { label: string; value: string; icon?: string; color?: string }[] = []
|
|
543
557
|
if (f.options && f.options.length > 0) {
|
|
544
558
|
options = f.options.map(o => ({ label: o.label, value: String(o.value), icon: o.icon, color: o.color }))
|
|
@@ -546,6 +560,22 @@ export function DynamicTable({
|
|
|
546
560
|
if (f.searchEndpoint && filterOptionsMap.has(f.searchEndpoint)) {
|
|
547
561
|
options = filterOptionsMap.get(f.searchEndpoint) || []
|
|
548
562
|
}
|
|
563
|
+
// (A) A stage column with no options of its own inherits the
|
|
564
|
+
// pipeline stages (with their colors) as a real select.
|
|
565
|
+
if (options.length === 0 && !f.searchEndpoint && (f.column || f.key) === groupBy && stageOptions.length > 0) {
|
|
566
|
+
fType = 'select'
|
|
567
|
+
options = stageOptions
|
|
568
|
+
}
|
|
569
|
+
// (B) A plain text filter becomes a facet value-picker when a facets
|
|
570
|
+
// endpoint is available (degrades to "Contiene..." if it yields nothing).
|
|
571
|
+
let loadOptions: ColumnFilterConfig['loadOptions']
|
|
572
|
+
if (fType === 'text' && facetsBase) {
|
|
573
|
+
const loader = getFacetLoader(f.column || f.key)
|
|
574
|
+
if (loader) {
|
|
575
|
+
fType = 'facet'
|
|
576
|
+
loadOptions = loader
|
|
577
|
+
}
|
|
578
|
+
}
|
|
549
579
|
if (fType === 'select' && options.length === 0 && !f.searchEndpoint) continue
|
|
550
580
|
map.set(f.key, {
|
|
551
581
|
filterType: fType,
|
|
@@ -555,6 +585,7 @@ export function DynamicTable({
|
|
|
555
585
|
onFilterChange: handleDynamicFilterChange,
|
|
556
586
|
loading: f.searchEndpoint ? !filterOptionsMap.has(f.searchEndpoint) : false,
|
|
557
587
|
searchEndpoint: f.searchEndpoint,
|
|
588
|
+
loadOptions,
|
|
558
589
|
})
|
|
559
590
|
}
|
|
560
591
|
for (const c of metadata.columns ?? []) {
|
|
@@ -571,8 +602,11 @@ export function DynamicTable({
|
|
|
571
602
|
// - number / number_range / numeric → number range
|
|
572
603
|
// - date → date range picker (start/end calendar)
|
|
573
604
|
// - everything else (text, email, phone, tags…) → text contains
|
|
605
|
+
// (A) Stage column with no options of its own → colored select.
|
|
606
|
+
const isStageColumn = c.key === groupBy && !hasStaticOptions && !hasEndpoint && !c.filterType && stageOptions.length > 0
|
|
574
607
|
let filterType: ColumnFilterConfig['filterType']
|
|
575
|
-
if (
|
|
608
|
+
if (isStageColumn) filterType = 'select'
|
|
609
|
+
else if (c.filterType) filterType = c.filterType
|
|
576
610
|
else if (isRelation && hasEndpoint) filterType = 'dynamic_select'
|
|
577
611
|
else if (hasStaticOptions || hasEndpoint) filterType = 'select'
|
|
578
612
|
else if (c.type === 'boolean') filterType = 'boolean'
|
|
@@ -581,7 +615,7 @@ export function DynamicTable({
|
|
|
581
615
|
filterType = 'date_range'
|
|
582
616
|
else filterType = 'text'
|
|
583
617
|
|
|
584
|
-
|
|
618
|
+
let options = hasStaticOptions
|
|
585
619
|
? c.options!.map(o => ({
|
|
586
620
|
label: o.label,
|
|
587
621
|
value: String(o.value),
|
|
@@ -591,6 +625,19 @@ export function DynamicTable({
|
|
|
591
625
|
: hasEndpoint && filterOptionsMap.has(c.searchEndpoint!)
|
|
592
626
|
? filterOptionsMap.get(c.searchEndpoint!) || []
|
|
593
627
|
: []
|
|
628
|
+
if (isStageColumn) options = stageOptions
|
|
629
|
+
|
|
630
|
+
// (B) Upgrade a plain text filter to a facet value-picker unless it's
|
|
631
|
+
// a long-text/body column (too many unique values to enumerate).
|
|
632
|
+
let loadOptions: ColumnFilterConfig['loadOptions']
|
|
633
|
+
if (filterType === 'text' && facetsBase && !isLongTextColumn(c)) {
|
|
634
|
+
const loader = getFacetLoader(c.key)
|
|
635
|
+
if (loader) {
|
|
636
|
+
filterType = 'facet'
|
|
637
|
+
loadOptions = loader
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
594
641
|
map.set(c.key, {
|
|
595
642
|
filterType,
|
|
596
643
|
filterKey: c.key,
|
|
@@ -599,10 +646,11 @@ export function DynamicTable({
|
|
|
599
646
|
onFilterChange: handleDynamicFilterChange,
|
|
600
647
|
loading: hasEndpoint && !filterOptionsMap.has(c.searchEndpoint!),
|
|
601
648
|
searchEndpoint: c.searchEndpoint,
|
|
649
|
+
loadOptions,
|
|
602
650
|
})
|
|
603
651
|
}
|
|
604
652
|
return map
|
|
605
|
-
}, [metadata, filterOptionsMap, dynamicFilters, handleDynamicFilterChange])
|
|
653
|
+
}, [metadata, filterOptionsMap, dynamicFilters, handleDynamicFilterChange, facetsBase, getFacetLoader])
|
|
606
654
|
|
|
607
655
|
const columns = useMemo(() => {
|
|
608
656
|
if (!viewMetadata) return []
|
|
@@ -14,6 +14,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
14
14
|
import { useApi } from './api-context'
|
|
15
15
|
import { DATE_CELL_TYPES } from './dynamic-columns'
|
|
16
16
|
import { getSearchableColumnKeys } from './column-visibility'
|
|
17
|
+
import { useFacetLoaders, isLongTextColumn } from './use-facet-loaders'
|
|
17
18
|
import type { ColumnFilterConfig, FilterOption } from './dynamic-columns-shim'
|
|
18
19
|
import type { TableMetadata } from './types'
|
|
19
20
|
|
|
@@ -23,6 +24,25 @@ export interface UseDynamicFiltersOptions {
|
|
|
23
24
|
* Mirrors DynamicTable's `defaultFilters` — e.g. scoping a board to one owner.
|
|
24
25
|
*/
|
|
25
26
|
defaultFilters?: Record<string, any>
|
|
27
|
+
/**
|
|
28
|
+
* Model key — the fallback base for the per-field `facets` endpoint when no
|
|
29
|
+
* `endpoint` is given. Omit all of `endpoint`/`model`/`facetsEndpoint` to keep
|
|
30
|
+
* text filters as bare "Contiene..." boxes.
|
|
31
|
+
*/
|
|
32
|
+
model?: string
|
|
33
|
+
/**
|
|
34
|
+
* The org-scoped LIST endpoint (e.g. `/data/<model>/me`). The facets base is
|
|
35
|
+
* derived from it EXACTLY like DynamicTable derives the aggregate endpoint —
|
|
36
|
+
* `<endpoint>/facets` — so the board and its table sibling hit the same route
|
|
37
|
+
* (the backend registers both `/data/:model/facets` and
|
|
38
|
+
* `/data/:model/me/facets`).
|
|
39
|
+
*/
|
|
40
|
+
endpoint?: string
|
|
41
|
+
/**
|
|
42
|
+
* Explicit `facets` endpoint override — wins over `endpoint`/`model`. The
|
|
43
|
+
* loader appends `?field=<key>&q=<text>&limit=50`.
|
|
44
|
+
*/
|
|
45
|
+
facetsEndpoint?: string
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
export interface UseDynamicFiltersResult {
|
|
@@ -57,7 +77,7 @@ export function useDynamicFilters(
|
|
|
57
77
|
metadata: TableMetadata | null,
|
|
58
78
|
opts: UseDynamicFiltersOptions = {},
|
|
59
79
|
): UseDynamicFiltersResult {
|
|
60
|
-
const { defaultFilters } = opts
|
|
80
|
+
const { defaultFilters, model, endpoint, facetsEndpoint } = opts
|
|
61
81
|
const api = useApi()
|
|
62
82
|
|
|
63
83
|
const [dynamicFilters, setDynamicFilters] = useState<Record<string, string[]>>({})
|
|
@@ -66,6 +86,18 @@ export function useDynamicFilters(
|
|
|
66
86
|
Map<string, FilterOption[]>
|
|
67
87
|
>(new Map())
|
|
68
88
|
|
|
89
|
+
// Where a `facet` filter loads its distinct values from. Explicit override
|
|
90
|
+
// wins; otherwise `<endpoint>/facets` (same derivation as the aggregate
|
|
91
|
+
// endpoint), falling back to the model. Null → text filters stay plain.
|
|
92
|
+
const facetsBase =
|
|
93
|
+
facetsEndpoint ??
|
|
94
|
+
(endpoint
|
|
95
|
+
? `${endpoint}/facets`
|
|
96
|
+
: model
|
|
97
|
+
? `/data/${model}/facets`
|
|
98
|
+
: null)
|
|
99
|
+
const getFacetLoader = useFacetLoaders(facetsBase)
|
|
100
|
+
|
|
69
101
|
// Prefetch the option lists for relation/select filters (once per model). The
|
|
70
102
|
// combobox needs these before the user opens it; mirrors DynamicTable's
|
|
71
103
|
// metadata-init option prefetch, narrowed to the FILTER endpoints only (the
|
|
@@ -151,8 +183,18 @@ export function useDynamicFilters(
|
|
|
151
183
|
const columnFilterConfigs = useMemo(() => {
|
|
152
184
|
const map = new Map<string, ColumnFilterConfig>()
|
|
153
185
|
if (!metadata) return map
|
|
186
|
+
const stages = metadata.stages ?? []
|
|
187
|
+
const groupBy = metadata.group_by
|
|
188
|
+
// Stage options derived once from the pipeline machine, reused wherever a
|
|
189
|
+
// stage column has no inline options of its own (A).
|
|
190
|
+
const stageOptions: FilterOption[] = stages.map((s) => ({
|
|
191
|
+
label: s.label,
|
|
192
|
+
value: s.key,
|
|
193
|
+
color: s.color,
|
|
194
|
+
}))
|
|
195
|
+
|
|
154
196
|
for (const f of metadata.filters ?? []) {
|
|
155
|
-
|
|
197
|
+
let fType = f.type as ColumnFilterConfig['filterType']
|
|
156
198
|
let options: FilterOption[] = []
|
|
157
199
|
if (f.options && f.options.length > 0) {
|
|
158
200
|
options = f.options.map((o) => ({
|
|
@@ -165,6 +207,28 @@ export function useDynamicFilters(
|
|
|
165
207
|
if (f.searchEndpoint && filterOptionsMap.has(f.searchEndpoint)) {
|
|
166
208
|
options = filterOptionsMap.get(f.searchEndpoint) || []
|
|
167
209
|
}
|
|
210
|
+
// (A) Stage column with no options of its own → project the pipeline
|
|
211
|
+
// stages (with their colors) into a real select instead of leaving it as
|
|
212
|
+
// a text box.
|
|
213
|
+
if (
|
|
214
|
+
options.length === 0 &&
|
|
215
|
+
!f.searchEndpoint &&
|
|
216
|
+
(f.column || f.key) === groupBy &&
|
|
217
|
+
stageOptions.length > 0
|
|
218
|
+
) {
|
|
219
|
+
fType = 'select'
|
|
220
|
+
options = stageOptions
|
|
221
|
+
}
|
|
222
|
+
// (B) A plain text filter becomes a facet value-picker when a facets
|
|
223
|
+
// endpoint is available.
|
|
224
|
+
let loadOptions: ColumnFilterConfig['loadOptions']
|
|
225
|
+
if (fType === 'text' && facetsBase) {
|
|
226
|
+
const loader = getFacetLoader(f.column || f.key)
|
|
227
|
+
if (loader) {
|
|
228
|
+
fType = 'facet'
|
|
229
|
+
loadOptions = loader
|
|
230
|
+
}
|
|
231
|
+
}
|
|
168
232
|
if (fType === 'select' && options.length === 0 && !f.searchEndpoint) continue
|
|
169
233
|
map.set(f.key, {
|
|
170
234
|
filterType: fType,
|
|
@@ -174,6 +238,7 @@ export function useDynamicFilters(
|
|
|
174
238
|
onFilterChange: handleDynamicFilterChange,
|
|
175
239
|
loading: f.searchEndpoint ? !filterOptionsMap.has(f.searchEndpoint) : false,
|
|
176
240
|
searchEndpoint: f.searchEndpoint,
|
|
241
|
+
loadOptions,
|
|
177
242
|
})
|
|
178
243
|
}
|
|
179
244
|
for (const c of metadata.columns ?? []) {
|
|
@@ -181,8 +246,17 @@ export function useDynamicFilters(
|
|
|
181
246
|
const hasStaticOptions = (c.options?.length ?? 0) > 0
|
|
182
247
|
const hasEndpoint = !!c.searchEndpoint
|
|
183
248
|
const isRelation = !!c.ref || c.filterType === 'dynamic_select'
|
|
249
|
+
// (A) A stage column with no options of its own inherits the pipeline's
|
|
250
|
+
// stages — so "Stage" becomes a colored select, not a text box.
|
|
251
|
+
const isStageColumn =
|
|
252
|
+
c.key === groupBy &&
|
|
253
|
+
!hasStaticOptions &&
|
|
254
|
+
!hasEndpoint &&
|
|
255
|
+
!c.filterType &&
|
|
256
|
+
stageOptions.length > 0
|
|
184
257
|
let filterType: ColumnFilterConfig['filterType']
|
|
185
|
-
if (
|
|
258
|
+
if (isStageColumn) filterType = 'select'
|
|
259
|
+
else if (c.filterType) filterType = c.filterType
|
|
186
260
|
else if (isRelation && hasEndpoint) filterType = 'dynamic_select'
|
|
187
261
|
else if (hasStaticOptions || hasEndpoint) filterType = 'select'
|
|
188
262
|
else if (c.type === 'boolean') filterType = 'boolean'
|
|
@@ -191,7 +265,7 @@ export function useDynamicFilters(
|
|
|
191
265
|
filterType = 'date_range'
|
|
192
266
|
else filterType = 'text'
|
|
193
267
|
|
|
194
|
-
|
|
268
|
+
let options = hasStaticOptions
|
|
195
269
|
? c.options!.map((o) => ({
|
|
196
270
|
label: o.label,
|
|
197
271
|
value: String(o.value),
|
|
@@ -201,6 +275,19 @@ export function useDynamicFilters(
|
|
|
201
275
|
: hasEndpoint && filterOptionsMap.has(c.searchEndpoint!)
|
|
202
276
|
? filterOptionsMap.get(c.searchEndpoint!) || []
|
|
203
277
|
: []
|
|
278
|
+
if (isStageColumn) options = stageOptions
|
|
279
|
+
|
|
280
|
+
// (B) Upgrade a plain text filter to a facet value-picker (unless it's a
|
|
281
|
+
// long-text/body column — too many unique values to enumerate).
|
|
282
|
+
let loadOptions: ColumnFilterConfig['loadOptions']
|
|
283
|
+
if (filterType === 'text' && facetsBase && !isLongTextColumn(c)) {
|
|
284
|
+
const loader = getFacetLoader(c.key)
|
|
285
|
+
if (loader) {
|
|
286
|
+
filterType = 'facet'
|
|
287
|
+
loadOptions = loader
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
204
291
|
map.set(c.key, {
|
|
205
292
|
filterType,
|
|
206
293
|
filterKey: c.key,
|
|
@@ -209,10 +296,18 @@ export function useDynamicFilters(
|
|
|
209
296
|
onFilterChange: handleDynamicFilterChange,
|
|
210
297
|
loading: hasEndpoint && !filterOptionsMap.has(c.searchEndpoint!),
|
|
211
298
|
searchEndpoint: c.searchEndpoint,
|
|
299
|
+
loadOptions,
|
|
212
300
|
})
|
|
213
301
|
}
|
|
214
302
|
return map
|
|
215
|
-
}, [
|
|
303
|
+
}, [
|
|
304
|
+
metadata,
|
|
305
|
+
filterOptionsMap,
|
|
306
|
+
dynamicFilters,
|
|
307
|
+
handleDynamicFilterChange,
|
|
308
|
+
facetsBase,
|
|
309
|
+
getFacetLoader,
|
|
310
|
+
])
|
|
216
311
|
|
|
217
312
|
const searchableKeys = useMemo(
|
|
218
313
|
() => (metadata ? getSearchableColumnKeys(metadata) : null),
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// useFacetLoaders — the shared machinery that turns a plain text filter into a
|
|
2
|
+
// `facet` value-picker. Both DynamicKanban (via useDynamicFilters) and
|
|
3
|
+
// DynamicTable derive their filter configs independently, so the lazy
|
|
4
|
+
// per-field option loader + result cache lives here to guarantee they facet
|
|
5
|
+
// identically (one board and its table sibling hit the same `/facets` endpoint
|
|
6
|
+
// with the same caching).
|
|
7
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
8
|
+
import { useApi } from './api-context'
|
|
9
|
+
import type { FilterOption } from './dynamic-columns-shim'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Long-text / body columns aren't worth faceting (thousands of unique values,
|
|
13
|
+
* each a paragraph). Heuristic on the metadata, not a hardcoded name list:
|
|
14
|
+
* a truncate-text/widget cell or a json/long-text SQL type are the strong
|
|
15
|
+
* signals; the name pattern is only a last-resort fallback.
|
|
16
|
+
*/
|
|
17
|
+
export function isLongTextColumn(c: {
|
|
18
|
+
key: string
|
|
19
|
+
type?: string
|
|
20
|
+
cellStyle?: string
|
|
21
|
+
}): boolean {
|
|
22
|
+
if (c.cellStyle === 'truncate-text' || c.cellStyle === 'widget') return true
|
|
23
|
+
const t = (c.type || '').toLowerCase()
|
|
24
|
+
if (t === 'json' || t === 'long_text' || t === 'text_long' || t === 'widget')
|
|
25
|
+
return true
|
|
26
|
+
return /^(body|description|content|notes?|comment|message|summary)$/i.test(
|
|
27
|
+
c.key,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns `getFacetLoader(field)` — a stable per-field loader that resolves the
|
|
33
|
+
* column's distinct values + counts from `<facetsBase>?field=&q=&limit=`. Loader
|
|
34
|
+
* identity is memoized per field (so it doesn't churn the config memo that
|
|
35
|
+
* depends on it) and results are cached per `field+query`. Both caches reset
|
|
36
|
+
* when `facetsBase` changes (model switch). A null `facetsBase` returns a
|
|
37
|
+
* factory that yields `undefined` — the caller keeps the column as plain text.
|
|
38
|
+
*/
|
|
39
|
+
export function useFacetLoaders(facetsBase: string | null) {
|
|
40
|
+
const api = useApi()
|
|
41
|
+
const loaderCache = useRef<
|
|
42
|
+
Map<string, (q?: string) => Promise<FilterOption[]>>
|
|
43
|
+
>(new Map())
|
|
44
|
+
const resultCache = useRef<Map<string, FilterOption[]>>(new Map())
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
loaderCache.current.clear()
|
|
48
|
+
resultCache.current.clear()
|
|
49
|
+
}, [facetsBase])
|
|
50
|
+
|
|
51
|
+
return useCallback(
|
|
52
|
+
(field: string) => {
|
|
53
|
+
if (!facetsBase) return undefined
|
|
54
|
+
const existing = loaderCache.current.get(field)
|
|
55
|
+
if (existing) return existing
|
|
56
|
+
const loader = async (q?: string): Promise<FilterOption[]> => {
|
|
57
|
+
const term = q?.trim() || ''
|
|
58
|
+
const cacheKey = `${field}::${term}`
|
|
59
|
+
const cached = resultCache.current.get(cacheKey)
|
|
60
|
+
if (cached) return cached
|
|
61
|
+
const res = await api.get(facetsBase, {
|
|
62
|
+
params: { field, q: term || undefined, limit: 50 },
|
|
63
|
+
})
|
|
64
|
+
const body = (res as { data: any }).data
|
|
65
|
+
const rows: any[] = body?.success ? body.data || [] : []
|
|
66
|
+
const opts: FilterOption[] = rows.map((r) => ({
|
|
67
|
+
value: String(r.value ?? ''),
|
|
68
|
+
label: String(r.label ?? r.value ?? ''),
|
|
69
|
+
count: typeof r.count === 'number' ? r.count : undefined,
|
|
70
|
+
}))
|
|
71
|
+
resultCache.current.set(cacheKey, opts)
|
|
72
|
+
return opts
|
|
73
|
+
}
|
|
74
|
+
loaderCache.current.set(field, loader)
|
|
75
|
+
return loader
|
|
76
|
+
},
|
|
77
|
+
[api, facetsBase],
|
|
78
|
+
)
|
|
79
|
+
}
|