@asteby/metacore-runtime-react 22.0.0 → 23.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 CHANGED
@@ -1,5 +1,38 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 23.0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 25a78e7: Fixes del embudo por columna del kanban (LaneFilterButton).
8
+ - **Precarga de opciones dinámicas:** el value-picker del embudo ahora usa el
9
+ MISMO combobox pro que el Sheet — si el campo elegido tiene options estáticas
10
+ o `loadOptions` (facet), renderiza el combobox multi-select con carga lazy
11
+ (counts, buscador "Buscar valores...", estados de carga). Antes un campo facet
12
+ (p.ej. "Repo") caía al input crudo "Valor..." porque sus options aún no
13
+ estaban cargadas. Solo cae a input de texto cuando el campo es texto libre sin
14
+ facets. Nuevo componente compartible `FilterValueCombobox` en
15
+ `@asteby/metacore-ui`. El filtrado del lane sigue client-side: igualdad/IN
16
+ para valores de select/facet, substring para texto libre.
17
+ - **Traducción de opciones:** las opciones de Stage (y cualquier option con key
18
+ i18n del manifest) se traducen con `t(label, {defaultValue})` en runtime-react
19
+ ANTES de pasarlas al control (ColumnFilterControl vive en ui, sin i18n). Aplica
20
+ al embudo, al Sheet, a los chips y a los resúmenes de valor; el valor activo de
21
+ stage se muestra traducido y con su color.
22
+ - **Diseño del popover del embudo:** select de campo y control de valor a ancho
23
+ completo, popover más ancho (`w-72`), combobox con borde redondeado, botones
24
+ Limpiar/Aplicar full-width en fila — al nivel del resto del rediseño.
25
+ - **Prefetch de facetas:** al resolver la metadata se precargan en paralelo las
26
+ facetas de TODOS los campos facet (un `api.get` por campo, dedupeado por firma,
27
+ `allSettled` para que un campo con error no rompa el resto), sembrando el cache
28
+ del loader y poblando las `options` del `ColumnFilterConfig`. Así el popover
29
+ del Sheet y el value-picker del lane abren instantáneos con valores + counts,
30
+ sin "Cargando…"; el spinner queda solo para el refetch con búsqueda (`q`).
31
+ `useFacetLoaders` ahora expone `prefetchFacets` y `facetOptions`.
32
+
33
+ - Updated dependencies [25a78e7]
34
+ - @asteby/metacore-ui@2.9.0
35
+
3
36
  ## 22.0.0
4
37
 
5
38
  ### Minor Changes
@@ -50,6 +50,25 @@ export declare function summarizeFilterValues(values: string[] | undefined, opti
50
50
  label: string;
51
51
  value: string;
52
52
  }[] | undefined, maxShown?: number): string;
53
+ /**
54
+ * Whether a card passes a lane funnel. Picked select/facet `values` match by
55
+ * equality (IN — the card's field value must be one of them); a free-text
56
+ * `text` matches by case-insensitive substring. No field / no criteria → passes.
57
+ * Pure — exported for unit tests.
58
+ */
59
+ export declare function cardMatchesLaneFunnel(card: any, filter: {
60
+ field?: string;
61
+ values?: string[];
62
+ text?: string;
63
+ } | undefined): boolean;
64
+ /**
65
+ * Translates option labels through the app translator (manifest i18n keys →
66
+ * localized text). A raw value with no matching key falls through to itself via
67
+ * `defaultValue`. Pure — exported for unit tests.
68
+ */
69
+ export declare function translateOptionLabels<T extends {
70
+ label: string;
71
+ }>(options: T[], translate: (key: string) => string): T[];
53
72
  /**
54
73
  * Whether a card matches a free-text lane search: a case-insensitive substring
55
74
  * over the card's title + every visible field value (`String(v)`). Empty query
@@ -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;AA+D9B,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;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,EAC5B,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,SAAS,EACvD,QAAQ,SAAI,GACb,MAAM,CAuBR;AAgCD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAChC,IAAI,EAAE,GAAG,EACT,IAAI,EAAE,gBAAgB,EAAE,EACxB,KAAK,EAAE,MAAM,GACd,OAAO,CAQT;AAqCD,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,qBA2jBpB"}
1
+ {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
@@ -5,7 +5,7 @@ import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors, useDragg
5
5
  import { Calendar, CircleDot, Hash, ListFilter, MoreHorizontal, Search, Tag, ToggleLeft, Type, X, } from 'lucide-react';
6
6
  import { toast } from 'sonner';
7
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
+ import { ColumnFilterControl, FilterValueCombobox } from '@asteby/metacore-ui/data-table';
9
9
  import { generateBadgeStyles, optionColor, resolveColorCss } from '@asteby/metacore-ui/lib';
10
10
  import { useApi } from './api-context';
11
11
  import { useDynamicFilters } from './use-dynamic-filters';
@@ -187,6 +187,32 @@ function chipValueColor(config) {
187
187
  const opt = config.options.find((o) => o.value === value);
188
188
  return opt?.color ? resolveColorCss(opt.color) : undefined;
189
189
  }
190
+ /**
191
+ * Whether a card passes a lane funnel. Picked select/facet `values` match by
192
+ * equality (IN — the card's field value must be one of them); a free-text
193
+ * `text` matches by case-insensitive substring. No field / no criteria → passes.
194
+ * Pure — exported for unit tests.
195
+ */
196
+ export function cardMatchesLaneFunnel(card, filter) {
197
+ if (!filter?.field)
198
+ return true;
199
+ const raw = String(card?.[filter.field] ?? '');
200
+ if (filter.values && filter.values.length > 0) {
201
+ return filter.values.includes(raw);
202
+ }
203
+ if (filter.text?.trim()) {
204
+ return raw.toLowerCase().includes(filter.text.trim().toLowerCase());
205
+ }
206
+ return true;
207
+ }
208
+ /**
209
+ * Translates option labels through the app translator (manifest i18n keys →
210
+ * localized text). A raw value with no matching key falls through to itself via
211
+ * `defaultValue`. Pure — exported for unit tests.
212
+ */
213
+ export function translateOptionLabels(options, translate) {
214
+ return options.map((o) => ({ ...o, label: translate(o.label) }));
215
+ }
190
216
  /**
191
217
  * Whether a card matches a free-text lane search: a case-insensitive substring
192
218
  * over the card's title + every visible field value (`String(v)`). Empty query
@@ -308,11 +334,29 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
308
334
  if (!metadata)
309
335
  return [];
310
336
  const out = [];
337
+ // Option labels come from the manifest as i18n keys (e.g.
338
+ // "integration_github.stage.backlog"). ColumnFilterControl lives in the
339
+ // ui package (no i18n), so translate labels HERE — on the static options
340
+ // and on whatever the facet loader resolves — before they ever reach a
341
+ // control, chip or value summary. A raw value (a repo name) has no key,
342
+ // so t() returns it verbatim via defaultValue.
343
+ const tr = (label) => t(label, { defaultValue: label });
311
344
  for (const [key, config] of columnFilterConfigs) {
312
345
  const f = metadata.filters?.find((x) => x.key === key);
313
346
  const c = metadata.columns.find((x) => x.key === key);
314
347
  const rawLabel = f?.label || c?.label || key;
315
- out.push({ key, label: t(rawLabel, { defaultValue: rawLabel }), config });
348
+ const translatedConfig = {
349
+ ...config,
350
+ options: translateOptionLabels(config.options, tr),
351
+ loadOptions: config.loadOptions
352
+ ? (q) => config.loadOptions(q).then((opts) => translateOptionLabels(opts, tr))
353
+ : undefined,
354
+ };
355
+ out.push({
356
+ key,
357
+ label: tr(rawLabel),
358
+ config: translatedConfig,
359
+ });
316
360
  }
317
361
  return out;
318
362
  }, [metadata, columnFilterConfigs, t]);
@@ -342,7 +386,9 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
342
386
  setLaneFilters((prev) => {
343
387
  const merged = { ...prev[stageKey], ...patch };
344
388
  const next = { ...prev };
345
- const hasFunnel = !!(merged.field && merged.value?.trim());
389
+ const hasFunnel = !!(merged.field &&
390
+ ((merged.values && merged.values.length > 0) ||
391
+ merged.text?.trim()));
346
392
  const hasQuery = !!merged.query?.trim();
347
393
  if (hasFunnel || hasQuery)
348
394
  next[stageKey] = merged;
@@ -475,12 +521,8 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
475
521
  // (query) are AND-combined.
476
522
  const laneFilter = laneFilters[stage.key];
477
523
  let cards = allCards;
478
- if (laneFilter?.field && laneFilter.value?.trim()) {
479
- const v = laneFilter.value.trim().toLowerCase();
480
- const field = laneFilter.field;
481
- cards = cards.filter((c) => String(c[field] ?? '')
482
- .toLowerCase()
483
- .includes(v));
524
+ if (laneFilter?.field) {
525
+ cards = cards.filter((c) => cardMatchesLaneFunnel(c, laneFilter));
484
526
  }
485
527
  if (laneFilter?.query?.trim()) {
486
528
  cards = cards.filter((c) => cardMatchesLaneQuery(c, searchCols, laneFilter.query));
@@ -490,7 +532,8 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
490
532
  isTransitionAllowed(transitions, activeStage, stage.key);
491
533
  return (_jsx(KanbanLane, { stage: stage, count: cards.length, totalCount: allCards.length, filterFields: filterFields, laneFilter: laneFilter, onFunnelChange: (f) => updateLaneFilter(stage.key, {
492
534
  field: f?.field,
493
- value: f?.value,
535
+ values: f?.values,
536
+ text: f?.text,
494
537
  }), onQueryChange: (q) => updateLaneFilter(stage.key, { query: q }), 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));
495
538
  }) }), _jsx(DragOverlay, { children: activeCard ? (_jsx(CardPreview, { card: activeCard, titleCol: titleCol, fieldCols: fieldCols, locale: i18n.language, timeZone: timeZone, currency: currency })) : null }), rowActionDialogs] })] }));
496
539
  }
@@ -527,11 +570,18 @@ function KanbanLane({ stage, count, totalCount, filterFields, laneFilter, onFunn
527
570
  const headerStyle = generateBadgeStyles(stage.color || optionColor(stage.key), {
528
571
  isDark,
529
572
  });
530
- const funnelActive = !!(laneFilter?.field && laneFilter.value?.trim());
573
+ const funnelField = filterFields.find((f) => f.key === laneFilter?.field);
574
+ const funnelActive = !!(laneFilter?.field &&
575
+ ((laneFilter.values && laneFilter.values.length > 0) ||
576
+ laneFilter.text?.trim()));
531
577
  const queryActive = !!laneFilter?.query?.trim();
532
578
  const laneActive = funnelActive || queryActive;
533
- const activeFieldLabel = filterFields.find((f) => f.key === laneFilter?.field)?.label ??
534
- laneFilter?.field;
579
+ const activeFieldLabel = funnelField?.label ?? laneFilter?.field;
580
+ // Human summary of the funnel value: resolved option labels for picked
581
+ // values, or the raw free text.
582
+ const funnelSummary = laneFilter?.values && laneFilter.values.length > 0
583
+ ? summarizeFilterValues(laneFilter.values, funnelField?.config?.options)
584
+ : laneFilter?.text ?? '';
535
585
  // Inline lane search: a Search icon expands an Input; Escape or blur-while-
536
586
  // empty collapses it. The query itself lives in the parent's laneFilters so
537
587
  // it survives collapse and combines with the funnel.
@@ -541,8 +591,12 @@ function KanbanLane({ stage, count, totalCount, filterFields, laneFilter, onFunn
541
591
  if (searchOpen)
542
592
  searchRef.current?.focus();
543
593
  }, [searchOpen]);
544
- const funnelValue = laneFilter?.field && laneFilter.value
545
- ? { field: laneFilter.field, value: laneFilter.value }
594
+ const funnelValue = laneFilter?.field
595
+ ? {
596
+ field: laneFilter.field,
597
+ values: laneFilter.values,
598
+ text: laneFilter.text,
599
+ }
546
600
  : undefined;
547
601
  return (_jsxs("div", { ref: setNodeRef, className: "group/lane flex w-[300px] shrink-0 flex-col rounded-xl border bg-muted/30 transition-opacity", style: {
548
602
  opacity: dimmed ? 0.45 : 1,
@@ -560,7 +614,7 @@ function KanbanLane({ stage, count, totalCount, filterFields, laneFilter, onFunn
560
614
  setSearchOpen(false);
561
615
  }, placeholder: t('kanban.searchLanePlaceholder', {
562
616
  defaultValue: 'Buscar tarjetas...',
563
- }), className: "h-7 pl-7 text-xs" })] }) })), funnelActive && (_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: () => onFunnelChange(null), className: "ml-auto rounded p-0.5 hover:bg-muted", "aria-label": t('kanban.clearFilters', {
617
+ }), className: "h-7 pl-7 text-xs" })] }) })), funnelActive && (_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, ": ", funnelSummary] }), _jsx("button", { type: "button", onClick: () => onFunnelChange(null), className: "ml-auto rounded p-0.5 hover:bg-muted", "aria-label": t('kanban.clearFilters', {
564
618
  defaultValue: 'Limpiar',
565
619
  }), 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 })] }));
566
620
  }
@@ -571,53 +625,54 @@ function LaneFilterButton({ fields, value, onChange, }) {
571
625
  const { t } = useTranslation();
572
626
  const [open, setOpen] = useState(false);
573
627
  const [field, setField] = useState(value?.field ?? fields[0]?.key ?? '');
574
- const [text, setText] = useState(value?.value ?? '');
628
+ const [values, setValues] = useState(value?.values ?? []);
629
+ const [text, setText] = useState(value?.text ?? '');
575
630
  // Re-seed the draft from the committed filter each time the popover opens.
576
631
  useEffect(() => {
577
632
  if (open) {
578
633
  setField(value?.field ?? fields[0]?.key ?? '');
579
- setText(value?.value ?? '');
634
+ setValues(value?.values ?? []);
635
+ setText(value?.text ?? '');
580
636
  }
581
637
  }, [open, value, fields]);
582
638
  if (fields.length === 0)
583
639
  return null;
584
- const active = !!(value && value.value.trim());
585
- // When the chosen field carries a known option set (select/facet with
586
- // pre-loaded values), offer those values as a Select instead of a raw text
587
- // box so the funnel picks real values, not free strings.
588
- const selectedFieldCfg = fields.find((f) => f.key === field)?.config;
589
- const valueOptions = selectedFieldCfg?.options ?? [];
590
- const hasValueOptions = valueOptions.length > 0;
640
+ const active = !!(value &&
641
+ ((value.values && value.values.length > 0) || value.text?.trim()));
642
+ // The value step mirrors the sheet: when the chosen field is a select or a
643
+ // facet (static options OR a lazy loader), render the SAME pro combobox —
644
+ // multi-select, searchable, with counts. Only a genuinely free-text field
645
+ // (no options, no loader) falls back to a raw "Contiene..." input.
646
+ const cfg = fields.find((f) => f.key === field)?.config;
647
+ const hasValuePicker = (cfg?.options?.length ?? 0) > 0 || !!cfg?.loadOptions;
648
+ const toggle = (v) => setValues((prev) => prev.includes(v) ? prev.filter((x) => x !== v) : [...prev, v]);
591
649
  const apply = () => {
592
- if (field && text.trim())
593
- onChange({ field, value: text });
650
+ if (field && values.length > 0)
651
+ onChange({ field, values });
652
+ else if (field && text.trim())
653
+ onChange({ field, text: text.trim() });
594
654
  else
595
655
  onChange(null);
596
656
  setOpen(false);
597
657
  };
658
+ const clear = () => {
659
+ onChange(null);
660
+ setOpen(false);
661
+ };
598
662
  return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", className: `flex size-6 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-foreground ${active ? 'text-primary' : 'text-muted-foreground'}`, "aria-label": t('kanban.filterLane', {
599
663
  defaultValue: 'Filtrar columna',
600
- }), 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: (f) => {
664
+ }), children: _jsx(ListFilter, { className: "h-3.5 w-3.5" }) }) }), _jsxs(PopoverContent, { align: "end", className: "w-72 space-y-2.5 rounded-xl p-2.5 shadow-lg", children: [_jsxs(Select, { value: field, onValueChange: (f) => {
601
665
  setField(f);
602
666
  // Reset the value when switching fields — a value picked
603
667
  // for one field is meaningless for another.
668
+ setValues([]);
604
669
  setText('');
605
- }, 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))) })] }), hasValueOptions ? (_jsxs(Select, { value: text || undefined, onValueChange: (v) => {
606
- setText(v);
607
- if (field && v)
608
- onChange({ field, value: v });
609
- setOpen(false);
610
- }, children: [_jsx(SelectTrigger, { className: "h-8 text-xs", children: _jsx(SelectValue, { placeholder: t('kanban.filterValue', {
611
- defaultValue: 'Valor...',
612
- }) }) }), _jsx(SelectContent, { children: valueOptions.map((o) => (_jsx(SelectItem, { value: o.value, className: "text-xs", children: o.label }, o.value))) })] })) : (_jsx(Input, { autoFocus: true, value: text, onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
670
+ }, children: [_jsx(SelectTrigger, { className: "h-8 w-full text-xs", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: fields.map((f) => (_jsx(SelectItem, { value: f.key, className: "text-xs", children: f.label }, f.key))) })] }), hasValuePicker ? (_jsx("div", { className: "overflow-hidden rounded-lg border", children: _jsx(FilterValueCombobox, { staticOptions: cfg?.options, loadOptions: cfg?.loadOptions, selected: values, onToggle: toggle }, field) })) : (_jsx(Input, { autoFocus: true, value: text, onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
613
671
  if (e.key === 'Enter')
614
672
  apply();
615
673
  }, placeholder: t('kanban.filterValue', {
616
- defaultValue: 'Valor...',
617
- }), 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: () => {
618
- onChange(null);
619
- setOpen(false);
620
- }, children: t('kanban.clearFilters', { defaultValue: 'Limpiar' }) }), _jsx(Button, { size: "sm", className: "h-7 text-xs", onClick: apply, children: t('kanban.apply', { defaultValue: 'Aplicar' }) })] })] })] }));
674
+ defaultValue: 'Contiene...',
675
+ }), className: "h-8 w-full text-xs" })), _jsxs("div", { className: "flex gap-1.5", children: [_jsx(Button, { variant: "outline", size: "sm", className: "h-7 flex-1 text-xs", onClick: clear, disabled: !active && values.length === 0 && !text.trim(), children: t('kanban.clearFilters', { defaultValue: 'Limpiar' }) }), _jsxs(Button, { size: "sm", className: "h-7 flex-1 text-xs", onClick: apply, disabled: values.length === 0 && !text.trim(), children: [t('kanban.apply', { defaultValue: 'Aplicar' }), values.length > 0 ? ` (${values.length})` : ''] })] })] })] }));
621
676
  }
622
677
  function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, currency, onClick, onAction, }) {
623
678
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
@@ -462,7 +462,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
462
462
  // getDynamicColumns forwards `loadOptions` into the column meta). Facets base
463
463
  // derived off the list endpoint exactly like the aggregate endpoint below.
464
464
  const facetsBase = endpoint ? `${endpoint}/facets` : model ? `/data/${model}/facets` : null;
465
- const getFacetLoader = useFacetLoaders(facetsBase);
465
+ const { getFacetLoader } = useFacetLoaders(facetsBase);
466
466
  const columnFilterConfigs = useMemo(() => {
467
467
  const map = new Map();
468
468
  if (!metadata)
@@ -1 +1 @@
1
- {"version":3,"file":"use-dynamic-filters.d.ts","sourceRoot":"","sources":["../src/use-dynamic-filters.ts"],"names":[],"mappings":"AAiBA,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;IACpC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;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,CAgSzB"}
1
+ {"version":3,"file":"use-dynamic-filters.d.ts","sourceRoot":"","sources":["../src/use-dynamic-filters.ts"],"names":[],"mappings":"AAiBA,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;IACpC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;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,CAyTzB"}
@@ -36,7 +36,7 @@ export function useDynamicFilters(metadata, opts = {}) {
36
36
  : model
37
37
  ? `/data/${model}/facets`
38
38
  : null);
39
- const getFacetLoader = useFacetLoaders(facetsBase);
39
+ const { getFacetLoader, prefetchFacets, facetOptions } = useFacetLoaders(facetsBase);
40
40
  // Prefetch the option lists for relation/select filters (once per model). The
41
41
  // combobox needs these before the user opens it; mirrors DynamicTable's
42
42
  // metadata-init option prefetch, narrowed to the FILTER endpoints only (the
@@ -152,6 +152,9 @@ export function useDynamicFilters(metadata, opts = {}) {
152
152
  if (loader) {
153
153
  fType = 'facet';
154
154
  loadOptions = loader;
155
+ // Prewarmed values (from prefetchFacets) → the popover opens with the
156
+ // list already there, no "Cargando…" flash.
157
+ options = facetOptions.get(f.column || f.key) ?? [];
155
158
  }
156
159
  }
157
160
  if (fType === 'select' && options.length === 0 && !f.searchEndpoint)
@@ -217,6 +220,8 @@ export function useDynamicFilters(metadata, opts = {}) {
217
220
  if (loader) {
218
221
  filterType = 'facet';
219
222
  loadOptions = loader;
223
+ // Prewarmed values (from prefetchFacets) → instant open.
224
+ options = facetOptions.get(c.key) ?? [];
220
225
  }
221
226
  }
222
227
  map.set(c.key, {
@@ -238,7 +243,26 @@ export function useDynamicFilters(metadata, opts = {}) {
238
243
  handleDynamicFilterChange,
239
244
  facetsBase,
240
245
  getFacetLoader,
246
+ facetOptions,
241
247
  ]);
248
+ // Prewarm every facet field in one burst once the configs settle, so the
249
+ // Sheet popover and the lane value-picker open instantly (values + counts)
250
+ // instead of flashing "Cargando…". Deduped inside prefetchFacets by field set.
251
+ const facetFieldsSig = useMemo(() => {
252
+ const keys = [];
253
+ // Key on `filterKey` (the column the loader queries), not the config map key
254
+ // — they differ when an explicit filter sets a distinct `column`.
255
+ for (const config of columnFilterConfigs.values()) {
256
+ if (config.filterType === 'facet')
257
+ keys.push(config.filterKey);
258
+ }
259
+ return keys.join('|');
260
+ }, [columnFilterConfigs]);
261
+ useEffect(() => {
262
+ if (!facetFieldsSig)
263
+ return;
264
+ prefetchFacets(facetFieldsSig.split('|'));
265
+ }, [facetFieldsSig, prefetchFacets]);
242
266
  const searchableKeys = useMemo(() => (metadata ? getSearchableColumnKeys(metadata) : null), [metadata]);
243
267
  // Serialize to `/data/:model` query params — identical operator encoding to
244
268
  // DynamicTable.buildFilterParams (IN:/RANGE:/GTE:/LTE:/ILIKE:/plain).
@@ -1,4 +1,21 @@
1
1
  import type { FilterOption } from './dynamic-columns-shim';
2
+ export interface UseFacetLoadersResult {
3
+ /**
4
+ * Stable per-field loader → `<facetsBase>?field=&q=&limit=`. Cached per
5
+ * `field+query`. Null `facetsBase` → yields `undefined` (column stays text).
6
+ */
7
+ getFacetLoader: (field: string) => ((q?: string) => Promise<FilterOption[]>) | undefined;
8
+ /**
9
+ * Warms the caches for a set of facet fields in ONE parallel burst (called
10
+ * when metadata resolves), so a popover opens instantly with values + counts
11
+ * instead of showing "Cargando…". Deduped by the field set; a field whose
12
+ * request fails is simply omitted (it degrades to lazy/text — the rest are
13
+ * unaffected). Resolved options also land in `facetOptions`.
14
+ */
15
+ prefetchFacets: (fields: string[]) => void;
16
+ /** Prefetched options per field (empty until `prefetchFacets` resolves). */
17
+ facetOptions: Map<string, FilterOption[]>;
18
+ }
2
19
  /**
3
20
  * Long-text / body columns aren't worth faceting (thousands of unique values,
4
21
  * each a paragraph). Heuristic on the metadata, not a hardcoded name list:
@@ -11,12 +28,11 @@ export declare function isLongTextColumn(c: {
11
28
  cellStyle?: string;
12
29
  }): boolean;
13
30
  /**
14
- * Returns `getFacetLoader(field)` a stable per-field loader that resolves the
15
- * column's distinct values + counts from `<facetsBase>?field=&q=&limit=`. Loader
16
- * identity is memoized per field (so it doesn't churn the config memo that
17
- * depends on it) and results are cached per `field+query`. Both caches reset
18
- * when `facetsBase` changes (model switch). A null `facetsBase` returns a
19
- * factory that yields `undefined` — the caller keeps the column as plain text.
31
+ * Facet option machinery: a stable per-field lazy loader (`getFacetLoader`, with
32
+ * a `field+query` result cache) plus `prefetchFacets` to warm every facet
33
+ * field's values up front and `facetOptions` holding those prewarmed results.
34
+ * Loader identity is memoized per field (so it doesn't churn the config memo
35
+ * that depends on it). Caches reset when `facetsBase` changes (model switch).
20
36
  */
21
- export declare function useFacetLoaders(facetsBase: string | null): (field: string) => ((q?: string) => Promise<FilterOption[]>) | undefined;
37
+ export declare function useFacetLoaders(facetsBase: string | null): UseFacetLoadersResult;
22
38
  //# sourceMappingURL=use-facet-loaders.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-facet-loaders.d.ts","sourceRoot":"","sources":["../src/use-facet-loaders.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAE1D;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE;IAClC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAQV;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,WAa7C,MAAM,WAVG,MAAM,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,cAqCtD"}
1
+ {"version":3,"file":"use-facet-loaders.d.ts","sourceRoot":"","sources":["../src/use-facet-loaders.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAE1D,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,cAAc,EAAE,CACd,KAAK,EAAE,MAAM,KACV,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,GAAG,SAAS,CAAA;IAC1D;;;;;;OAMG;IACH,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC1C,4EAA4E;IAC5E,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAA;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE;IAClC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAQV;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,qBAAqB,CAwFhF"}
@@ -4,7 +4,7 @@
4
4
  // per-field option loader + result cache lives here to guarantee they facet
5
5
  // identically (one board and its table sibling hit the same `/facets` endpoint
6
6
  // with the same caching).
7
- import { useCallback, useEffect, useRef } from 'react';
7
+ import { useCallback, useEffect, useRef, useState } from 'react';
8
8
  import { useApi } from './api-context';
9
9
  /**
10
10
  * Long-text / body columns aren't worth faceting (thousands of unique values,
@@ -21,22 +21,32 @@ export function isLongTextColumn(c) {
21
21
  return /^(body|description|content|notes?|comment|message|summary)$/i.test(c.key);
22
22
  }
23
23
  /**
24
- * Returns `getFacetLoader(field)` a stable per-field loader that resolves the
25
- * column's distinct values + counts from `<facetsBase>?field=&q=&limit=`. Loader
26
- * identity is memoized per field (so it doesn't churn the config memo that
27
- * depends on it) and results are cached per `field+query`. Both caches reset
28
- * when `facetsBase` changes (model switch). A null `facetsBase` returns a
29
- * factory that yields `undefined` — the caller keeps the column as plain text.
24
+ * Facet option machinery: a stable per-field lazy loader (`getFacetLoader`, with
25
+ * a `field+query` result cache) plus `prefetchFacets` to warm every facet
26
+ * field's values up front and `facetOptions` holding those prewarmed results.
27
+ * Loader identity is memoized per field (so it doesn't churn the config memo
28
+ * that depends on it). Caches reset when `facetsBase` changes (model switch).
30
29
  */
31
30
  export function useFacetLoaders(facetsBase) {
32
31
  const api = useApi();
33
32
  const loaderCache = useRef(new Map());
34
33
  const resultCache = useRef(new Map());
34
+ const prefetchSig = useRef(null);
35
+ const mountedRef = useRef(true);
36
+ const [facetOptions, setFacetOptions] = useState(new Map());
37
+ useEffect(() => {
38
+ mountedRef.current = true;
39
+ return () => {
40
+ mountedRef.current = false;
41
+ };
42
+ }, []);
35
43
  useEffect(() => {
36
44
  loaderCache.current.clear();
37
45
  resultCache.current.clear();
46
+ prefetchSig.current = null;
47
+ setFacetOptions(new Map());
38
48
  }, [facetsBase]);
39
- return useCallback((field) => {
49
+ const getFacetLoader = useCallback((field) => {
40
50
  if (!facetsBase)
41
51
  return undefined;
42
52
  const existing = loaderCache.current.get(field);
@@ -64,4 +74,35 @@ export function useFacetLoaders(facetsBase) {
64
74
  loaderCache.current.set(field, loader);
65
75
  return loader;
66
76
  }, [api, facetsBase]);
77
+ const prefetchFacets = useCallback((fields) => {
78
+ if (!facetsBase || fields.length === 0)
79
+ return;
80
+ const sig = [...fields].sort().join('|');
81
+ if (prefetchSig.current === sig)
82
+ return;
83
+ prefetchSig.current = sig;
84
+ // One parallel burst; the loader seeds its own `${field}::` cache so the
85
+ // subsequent lazy open (q === '') is a cache hit. allSettled so one bad
86
+ // field never blocks the others.
87
+ void Promise.allSettled(fields.map(async (field) => {
88
+ const loader = getFacetLoader(field);
89
+ if (!loader)
90
+ return null;
91
+ const opts = await loader();
92
+ return { field, opts };
93
+ })).then((results) => {
94
+ if (!mountedRef.current)
95
+ return;
96
+ setFacetOptions((prev) => {
97
+ const next = new Map(prev);
98
+ for (const r of results) {
99
+ if (r.status === 'fulfilled' && r.value) {
100
+ next.set(r.value.field, r.value.opts);
101
+ }
102
+ }
103
+ return next;
104
+ });
105
+ });
106
+ }, [facetsBase, getFacetLoader]);
107
+ return { getFacetLoader, prefetchFacets, facetOptions };
67
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "22.0.0",
3
+ "version": "23.0.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.8.0"
41
+ "@asteby/metacore-ui": "^2.9.0"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@tanstack/react-router": {
@@ -67,8 +67,8 @@
67
67
  "typescript": "^6.0.0",
68
68
  "vitest": "^4.0.0",
69
69
  "zustand": "^5.0.0",
70
- "@asteby/metacore-sdk": "3.2.0",
71
- "@asteby/metacore-ui": "2.8.0"
70
+ "@asteby/metacore-ui": "2.9.0",
71
+ "@asteby/metacore-sdk": "3.2.0"
72
72
  },
73
73
  "scripts": {
74
74
  "build": "tsc -p tsconfig.json",