@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 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
@@ -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;AAoC9B,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;CACpB;AAED,wBAAgB,aAAa,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,WAAW,EACX,QAAQ,EACR,QAAc,EACd,QAAQ,EACR,QAAQ,GACX,EAAE,kBAAkB,qBAqSpB"}
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"}
@@ -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
- // eslint-disable-next-line react-hooks/exhaustive-deps
220
- }, [metadata, refreshTrigger]);
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
- const cards = grouped.get(stage.key) ?? [];
323
- const droppableAllowed = !activeId ||
324
- stage.key === activeStage ||
325
- isTransitionAllowed(transitions, activeStage, stage.key);
326
- return (_jsx(KanbanLane, { stage: stage, count: cards.length, 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));
327
- }) }), _jsx(DragOverlay, { children: activeCard ? (_jsx(CardPreview, { card: activeCard, titleCol: titleCol, fieldCols: fieldCols, locale: i18n.language, timeZone: timeZone, currency: currency })) : null }), rowActionDialogs] }));
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 })] }), _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 })] }));
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';
@@ -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": "20.1.7",
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.6.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.6.2"
71
+ "@asteby/metacore-ui": "2.7.0"
72
72
  },
73
73
  "scripts": {
74
74
  "build": "tsc -p tsconfig.json",