@asteby/metacore-runtime-react 20.1.7 → 21.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 +38 -0
- package/dist/dynamic-kanban.d.ts +6 -1
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +110 -17
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/use-dynamic-filters.d.ts +38 -0
- package/dist/use-dynamic-filters.d.ts.map +1 -0
- package/dist/use-dynamic-filters.js +226 -0
- package/package.json +3 -3
- package/src/__tests__/dynamic-kanban.test.tsx +57 -1
- package/src/dynamic-kanban.tsx +326 -14
- package/src/index.ts +5 -0
- package/src/use-dynamic-filters.ts +272 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 21.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2f21e1e: feat(kanban): agrupar filtros globales en un sheet lateral + filtro por columna
|
|
8
|
+
- **Filtros globales** dejan de spillear como chips inline: ahora un botón "Filtros"
|
|
9
|
+
(con contador de activos) abre un **sheet lateral** con todos los controles apilados
|
|
10
|
+
(mismo motor server-side `useDynamicFilters`). La búsqueda global queda inline.
|
|
11
|
+
- **Filtro por lane (stage):** cada columna tiene un icono de embudo → elige un campo
|
|
12
|
+
- un valor y **filtra solo las cards de ese stage** (client-side, instantáneo, sin
|
|
13
|
+
refetch). Un indicador bajo el header muestra el filtro activo (campo: valor) con
|
|
14
|
+
un botón para limpiarlo; el header muestra `filtradas/total`.
|
|
15
|
+
|
|
16
|
+
## 21.0.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 84aeaf2: feat(kanban): dynamic column filters on DynamicKanban
|
|
21
|
+
|
|
22
|
+
The board now filters like its DynamicTable sibling. A filter bar above the
|
|
23
|
+
lanes exposes a global search box plus one chip per filterable field (from
|
|
24
|
+
`metadata.filters[]` and every `filterable` column), driving the SAME
|
|
25
|
+
server-side `f_<key>=<op>:<value>` / `search` params — so a model's table and
|
|
26
|
+
kanban views filter identically.
|
|
27
|
+
- **ui**: extracted the per-column filter popover into a new, TanStack-agnostic
|
|
28
|
+
`ColumnFilterControl` (select/boolean/dynamic_select/text/number_range/
|
|
29
|
+
date_range). `FilterableColumnHeader` now delegates its filter UI to it
|
|
30
|
+
(behavior unchanged); it also powers the kanban's labeled filter chips.
|
|
31
|
+
- **runtime-react**: new `useDynamicFilters(metadata)` hook owning the filter
|
|
32
|
+
state, option prefetch, config derivation and param serialization (factored
|
|
33
|
+
out of DynamicTable's inline logic). `DynamicKanban` consumes it and gains a
|
|
34
|
+
`defaultFilters` prop for parity with DynamicTable.
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- Updated dependencies [84aeaf2]
|
|
39
|
+
- @asteby/metacore-ui@2.7.0
|
|
40
|
+
|
|
3
41
|
## 20.1.7
|
|
4
42
|
|
|
5
43
|
### Patch Changes
|
package/dist/dynamic-kanban.d.ts
CHANGED
|
@@ -68,6 +68,11 @@ export interface DynamicKanbanProps {
|
|
|
68
68
|
timeZone?: string;
|
|
69
69
|
/** ISO 4217 currency for money card fields (org config). */
|
|
70
70
|
currency?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Static equality filters always applied to the board (never shown as a
|
|
73
|
+
* removable chip). Same contract as DynamicTable's `defaultFilters`.
|
|
74
|
+
*/
|
|
75
|
+
defaultFilters?: Record<string, any>;
|
|
71
76
|
}
|
|
72
|
-
export declare function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, onAction, pageSize, timeZone, currency, }: DynamicKanbanProps): React.JSX.Element;
|
|
77
|
+
export declare function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, onAction, pageSize, timeZone, currency, defaultFilters, }: DynamicKanbanProps): React.JSX.Element;
|
|
73
78
|
//# sourceMappingURL=dynamic-kanban.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoD9B,OAAO,KAAK,EACR,aAAa,EACb,gBAAgB,EAGhB,SAAS,EACT,eAAe,EAClB,MAAM,SAAS,CAAA;AAMhB;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,SAAS,EAAE,CAejE;AAQD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,mBAAmB,CAAA;AAE/C,wBAAgB,YAAY,CACxB,OAAO,EAAE,GAAG,EAAE,EACd,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,EAAE,GACpB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAepB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,WAAW,EAAE,eAAe,EAAE,GAAG,SAAS,EAC1C,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACX,OAAO,CAOT;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,EAC3B,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACnB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAYpB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,aAAa,EACvB,SAAS,SAAI,GACd;IAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAkBhE;AA8BD,MAAM,WAAW,kBAAkB;IAC/B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACvC;AAED,wBAAgB,aAAa,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,WAAW,EACX,QAAQ,EACR,QAAc,EACd,QAAQ,EACR,QAAQ,EACR,cAAc,GACjB,EAAE,kBAAkB,qBAkcpB"}
|
package/dist/dynamic-kanban.js
CHANGED
|
@@ -2,11 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors, useDraggable, useDroppable, } from '@dnd-kit/core';
|
|
5
|
-
import { MoreHorizontal } from 'lucide-react';
|
|
5
|
+
import { ListFilter, MoreHorizontal, Search, X } from 'lucide-react';
|
|
6
6
|
import { toast } from 'sonner';
|
|
7
|
-
import { Badge, Button, Card, CardContent, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Skeleton, } from '@asteby/metacore-ui/primitives';
|
|
7
|
+
import { Badge, Button, Card, CardContent, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, Popover, PopoverContent, PopoverTrigger, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, Skeleton, } from '@asteby/metacore-ui/primitives';
|
|
8
|
+
import { ColumnFilterControl } from '@asteby/metacore-ui/data-table';
|
|
8
9
|
import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib';
|
|
9
10
|
import { useApi } from './api-context';
|
|
11
|
+
import { useDynamicFilters } from './use-dynamic-filters';
|
|
10
12
|
import { useMetadataCache } from './metadata-cache';
|
|
11
13
|
import { ActivityValueRenderer } from './activity-value-renderer';
|
|
12
14
|
import { DynamicIcon } from './dynamic-icon';
|
|
@@ -146,7 +148,7 @@ function useIsDarkTheme() {
|
|
|
146
148
|
}, []);
|
|
147
149
|
return isDark;
|
|
148
150
|
}
|
|
149
|
-
export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, onAction, pageSize = 200, timeZone, currency, }) {
|
|
151
|
+
export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, onAction, pageSize = 200, timeZone, currency, defaultFilters, }) {
|
|
150
152
|
const { t, i18n } = useTranslation();
|
|
151
153
|
const api = useApi();
|
|
152
154
|
const isDark = useIsDarkTheme();
|
|
@@ -194,6 +196,10 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
|
|
|
194
196
|
};
|
|
195
197
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
196
198
|
}, [model]);
|
|
199
|
+
// Shared metadata-driven filter engine — the SAME configs, option prefetch
|
|
200
|
+
// and `f_<key>` serialization DynamicTable uses, so the board filters
|
|
201
|
+
// identically to its table sibling.
|
|
202
|
+
const { globalFilter, setGlobalFilter, columnFilterConfigs, filterParams, activeFilterCount, clearAll, } = useDynamicFilters(metadata, { defaultFilters });
|
|
197
203
|
// ---- records fetch (same path as DynamicTable, single large page) ----
|
|
198
204
|
const fetchData = useCallback(async () => {
|
|
199
205
|
if (!metadata)
|
|
@@ -201,7 +207,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
|
|
|
201
207
|
setLoadingData(true);
|
|
202
208
|
try {
|
|
203
209
|
const res = (await api.get(endpoint || `/data/${model}`, {
|
|
204
|
-
params: { page: 1, per_page: pageSize },
|
|
210
|
+
params: { page: 1, per_page: pageSize, ...filterParams },
|
|
205
211
|
}));
|
|
206
212
|
if (res.data.success)
|
|
207
213
|
setRecords(res.data.data || []);
|
|
@@ -212,12 +218,50 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
|
|
|
212
218
|
finally {
|
|
213
219
|
setLoadingData(false);
|
|
214
220
|
}
|
|
215
|
-
}, [api, endpoint, model, metadata, pageSize]);
|
|
221
|
+
}, [api, endpoint, model, metadata, pageSize, filterParams]);
|
|
222
|
+
// Refetch when metadata resolves, on an explicit refresh, or when the
|
|
223
|
+
// filters change. `fetchData` is stable while `filterParams` is unchanged
|
|
224
|
+
// (both memoized), so this only re-runs on real input changes. Debounced so
|
|
225
|
+
// typing in the search box doesn't fire a request per keystroke.
|
|
216
226
|
useEffect(() => {
|
|
217
|
-
if (metadata)
|
|
227
|
+
if (!metadata)
|
|
228
|
+
return;
|
|
229
|
+
const handle = setTimeout(() => {
|
|
218
230
|
void fetchData();
|
|
219
|
-
|
|
220
|
-
|
|
231
|
+
}, 200);
|
|
232
|
+
return () => clearTimeout(handle);
|
|
233
|
+
}, [fetchData, metadata, refreshTrigger]);
|
|
234
|
+
// Filterable fields for the toolbar, in metadata order (explicit filters
|
|
235
|
+
// first, then filterable columns), each labeled from its metadata source.
|
|
236
|
+
const filterFields = useMemo(() => {
|
|
237
|
+
if (!metadata)
|
|
238
|
+
return [];
|
|
239
|
+
const out = [];
|
|
240
|
+
for (const [key, config] of columnFilterConfigs) {
|
|
241
|
+
const f = metadata.filters?.find((x) => x.key === key);
|
|
242
|
+
const c = metadata.columns.find((x) => x.key === key);
|
|
243
|
+
const rawLabel = f?.label || c?.label || key;
|
|
244
|
+
out.push({ key, label: t(rawLabel, { defaultValue: rawLabel }), config });
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}, [metadata, columnFilterConfigs, t]);
|
|
248
|
+
// Sheet (grouped global filters) open state + per-lane client-side filters.
|
|
249
|
+
// A lane filter narrows ONLY that stage's already-fetched cards by a field
|
|
250
|
+
// value — instant, no refetch — so a user can drill into one column without
|
|
251
|
+
// touching the rest of the board (the global filters, by contrast, refetch
|
|
252
|
+
// the whole board server-side).
|
|
253
|
+
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
254
|
+
const [laneFilters, setLaneFilters] = useState({});
|
|
255
|
+
const setLaneFilter = useCallback((stageKey, filter) => {
|
|
256
|
+
setLaneFilters((prev) => {
|
|
257
|
+
const next = { ...prev };
|
|
258
|
+
if (filter)
|
|
259
|
+
next[stageKey] = filter;
|
|
260
|
+
else
|
|
261
|
+
delete next[stageKey];
|
|
262
|
+
return next;
|
|
263
|
+
});
|
|
264
|
+
}, []);
|
|
221
265
|
const stages = useMemo(() => (metadata ? deriveStages(metadata) : []), [metadata]);
|
|
222
266
|
const groupByKey = metadata?.group_by || '';
|
|
223
267
|
const transitions = metadata?.transitions;
|
|
@@ -318,25 +362,74 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
|
|
|
318
362
|
order: Number.MAX_SAFE_INTEGER,
|
|
319
363
|
});
|
|
320
364
|
}
|
|
321
|
-
return (_jsxs(DndContext, { sensors: sensors, onDragStart: onDragStart, onDragEnd: onDragEnd, children: [_jsx("div", { className: "flex min-w-0 gap-4 overflow-x-auto p-1", "data-testid": "kanban-board", children: lanes.map((stage) => {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
365
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", "data-testid": "kanban-filters", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: globalFilter, onChange: (e) => setGlobalFilter(e.target.value), placeholder: t('kanban.searchPlaceholder', { defaultValue: 'Buscar...' }), className: "h-8 w-52 pl-8 text-sm" })] }), filterFields.length > 0 && (_jsxs(Sheet, { open: filtersOpen, onOpenChange: setFiltersOpen, children: [_jsx(SheetTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5", children: [_jsx(ListFilter, { className: "h-3.5 w-3.5" }), t('kanban.filters', { defaultValue: 'Filtros' }), activeFilterCount > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-0.5 h-4 min-w-4 justify-center rounded-full px-1 text-[10px] tabular-nums", children: activeFilterCount }))] }) }), _jsxs(SheetContent, { side: "right", className: "flex w-80 flex-col gap-4 sm:max-w-sm", children: [_jsx(SheetHeader, { className: "space-y-0", children: _jsx(SheetTitle, { children: t('kanban.filters', { defaultValue: 'Filtros' }) }) }), _jsx("div", { className: "flex flex-1 flex-col gap-2 overflow-y-auto pr-1", children: filterFields.map((field) => (_jsx(ColumnFilterControl, { showLabel: true, label: field.label, filterKey: field.config.filterKey, filterType: field.config.filterType, filterOptions: field.config.options, filterLoading: field.config.loading, filterSearchEndpoint: field.config.searchEndpoint, selectedValues: field.config.selectedValues, onFilterChange: field.config.onFilterChange }, field.key))) }), activeFilterCount > 0 && (_jsxs(Button, { variant: "ghost", size: "sm", className: "gap-1 text-xs text-muted-foreground", onClick: clearAll, children: [_jsx(X, { className: "h-3.5 w-3.5" }), t('kanban.clearFilters', { defaultValue: 'Limpiar' }), ` (${activeFilterCount})`] }))] })] }))] }), _jsxs(DndContext, { sensors: sensors, onDragStart: onDragStart, onDragEnd: onDragEnd, children: [_jsx("div", { className: "flex min-w-0 gap-4 overflow-x-auto p-1", "data-testid": "kanban-board", children: lanes.map((stage) => {
|
|
366
|
+
const allCards = grouped.get(stage.key) ?? [];
|
|
367
|
+
// Per-lane client-side narrowing: substring match on the
|
|
368
|
+
// chosen field over the already-fetched cards. Instant, and
|
|
369
|
+
// scoped to this stage only.
|
|
370
|
+
const laneFilter = laneFilters[stage.key];
|
|
371
|
+
const cards = laneFilter && laneFilter.value.trim()
|
|
372
|
+
? allCards.filter((c) => String(c[laneFilter.field] ?? '')
|
|
373
|
+
.toLowerCase()
|
|
374
|
+
.includes(laneFilter.value.trim().toLowerCase()))
|
|
375
|
+
: allCards;
|
|
376
|
+
const droppableAllowed = !activeId ||
|
|
377
|
+
stage.key === activeStage ||
|
|
378
|
+
isTransitionAllowed(transitions, activeStage, stage.key);
|
|
379
|
+
return (_jsx(KanbanLane, { stage: stage, count: cards.length, totalCount: allCards.length, filterFields: filterFields, laneFilter: laneFilter, onLaneFilterChange: (f) => setLaneFilter(stage.key, f), isDark: isDark, dimmed: !!activeId && !droppableAllowed, disabled: !!activeId && !droppableAllowed, children: loadingData && cards.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] })) : cards.length === 0 ? (_jsx("p", { className: "px-1 py-6 text-center text-xs text-muted-foreground", children: t('kanban.emptyLane', { defaultValue: 'Sin tarjetas' }) })) : (cards.map((card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction }, String(card.id))))) }, stage.key));
|
|
380
|
+
}) }), _jsx(DragOverlay, { children: activeCard ? (_jsx(CardPreview, { card: activeCard, titleCol: titleCol, fieldCols: fieldCols, locale: i18n.language, timeZone: timeZone, currency: currency })) : null }), rowActionDialogs] })] }));
|
|
328
381
|
}
|
|
329
|
-
function KanbanLane({ stage, count, isDark, dimmed, disabled, children }) {
|
|
382
|
+
function KanbanLane({ stage, count, totalCount, filterFields, laneFilter, onLaneFilterChange, isDark, dimmed, disabled, children, }) {
|
|
330
383
|
const { t } = useTranslation();
|
|
331
384
|
const { setNodeRef, isOver } = useDroppable({ id: stage.key, disabled });
|
|
332
385
|
const headerStyle = generateBadgeStyles(stage.color || optionColor(stage.key), {
|
|
333
386
|
isDark,
|
|
334
387
|
});
|
|
388
|
+
const laneActive = !!(laneFilter && laneFilter.value.trim());
|
|
389
|
+
const activeFieldLabel = filterFields.find((f) => f.key === laneFilter?.field)?.label ??
|
|
390
|
+
laneFilter?.field;
|
|
335
391
|
return (_jsxs("div", { ref: setNodeRef, className: "flex w-[300px] shrink-0 flex-col rounded-lg border bg-muted/30 transition-opacity", style: {
|
|
336
392
|
opacity: dimmed ? 0.45 : 1,
|
|
337
393
|
outline: isOver && !disabled ? '2px solid var(--ring, #3b82f6)' : 'none',
|
|
338
394
|
outlineOffset: 2,
|
|
339
|
-
}, "data-stage": stage.key, "data-disabled": disabled || undefined, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2.5", children: [_jsx(Badge, { variant: "outline", className: "border-0 text-xs font-semibold", style: headerStyle, children: t(stage.label, { defaultValue: stage.label }) }), _jsx("span", { className: "text-xs font-medium tabular-nums text-muted-foreground", children: count })] }),
|
|
395
|
+
}, "data-stage": stage.key, "data-disabled": disabled || undefined, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2.5", children: [_jsx(Badge, { variant: "outline", className: "border-0 text-xs font-semibold", style: headerStyle, children: t(stage.label, { defaultValue: stage.label }) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: "text-xs font-medium tabular-nums text-muted-foreground", children: laneActive ? `${count}/${totalCount}` : count }), _jsx(LaneFilterButton, { fields: filterFields, value: laneFilter, onChange: onLaneFilterChange })] })] }), laneActive && (_jsxs("div", { className: "flex items-center gap-1 px-3 pb-1.5 text-[11px] text-muted-foreground", children: [_jsx(ListFilter, { className: "h-3 w-3 shrink-0" }), _jsxs("span", { className: "truncate", children: [activeFieldLabel, ": ", laneFilter.value] }), _jsx("button", { type: "button", onClick: () => onLaneFilterChange(null), className: "ml-auto rounded p-0.5 hover:bg-muted", "aria-label": t('kanban.clearFilters', {
|
|
396
|
+
defaultValue: 'Limpiar',
|
|
397
|
+
}), children: _jsx(X, { className: "h-3 w-3" }) })] })), _jsx("div", { className: "flex min-h-[55vh] max-h-[70vh] min-w-0 flex-col gap-2 overflow-y-auto px-2 pb-3", children: children })] }));
|
|
398
|
+
}
|
|
399
|
+
// LaneFilterButton — the per-column funnel. Picks a field + a value and narrows
|
|
400
|
+
// ONLY this lane's cards (client-side, in the parent). Draft state lives here so
|
|
401
|
+
// typing doesn't refilter mid-keystroke; Apply/Enter commits, Limpiar clears.
|
|
402
|
+
function LaneFilterButton({ fields, value, onChange, }) {
|
|
403
|
+
const { t } = useTranslation();
|
|
404
|
+
const [open, setOpen] = useState(false);
|
|
405
|
+
const [field, setField] = useState(value?.field ?? fields[0]?.key ?? '');
|
|
406
|
+
const [text, setText] = useState(value?.value ?? '');
|
|
407
|
+
// Re-seed the draft from the committed filter each time the popover opens.
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
if (open) {
|
|
410
|
+
setField(value?.field ?? fields[0]?.key ?? '');
|
|
411
|
+
setText(value?.value ?? '');
|
|
412
|
+
}
|
|
413
|
+
}, [open, value, fields]);
|
|
414
|
+
if (fields.length === 0)
|
|
415
|
+
return null;
|
|
416
|
+
const active = !!(value && value.value.trim());
|
|
417
|
+
const apply = () => {
|
|
418
|
+
if (field && text.trim())
|
|
419
|
+
onChange({ field, value: text });
|
|
420
|
+
else
|
|
421
|
+
onChange(null);
|
|
422
|
+
setOpen(false);
|
|
423
|
+
};
|
|
424
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", className: `rounded p-1 transition-colors hover:bg-muted hover:text-foreground ${active ? 'text-primary' : 'text-muted-foreground'}`, "aria-label": t('kanban.filterLane', {
|
|
425
|
+
defaultValue: 'Filtrar columna',
|
|
426
|
+
}), children: _jsx(ListFilter, { className: "h-3.5 w-3.5" }) }) }), _jsxs(PopoverContent, { align: "end", className: "w-56 space-y-2 p-2", children: [_jsxs(Select, { value: field, onValueChange: setField, children: [_jsx(SelectTrigger, { className: "h-8 text-xs", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: fields.map((f) => (_jsx(SelectItem, { value: f.key, className: "text-xs", children: f.label }, f.key))) })] }), _jsx(Input, { autoFocus: true, value: text, onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
|
|
427
|
+
if (e.key === 'Enter')
|
|
428
|
+
apply();
|
|
429
|
+
}, placeholder: t('kanban.filterValue', { defaultValue: 'Valor...' }), className: "h-8 text-xs" }), _jsxs("div", { className: "flex justify-between gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", className: "h-7 text-xs", onClick: () => {
|
|
430
|
+
onChange(null);
|
|
431
|
+
setOpen(false);
|
|
432
|
+
}, children: t('kanban.clearFilters', { defaultValue: 'Limpiar' }) }), _jsx(Button, { size: "sm", className: "h-7 text-xs", onClick: apply, children: t('kanban.apply', { defaultValue: 'Aplicar' }) })] })] })] }));
|
|
340
433
|
}
|
|
341
434
|
function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, currency, onClick, onAction, }) {
|
|
342
435
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './options-context';
|
|
|
3
3
|
export * from './dynamic-table';
|
|
4
4
|
export { DynamicKanban, type DynamicKanbanProps, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
|
|
5
5
|
export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, type DynamicViewProps, } from './dynamic-view';
|
|
6
|
+
export { useDynamicFilters, type UseDynamicFiltersOptions, type UseDynamicFiltersResult, } from './use-dynamic-filters';
|
|
6
7
|
export * from './dynamic-form';
|
|
7
8
|
export { ActionModalDispatcher, type ActionModalProps, } from './action-modal-dispatcher';
|
|
8
9
|
export { ModelActionToolbar, useModelActions, type ModelActionToolbarProps, type ActionPlacement, } from './model-action-toolbar';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from './options-context';
|
|
|
8
8
|
export * from './dynamic-table';
|
|
9
9
|
export { DynamicKanban, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
|
|
10
10
|
export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, } from './dynamic-view';
|
|
11
|
+
export { useDynamicFilters, } from './use-dynamic-filters';
|
|
11
12
|
export * from './dynamic-form';
|
|
12
13
|
export { ActionModalDispatcher, } from './action-modal-dispatcher';
|
|
13
14
|
export { ModelActionToolbar, useModelActions, } from './model-action-toolbar';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ColumnFilterConfig } from './dynamic-columns-shim';
|
|
2
|
+
import type { TableMetadata } from './types';
|
|
3
|
+
export interface UseDynamicFiltersOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Static equality filters always applied (never shown as removable chips).
|
|
6
|
+
* Mirrors DynamicTable's `defaultFilters` — e.g. scoping a board to one owner.
|
|
7
|
+
*/
|
|
8
|
+
defaultFilters?: Record<string, any>;
|
|
9
|
+
}
|
|
10
|
+
export interface UseDynamicFiltersResult {
|
|
11
|
+
/** Per-field selected values, `filterKey → values[]` (empty = inactive). */
|
|
12
|
+
dynamicFilters: Record<string, string[]>;
|
|
13
|
+
/** Free-text search term (the toolbar search box). */
|
|
14
|
+
globalFilter: string;
|
|
15
|
+
setGlobalFilter: (v: string) => void;
|
|
16
|
+
/** Config for each filterable field, keyed by the metadata filter/column key. */
|
|
17
|
+
columnFilterConfigs: Map<string, ColumnFilterConfig>;
|
|
18
|
+
/**
|
|
19
|
+
* The serialized query params (`f_<key>`, `search`, `search_columns`) ready to
|
|
20
|
+
* spread into a `/data/:model` request. Stable object identity while inputs
|
|
21
|
+
* are unchanged so a consumer can safely depend on it in a fetch effect.
|
|
22
|
+
*/
|
|
23
|
+
filterParams: Record<string, any>;
|
|
24
|
+
/** Apply a field's selection (replaces its values). */
|
|
25
|
+
handleDynamicFilterChange: (filterKey: string, values: string[]) => void;
|
|
26
|
+
/** Count of fields with an active selection + the search term. */
|
|
27
|
+
activeFilterCount: number;
|
|
28
|
+
/** Clear every field filter and the search term. */
|
|
29
|
+
clearAll: () => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Reads the model's filter metadata and returns the live filter state + the
|
|
33
|
+
* serialized request params. Pure of any view concern — DynamicTable adds its
|
|
34
|
+
* own sort/pagination/`columnFilters`; DynamicKanban spreads `filterParams`
|
|
35
|
+
* straight onto its single-page fetch.
|
|
36
|
+
*/
|
|
37
|
+
export declare function useDynamicFilters(metadata: TableMetadata | null, opts?: UseDynamicFiltersOptions): UseDynamicFiltersResult;
|
|
38
|
+
//# sourceMappingURL=use-dynamic-filters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-dynamic-filters.d.ts","sourceRoot":"","sources":["../src/use-dynamic-filters.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,kBAAkB,EAAgB,MAAM,wBAAwB,CAAA;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACrC;AAED,MAAM,WAAW,uBAAuB;IACtC,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACxC,sDAAsD;IACtD,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,iFAAiF;IACjF,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;IACpD;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,uDAAuD;IACvD,yBAAyB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IACxE,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,CAAA;IACzB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,GAAG,IAAI,EAC9B,IAAI,GAAE,wBAA6B,GAClC,uBAAuB,CAqNzB"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// useDynamicFilters — the metadata-driven filter engine shared by DynamicTable
|
|
2
|
+
// and DynamicKanban. It owns nothing view-specific: given a model's
|
|
3
|
+
// `TableMetadata`, it derives the per-field filter configs (from
|
|
4
|
+
// `metadata.filters[]` + every `metadata.columns[].filterable` column),
|
|
5
|
+
// prefetches the server-side option lists for relation/select filters, tracks
|
|
6
|
+
// the selected values + global search, and serializes them into the exact
|
|
7
|
+
// `f_<key>=<op>:<value>` / `search` query params the `/data/:model` endpoint
|
|
8
|
+
// already understands.
|
|
9
|
+
//
|
|
10
|
+
// DynamicTable historically inlined this; DynamicKanban now needs the SAME
|
|
11
|
+
// filtering (RFC: a board is just another view of the model), so the logic is
|
|
12
|
+
// factored here to guarantee both surfaces filter identically.
|
|
13
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
+
import { useApi } from './api-context';
|
|
15
|
+
import { DATE_CELL_TYPES } from './dynamic-columns';
|
|
16
|
+
import { getSearchableColumnKeys } from './column-visibility';
|
|
17
|
+
/**
|
|
18
|
+
* Reads the model's filter metadata and returns the live filter state + the
|
|
19
|
+
* serialized request params. Pure of any view concern — DynamicTable adds its
|
|
20
|
+
* own sort/pagination/`columnFilters`; DynamicKanban spreads `filterParams`
|
|
21
|
+
* straight onto its single-page fetch.
|
|
22
|
+
*/
|
|
23
|
+
export function useDynamicFilters(metadata, opts = {}) {
|
|
24
|
+
const { defaultFilters } = opts;
|
|
25
|
+
const api = useApi();
|
|
26
|
+
const [dynamicFilters, setDynamicFilters] = useState({});
|
|
27
|
+
const [globalFilter, setGlobalFilter] = useState('');
|
|
28
|
+
const [filterOptionsMap, setFilterOptionsMap] = useState(new Map());
|
|
29
|
+
// Prefetch the option lists for relation/select filters (once per model). The
|
|
30
|
+
// combobox needs these before the user opens it; mirrors DynamicTable's
|
|
31
|
+
// metadata-init option prefetch, narrowed to the FILTER endpoints only (the
|
|
32
|
+
// kanban renders card cells through ActivityValueRenderer, not the cell
|
|
33
|
+
// options map, so we skip that half).
|
|
34
|
+
const prefetchedSig = useRef(null);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!metadata)
|
|
37
|
+
return;
|
|
38
|
+
const filterEndpoints = (metadata.filters || [])
|
|
39
|
+
.filter((f) => f.searchEndpoint &&
|
|
40
|
+
(f.type === 'select' ||
|
|
41
|
+
f.type === 'dynamic_select' ||
|
|
42
|
+
f.type === 'boolean'))
|
|
43
|
+
.map((f) => f.searchEndpoint);
|
|
44
|
+
const columnFilterEndpoints = (metadata.columns || [])
|
|
45
|
+
.filter((c) => c.filterable && c.searchEndpoint)
|
|
46
|
+
.map((c) => c.searchEndpoint);
|
|
47
|
+
const endpoints = Array.from(new Set([...filterEndpoints, ...columnFilterEndpoints]));
|
|
48
|
+
if (endpoints.length === 0)
|
|
49
|
+
return;
|
|
50
|
+
// Dedup on the endpoint set, not model identity (TableMetadata carries no
|
|
51
|
+
// name) — the same board re-rendering shouldn't refetch its options.
|
|
52
|
+
const sig = endpoints.join('|');
|
|
53
|
+
if (prefetchedSig.current === sig)
|
|
54
|
+
return;
|
|
55
|
+
prefetchedSig.current = sig;
|
|
56
|
+
let cancelled = false;
|
|
57
|
+
Promise.all(endpoints.map(async (ep) => {
|
|
58
|
+
try {
|
|
59
|
+
const res = await api.get(ep);
|
|
60
|
+
return {
|
|
61
|
+
endpoint: ep,
|
|
62
|
+
data: res.data?.success ? res.data.data : [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
console.error(`Failed to fetch filter options for ${ep}`, e);
|
|
67
|
+
return { endpoint: ep, data: [] };
|
|
68
|
+
}
|
|
69
|
+
})).then((results) => {
|
|
70
|
+
if (cancelled)
|
|
71
|
+
return;
|
|
72
|
+
const fMap = new Map();
|
|
73
|
+
for (const r of results) {
|
|
74
|
+
fMap.set(r.endpoint, (r.data || []).map((item) => ({
|
|
75
|
+
label: item.label || item.name || '',
|
|
76
|
+
value: String(item.value ?? item.id ?? ''),
|
|
77
|
+
icon: item.icon,
|
|
78
|
+
color: item.color || item.class,
|
|
79
|
+
})));
|
|
80
|
+
}
|
|
81
|
+
setFilterOptionsMap(fMap);
|
|
82
|
+
});
|
|
83
|
+
return () => {
|
|
84
|
+
cancelled = true;
|
|
85
|
+
};
|
|
86
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
+
}, [metadata]);
|
|
88
|
+
const handleDynamicFilterChange = useCallback((filterKey, values) => {
|
|
89
|
+
setDynamicFilters((prev) => ({ ...prev, [filterKey]: values }));
|
|
90
|
+
}, []);
|
|
91
|
+
const clearAll = useCallback(() => {
|
|
92
|
+
setDynamicFilters({});
|
|
93
|
+
setGlobalFilter('');
|
|
94
|
+
}, []);
|
|
95
|
+
// Derive one ColumnFilterConfig per filterable field. Explicit
|
|
96
|
+
// `metadata.filters[]` wins; otherwise every `filterable` column yields a
|
|
97
|
+
// config with its UI inferred from the column shape. Copied 1:1 from
|
|
98
|
+
// DynamicTable so table and board build identical configs.
|
|
99
|
+
const columnFilterConfigs = useMemo(() => {
|
|
100
|
+
const map = new Map();
|
|
101
|
+
if (!metadata)
|
|
102
|
+
return map;
|
|
103
|
+
for (const f of metadata.filters ?? []) {
|
|
104
|
+
const fType = f.type;
|
|
105
|
+
let options = [];
|
|
106
|
+
if (f.options && f.options.length > 0) {
|
|
107
|
+
options = f.options.map((o) => ({
|
|
108
|
+
label: o.label,
|
|
109
|
+
value: String(o.value),
|
|
110
|
+
icon: o.icon,
|
|
111
|
+
color: o.color,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
if (f.searchEndpoint && filterOptionsMap.has(f.searchEndpoint)) {
|
|
115
|
+
options = filterOptionsMap.get(f.searchEndpoint) || [];
|
|
116
|
+
}
|
|
117
|
+
if (fType === 'select' && options.length === 0 && !f.searchEndpoint)
|
|
118
|
+
continue;
|
|
119
|
+
map.set(f.key, {
|
|
120
|
+
filterType: fType,
|
|
121
|
+
filterKey: f.column || f.key,
|
|
122
|
+
options,
|
|
123
|
+
selectedValues: dynamicFilters[f.column || f.key] || [],
|
|
124
|
+
onFilterChange: handleDynamicFilterChange,
|
|
125
|
+
loading: f.searchEndpoint ? !filterOptionsMap.has(f.searchEndpoint) : false,
|
|
126
|
+
searchEndpoint: f.searchEndpoint,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
for (const c of metadata.columns ?? []) {
|
|
130
|
+
if (!c.filterable || map.has(c.key))
|
|
131
|
+
continue;
|
|
132
|
+
const hasStaticOptions = (c.options?.length ?? 0) > 0;
|
|
133
|
+
const hasEndpoint = !!c.searchEndpoint;
|
|
134
|
+
const isRelation = !!c.ref || c.filterType === 'dynamic_select';
|
|
135
|
+
let filterType;
|
|
136
|
+
if (c.filterType)
|
|
137
|
+
filterType = c.filterType;
|
|
138
|
+
else if (isRelation && hasEndpoint)
|
|
139
|
+
filterType = 'dynamic_select';
|
|
140
|
+
else if (hasStaticOptions || hasEndpoint)
|
|
141
|
+
filterType = 'select';
|
|
142
|
+
else if (c.type === 'boolean')
|
|
143
|
+
filterType = 'boolean';
|
|
144
|
+
else if (c.type === 'number')
|
|
145
|
+
filterType = 'number_range';
|
|
146
|
+
else if (DATE_CELL_TYPES.includes(c.type))
|
|
147
|
+
filterType = 'date_range';
|
|
148
|
+
else
|
|
149
|
+
filterType = 'text';
|
|
150
|
+
const options = hasStaticOptions
|
|
151
|
+
? c.options.map((o) => ({
|
|
152
|
+
label: o.label,
|
|
153
|
+
value: String(o.value),
|
|
154
|
+
icon: o.icon,
|
|
155
|
+
color: o.color,
|
|
156
|
+
}))
|
|
157
|
+
: hasEndpoint && filterOptionsMap.has(c.searchEndpoint)
|
|
158
|
+
? filterOptionsMap.get(c.searchEndpoint) || []
|
|
159
|
+
: [];
|
|
160
|
+
map.set(c.key, {
|
|
161
|
+
filterType,
|
|
162
|
+
filterKey: c.key,
|
|
163
|
+
options,
|
|
164
|
+
selectedValues: dynamicFilters[c.key] || [],
|
|
165
|
+
onFilterChange: handleDynamicFilterChange,
|
|
166
|
+
loading: hasEndpoint && !filterOptionsMap.has(c.searchEndpoint),
|
|
167
|
+
searchEndpoint: c.searchEndpoint,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return map;
|
|
171
|
+
}, [metadata, filterOptionsMap, dynamicFilters, handleDynamicFilterChange]);
|
|
172
|
+
const searchableKeys = useMemo(() => (metadata ? getSearchableColumnKeys(metadata) : null), [metadata]);
|
|
173
|
+
// Serialize to `/data/:model` query params — identical operator encoding to
|
|
174
|
+
// DynamicTable.buildFilterParams (IN:/RANGE:/GTE:/LTE:/ILIKE:/plain).
|
|
175
|
+
const filterParams = useMemo(() => {
|
|
176
|
+
const params = {};
|
|
177
|
+
if (globalFilter) {
|
|
178
|
+
if (searchableKeys === null) {
|
|
179
|
+
params.search = globalFilter;
|
|
180
|
+
}
|
|
181
|
+
else if (searchableKeys.length > 0) {
|
|
182
|
+
params.search = globalFilter;
|
|
183
|
+
params.search_columns = searchableKeys.join(',');
|
|
184
|
+
}
|
|
185
|
+
// searchableKeys === [] → no searchable column, skip the search param.
|
|
186
|
+
}
|
|
187
|
+
if (defaultFilters) {
|
|
188
|
+
Object.entries(defaultFilters).forEach(([key, value]) => {
|
|
189
|
+
params[`f_${key}`] = value;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
Object.entries(dynamicFilters).forEach(([key, values]) => {
|
|
193
|
+
if (values.length === 0)
|
|
194
|
+
return;
|
|
195
|
+
const gteVal = values.find((v) => v.startsWith('GTE:'));
|
|
196
|
+
const lteVal = values.find((v) => v.startsWith('LTE:'));
|
|
197
|
+
if (gteVal || lteVal) {
|
|
198
|
+
const min = gteVal ? gteVal.replace('GTE:', '') : '';
|
|
199
|
+
const max = lteVal ? lteVal.replace('LTE:', '') : '';
|
|
200
|
+
params[`f_${key}`] = `RANGE:${min},${max}`;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (values.length === 1)
|
|
204
|
+
params[`f_${key}`] = values[0];
|
|
205
|
+
else
|
|
206
|
+
params[`f_${key}`] = `IN:${values.join(',')}`;
|
|
207
|
+
});
|
|
208
|
+
return params;
|
|
209
|
+
}, [globalFilter, searchableKeys, defaultFilters, dynamicFilters]);
|
|
210
|
+
const activeFilterCount = useMemo(() => {
|
|
211
|
+
let n = Object.values(dynamicFilters).filter((v) => v.length > 0).length;
|
|
212
|
+
if (globalFilter)
|
|
213
|
+
n += 1;
|
|
214
|
+
return n;
|
|
215
|
+
}, [dynamicFilters, globalFilter]);
|
|
216
|
+
return {
|
|
217
|
+
dynamicFilters,
|
|
218
|
+
globalFilter,
|
|
219
|
+
setGlobalFilter,
|
|
220
|
+
columnFilterConfigs,
|
|
221
|
+
filterParams,
|
|
222
|
+
handleDynamicFilterChange,
|
|
223
|
+
activeFilterCount,
|
|
224
|
+
clearAll,
|
|
225
|
+
};
|
|
226
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "21.1.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"sonner": ">=1.7",
|
|
39
39
|
"zustand": ">=5",
|
|
40
40
|
"@asteby/metacore-sdk": "^3.2.0",
|
|
41
|
-
"@asteby/metacore-ui": "^2.
|
|
41
|
+
"@asteby/metacore-ui": "^2.7.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"@tanstack/react-router": {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"vitest": "^4.0.0",
|
|
69
69
|
"zustand": "^5.0.0",
|
|
70
70
|
"@asteby/metacore-sdk": "3.2.0",
|
|
71
|
-
"@asteby/metacore-ui": "2.
|
|
71
|
+
"@asteby/metacore-ui": "2.7.0"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"build": "tsc -p tsconfig.json",
|