@asteby/metacore-runtime-react 23.0.0 → 23.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/dynamic-kanban.d.ts +9 -16
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +21 -75
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +60 -7
- package/dist/filter-chips.d.ts +60 -0
- package/dist/filter-chips.d.ts.map +1 -0
- package/dist/filter-chips.js +93 -0
- package/package.json +3 -3
- package/src/__tests__/dynamic-kanban.test.tsx +40 -0
- package/src/__tests__/dynamic-table-filters.test.tsx +102 -0
- package/src/__tests__/filter-chips.test.tsx +119 -0
- package/src/dynamic-kanban.tsx +42 -141
- package/src/dynamic-table.tsx +69 -6
- package/src/filter-chips.tsx +175 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 08e18bf: Paridad de la experiencia pro de filtros en DynamicTable (vista tabla de los
|
|
8
|
+
módulos dinámicos), igualando al kanban.
|
|
9
|
+
- **Prefetch de facetas en la tabla:** al resolver la metadata se precargan en
|
|
10
|
+
paralelo (dedup por firma, `allSettled`) las facetas de todos los campos facet,
|
|
11
|
+
sembrando sus `options`. Abrir el filtro de una columna de texto en el header
|
|
12
|
+
ya muestra el combobox con valores + counts al instante, sin "Cargando…"; el
|
|
13
|
+
spinner queda solo para el refetch con búsqueda.
|
|
14
|
+
- **i18n de opciones en la tabla:** las opciones (stages y cualquier key i18n del
|
|
15
|
+
manifest) se traducen con `t(label, {defaultValue})` en runtime-react antes de
|
|
16
|
+
pasarlas al ui package, tanto en los filtros de header como en los chips y
|
|
17
|
+
resúmenes de valor.
|
|
18
|
+
- **Fila de chips de filtros activos sobre la tabla:** debajo de la toolbar,
|
|
19
|
+
removibles ("Campo: valor(es) ×" + "Limpiar todo"), con el color del valor
|
|
20
|
+
cuando aplica (p.ej. la etapa). Extraído a un componente compartido
|
|
21
|
+
`FilterChipsRow` (con `summarizeFilterValues`/`chipValueColor`/
|
|
22
|
+
`translateOptionLabels`) reusado por el kanban y la tabla — sin duplicar.
|
|
23
|
+
- **Stage-select en la tabla:** la columna `group_by`/stage sin opciones propias
|
|
24
|
+
ofrece el select con las etapas del pipeline (traducidas, con color), no un
|
|
25
|
+
cuadro de texto — sale del motor compartido; confirmado con test.
|
|
26
|
+
- **Header de lane del kanban:** los botones de búsqueda y embudo se ven SIEMPRE
|
|
27
|
+
(se quitó el hover-reveal, que nadie descubría) en muted con hover a foreground.
|
|
28
|
+
El embudo muestra un badge numérico con la cantidad de valores/filtros
|
|
29
|
+
aplicados en esa lane (como el badge del botón Filtros de la toolbar); la lupa
|
|
30
|
+
tiñe a primary + dot cuando hay búsqueda activa.
|
|
31
|
+
|
|
3
32
|
## 23.0.0
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
package/dist/dynamic-kanban.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { summarizeFilterValues, translateOptionLabels } from './filter-chips';
|
|
2
3
|
import type { TableMetadata, ColumnDefinition, StageMeta, StageTransition } from './types';
|
|
4
|
+
export { summarizeFilterValues, translateOptionLabels };
|
|
3
5
|
/**
|
|
4
6
|
* Resolves the board lanes for a kanban view. Prefers the model-level
|
|
5
7
|
* `metadata.stages` (the kernel's `stages[]`); falls back to the `group_by`
|
|
@@ -40,16 +42,6 @@ export declare function selectCardColumns(metadata: TableMetadata, maxFields?: n
|
|
|
40
42
|
title: ColumnDefinition | null;
|
|
41
43
|
fields: ColumnDefinition[];
|
|
42
44
|
};
|
|
43
|
-
/**
|
|
44
|
-
* Human-readable summary of a field's selected filter values for the removable
|
|
45
|
-
* chip row. Resolves option labels, unwraps the wire operators
|
|
46
|
-
* (`IN:`/`ILIKE:`/`RANGE:`/`GTE:`/`LTE:`/date `from_to`), and caps the list at
|
|
47
|
-
* `maxShown` values with a `+n` overflow. Pure — exported for unit tests.
|
|
48
|
-
*/
|
|
49
|
-
export declare function summarizeFilterValues(values: string[] | undefined, options: {
|
|
50
|
-
label: string;
|
|
51
|
-
value: string;
|
|
52
|
-
}[] | undefined, maxShown?: number): string;
|
|
53
45
|
/**
|
|
54
46
|
* Whether a card passes a lane funnel. Picked select/facet `values` match by
|
|
55
47
|
* equality (IN — the card's field value must be one of them); a free-text
|
|
@@ -62,13 +54,14 @@ export declare function cardMatchesLaneFunnel(card: any, filter: {
|
|
|
62
54
|
text?: string;
|
|
63
55
|
} | undefined): boolean;
|
|
64
56
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
57
|
+
* Count of applied criteria on a lane funnel: the number of picked select/facet
|
|
58
|
+
* `values`, else 1 for a free-text `text`, else 0. Drives the funnel's count
|
|
59
|
+
* badge. Pure — exported for unit tests.
|
|
68
60
|
*/
|
|
69
|
-
export declare function
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
export declare function laneFunnelCount(value: {
|
|
62
|
+
values?: string[];
|
|
63
|
+
text?: string;
|
|
64
|
+
} | undefined): number;
|
|
72
65
|
/**
|
|
73
66
|
* Whether a card matches a free-text lane search: a case-insensitive substring
|
|
74
67
|
* 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;
|
|
1
|
+
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
|
package/dist/dynamic-kanban.js
CHANGED
|
@@ -6,9 +6,10 @@ import { Calendar, CircleDot, Hash, ListFilter, MoreHorizontal, Search, Tag, Tog
|
|
|
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
8
|
import { ColumnFilterControl, FilterValueCombobox } from '@asteby/metacore-ui/data-table';
|
|
9
|
-
import { generateBadgeStyles, optionColor
|
|
9
|
+
import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib';
|
|
10
10
|
import { useApi } from './api-context';
|
|
11
11
|
import { useDynamicFilters } from './use-dynamic-filters';
|
|
12
|
+
import { FilterChipsRow, summarizeFilterValues, translateOptionLabels, } from './filter-chips';
|
|
12
13
|
import { useMetadataCache } from './metadata-cache';
|
|
13
14
|
import { ActivityValueRenderer } from './activity-value-renderer';
|
|
14
15
|
import { DynamicIcon } from './dynamic-icon';
|
|
@@ -16,6 +17,9 @@ import { isColumnVisibleInTable } from './column-visibility';
|
|
|
16
17
|
import { isRowActionVisible } from './dynamic-columns';
|
|
17
18
|
import { useCan, usePermissionsActive, resolveRowActions } from './permissions-context';
|
|
18
19
|
import { useDynamicRowActions } from './dynamic-row-actions';
|
|
20
|
+
// Re-exported for tests + backward-compat: these live in ./filter-chips now
|
|
21
|
+
// (shared with DynamicTable) but were historically imported from here.
|
|
22
|
+
export { summarizeFilterValues, translateOptionLabels };
|
|
19
23
|
// ---------------------------------------------------------------------------
|
|
20
24
|
// Pure helpers (exported for unit tests — no React, no transport)
|
|
21
25
|
// ---------------------------------------------------------------------------
|
|
@@ -129,64 +133,6 @@ export function selectCardColumns(metadata, maxFields = 3) {
|
|
|
129
133
|
.slice(0, maxFields);
|
|
130
134
|
return { title, fields };
|
|
131
135
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Human-readable summary of a field's selected filter values for the removable
|
|
134
|
-
* chip row. Resolves option labels, unwraps the wire operators
|
|
135
|
-
* (`IN:`/`ILIKE:`/`RANGE:`/`GTE:`/`LTE:`/date `from_to`), and caps the list at
|
|
136
|
-
* `maxShown` values with a `+n` overflow. Pure — exported for unit tests.
|
|
137
|
-
*/
|
|
138
|
-
export function summarizeFilterValues(values, options, maxShown = 2) {
|
|
139
|
-
if (!values || values.length === 0)
|
|
140
|
-
return '';
|
|
141
|
-
const opts = options ?? [];
|
|
142
|
-
const labelFor = (v) => opts.find((o) => o.value === v)?.label ?? v;
|
|
143
|
-
const first = values[0];
|
|
144
|
-
if (values.length === 1) {
|
|
145
|
-
if (first.startsWith('ILIKE:'))
|
|
146
|
-
return `"${first.slice(6)}"`;
|
|
147
|
-
if (first.startsWith('IN:')) {
|
|
148
|
-
return summarizeList(first.slice(3).split(','), labelFor, maxShown);
|
|
149
|
-
}
|
|
150
|
-
if (first.startsWith('RANGE:')) {
|
|
151
|
-
const [min, max] = first.slice(6).split(',');
|
|
152
|
-
return `${min || '…'} – ${max || '…'}`;
|
|
153
|
-
}
|
|
154
|
-
if (/^\d{4}-\d{2}-\d{2}_/.test(first))
|
|
155
|
-
return first.replace('_', ' – ');
|
|
156
|
-
}
|
|
157
|
-
if (first.startsWith('GTE:') || first.startsWith('LTE:')) {
|
|
158
|
-
const min = values.find((v) => v.startsWith('GTE:'))?.slice(4) ?? '';
|
|
159
|
-
const max = values.find((v) => v.startsWith('LTE:'))?.slice(4) ?? '';
|
|
160
|
-
return `${min || '…'} – ${max || '…'}`;
|
|
161
|
-
}
|
|
162
|
-
return summarizeList(values, labelFor, maxShown);
|
|
163
|
-
}
|
|
164
|
-
function summarizeList(items, labelFor, maxShown) {
|
|
165
|
-
const labels = items.map(labelFor);
|
|
166
|
-
if (labels.length <= maxShown)
|
|
167
|
-
return labels.join(', ');
|
|
168
|
-
return `${labels.slice(0, maxShown).join(', ')} +${labels.length - maxShown}`;
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* The color of a filter's first selected value (for the chip's dot) — e.g. a
|
|
172
|
-
* stage's palette color. Returns a resolved CSS color, or undefined for
|
|
173
|
-
* operator/range/free-text values that carry no option color.
|
|
174
|
-
*/
|
|
175
|
-
function chipValueColor(config) {
|
|
176
|
-
const sel = config.selectedValues;
|
|
177
|
-
if (!sel || sel.length === 0)
|
|
178
|
-
return undefined;
|
|
179
|
-
const first = sel[0];
|
|
180
|
-
let value = first;
|
|
181
|
-
if (first.startsWith('IN:'))
|
|
182
|
-
value = first.slice(3).split(',')[0];
|
|
183
|
-
else if (/^(ILIKE|RANGE|GTE|LTE):/.test(first))
|
|
184
|
-
return undefined;
|
|
185
|
-
else if (/^\d{4}-\d{2}-\d{2}_/.test(first))
|
|
186
|
-
return undefined;
|
|
187
|
-
const opt = config.options.find((o) => o.value === value);
|
|
188
|
-
return opt?.color ? resolveColorCss(opt.color) : undefined;
|
|
189
|
-
}
|
|
190
136
|
/**
|
|
191
137
|
* Whether a card passes a lane funnel. Picked select/facet `values` match by
|
|
192
138
|
* equality (IN — the card's field value must be one of them); a free-text
|
|
@@ -206,12 +152,16 @@ export function cardMatchesLaneFunnel(card, filter) {
|
|
|
206
152
|
return true;
|
|
207
153
|
}
|
|
208
154
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
155
|
+
* Count of applied criteria on a lane funnel: the number of picked select/facet
|
|
156
|
+
* `values`, else 1 for a free-text `text`, else 0. Drives the funnel's count
|
|
157
|
+
* badge. Pure — exported for unit tests.
|
|
212
158
|
*/
|
|
213
|
-
export function
|
|
214
|
-
|
|
159
|
+
export function laneFunnelCount(value) {
|
|
160
|
+
if (value?.values?.length)
|
|
161
|
+
return value.values.length;
|
|
162
|
+
if (value?.text?.trim())
|
|
163
|
+
return 1;
|
|
164
|
+
return 0;
|
|
215
165
|
}
|
|
216
166
|
/**
|
|
217
167
|
* Whether a card matches a free-text lane search: a case-insensitive substring
|
|
@@ -508,13 +458,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
|
|
|
508
458
|
})
|
|
509
459
|
: t('kanban.noActiveFilters', {
|
|
510
460
|
defaultValue: 'Sin filtros',
|
|
511
|
-
}) }), _jsxs(Button, { variant: "ghost", size: "sm", className: "h-7 gap-1 text-xs", onClick: clearAll, disabled: activeFilterCount === 0, children: [_jsx(X, { className: "h-3.5 w-3.5" }), t('kanban.clearAll', { defaultValue: 'Limpiar todo' }), activeFilterCount > 0 ? ` (${activeFilterCount})` : ''] })] })] })] }))] }), activeFields
|
|
512
|
-
const summary = summarizeFilterValues(field.config.selectedValues, field.config.options);
|
|
513
|
-
const dot = chipValueColor(field.config);
|
|
514
|
-
return (_jsxs(Badge, { variant: "secondary", className: "h-6 gap-1.5 rounded-md pl-2 pr-1 text-xs font-normal", children: [dot && (_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { backgroundColor: dot } })), _jsxs("span", { className: "font-medium", children: [field.label, ":"] }), _jsx("span", { className: "max-w-[180px] truncate text-muted-foreground", children: summary }), _jsx("button", { type: "button", onClick: () => field.config.onFilterChange(field.config.filterKey, []), className: "ml-0.5 rounded-sm p-0.5 transition-colors hover:bg-muted-foreground/20", "aria-label": t('kanban.removeFilter', {
|
|
515
|
-
defaultValue: 'Quitar filtro',
|
|
516
|
-
}), children: _jsx(X, { className: "h-3 w-3" }) })] }, field.key));
|
|
517
|
-
}), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 gap-1 px-2 text-xs text-muted-foreground", onClick: clearAll, children: t('kanban.clearAll', { defaultValue: 'Limpiar todo' }) })] })), _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) => {
|
|
461
|
+
}) }), _jsxs(Button, { variant: "ghost", size: "sm", className: "h-7 gap-1 text-xs", onClick: clearAll, disabled: activeFilterCount === 0, children: [_jsx(X, { className: "h-3.5 w-3.5" }), t('kanban.clearAll', { defaultValue: 'Limpiar todo' }), activeFilterCount > 0 ? ` (${activeFilterCount})` : ''] })] })] })] }))] }), _jsx(FilterChipsRow, { fields: activeFields, onClearAll: clearAll, "data-testid": "kanban-filter-chips" }), _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) => {
|
|
518
462
|
const allCards = grouped.get(stage.key) ?? [];
|
|
519
463
|
// Per-lane client-side narrowing (instant, scoped to this
|
|
520
464
|
// stage). The funnel (field/value) and the lane search
|
|
@@ -602,9 +546,9 @@ function KanbanLane({ stage, count, totalCount, filterFields, laneFilter, onFunn
|
|
|
602
546
|
opacity: dimmed ? 0.45 : 1,
|
|
603
547
|
outline: isOver && !disabled ? '2px solid var(--ring, #3b82f6)' : 'none',
|
|
604
548
|
outlineOffset: 2,
|
|
605
|
-
}, "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: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", 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: laneActive ? `${count}/${totalCount}` : count })] }), _jsxs("div", { className:
|
|
549
|
+
}, "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: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", 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: laneActive ? `${count}/${totalCount}` : count })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsxs("button", { type: "button", onClick: () => setSearchOpen((o) => !o), className: `relative flex size-6 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-foreground ${queryActive ? 'text-primary' : 'text-muted-foreground'}`, "aria-label": t('kanban.searchLane', {
|
|
606
550
|
defaultValue: 'Buscar en la columna',
|
|
607
|
-
}), children: _jsx(Search, { className: "h-3.5 w-3.5" }) }), _jsx(LaneFilterButton, { fields: filterFields, value: funnelValue, onChange: onFunnelChange })] })] }), searchOpen && (_jsx("div", { className: "px-3 pb-1.5", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { ref: searchRef, value: laneFilter?.query ?? '', onChange: (e) => onQueryChange(e.target.value), onKeyDown: (e) => {
|
|
551
|
+
}), children: [_jsx(Search, { className: "h-3.5 w-3.5" }), queryActive && (_jsx("span", { className: "absolute -right-0.5 -top-0.5 size-1.5 rounded-full bg-primary" }))] }), _jsx(LaneFilterButton, { fields: filterFields, value: funnelValue, onChange: onFunnelChange })] })] }), searchOpen && (_jsx("div", { className: "px-3 pb-1.5", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { ref: searchRef, value: laneFilter?.query ?? '', onChange: (e) => onQueryChange(e.target.value), onKeyDown: (e) => {
|
|
608
552
|
if (e.key === 'Escape') {
|
|
609
553
|
onQueryChange('');
|
|
610
554
|
setSearchOpen(false);
|
|
@@ -639,6 +583,8 @@ function LaneFilterButton({ fields, value, onChange, }) {
|
|
|
639
583
|
return null;
|
|
640
584
|
const active = !!(value &&
|
|
641
585
|
((value.values && value.values.length > 0) || value.text?.trim()));
|
|
586
|
+
// Number of applied criteria on this lane's funnel (drives the count badge).
|
|
587
|
+
const activeCount = laneFunnelCount(value);
|
|
642
588
|
// The value step mirrors the sheet: when the chosen field is a select or a
|
|
643
589
|
// facet (static options OR a lazy loader), render the SAME pro combobox —
|
|
644
590
|
// multi-select, searchable, with counts. Only a genuinely free-text field
|
|
@@ -659,9 +605,9 @@ function LaneFilterButton({ fields, value, onChange, }) {
|
|
|
659
605
|
onChange(null);
|
|
660
606
|
setOpen(false);
|
|
661
607
|
};
|
|
662
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children:
|
|
608
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: `relative 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', {
|
|
663
609
|
defaultValue: 'Filtrar columna',
|
|
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) => {
|
|
610
|
+
}), children: [_jsx(ListFilter, { className: "h-3.5 w-3.5" }), activeCount > 0 && (_jsx("span", { className: "absolute -right-1 -top-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-primary px-0.5 text-[9px] font-bold leading-none text-primary-foreground tabular-nums", children: activeCount }))] }) }), _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) => {
|
|
665
611
|
setField(f);
|
|
666
612
|
// Reset the value when switching fields — a value picked
|
|
667
613
|
// for one field is meaningless for another.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAYnF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,GACX,EAAE,iBAAiB,+BAw6BnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -25,6 +25,7 @@ import { useMetadataCache } from './metadata-cache';
|
|
|
25
25
|
import { useApi, useCurrentBranch } from './api-context';
|
|
26
26
|
import { defaultGetDynamicColumns, DATE_CELL_TYPES, aggregateOf, formatAggregateTotal } from './dynamic-columns';
|
|
27
27
|
import { useFacetLoaders, isLongTextColumn } from './use-facet-loaders';
|
|
28
|
+
import { FilterChipsRow, translateOptionLabels } from './filter-chips';
|
|
28
29
|
import { OptionsContext } from './options-context';
|
|
29
30
|
import { getSearchableColumnKeys } from './column-visibility';
|
|
30
31
|
import { useCan, usePermissionsActive, gateTableMetadata } from './permissions-context';
|
|
@@ -462,11 +463,15 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
462
463
|
// getDynamicColumns forwards `loadOptions` into the column meta). Facets base
|
|
463
464
|
// derived off the list endpoint exactly like the aggregate endpoint below.
|
|
464
465
|
const facetsBase = endpoint ? `${endpoint}/facets` : model ? `/data/${model}/facets` : null;
|
|
465
|
-
const { getFacetLoader } = useFacetLoaders(facetsBase);
|
|
466
|
+
const { getFacetLoader, prefetchFacets, facetOptions } = useFacetLoaders(facetsBase);
|
|
466
467
|
const columnFilterConfigs = useMemo(() => {
|
|
467
468
|
const map = new Map();
|
|
468
469
|
if (!metadata)
|
|
469
470
|
return map;
|
|
471
|
+
// Option labels arrive as manifest i18n keys; translate them here (the ui
|
|
472
|
+
// package has no i18n) so header filters, chips and value summaries show
|
|
473
|
+
// localized text. A raw value with no key falls through via defaultValue.
|
|
474
|
+
const tr = (label) => t(label, { defaultValue: label });
|
|
470
475
|
const stageOptions = (metadata.stages ?? []).map((s) => ({
|
|
471
476
|
label: s.label,
|
|
472
477
|
value: s.key,
|
|
@@ -500,6 +505,9 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
500
505
|
if (loader) {
|
|
501
506
|
fType = 'facet';
|
|
502
507
|
loadOptions = loader;
|
|
508
|
+
// Prewarmed values (from prefetchFacets) → the header filter
|
|
509
|
+
// opens with the list already there, no "Cargando…" flash.
|
|
510
|
+
options = facetOptions.get(f.column || f.key) ?? [];
|
|
503
511
|
}
|
|
504
512
|
}
|
|
505
513
|
if (fType === 'select' && options.length === 0 && !f.searchEndpoint)
|
|
@@ -507,12 +515,14 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
507
515
|
map.set(f.key, {
|
|
508
516
|
filterType: fType,
|
|
509
517
|
filterKey: f.column || f.key,
|
|
510
|
-
options,
|
|
518
|
+
options: translateOptionLabels(options, tr),
|
|
511
519
|
selectedValues: dynamicFilters[f.column || f.key] || [],
|
|
512
520
|
onFilterChange: handleDynamicFilterChange,
|
|
513
521
|
loading: f.searchEndpoint ? !filterOptionsMap.has(f.searchEndpoint) : false,
|
|
514
522
|
searchEndpoint: f.searchEndpoint,
|
|
515
|
-
loadOptions
|
|
523
|
+
loadOptions: loadOptions
|
|
524
|
+
? (q) => loadOptions(q).then((o) => translateOptionLabels(o, tr))
|
|
525
|
+
: undefined,
|
|
516
526
|
});
|
|
517
527
|
}
|
|
518
528
|
for (const c of metadata.columns ?? []) {
|
|
@@ -569,21 +579,64 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
569
579
|
if (loader) {
|
|
570
580
|
filterType = 'facet';
|
|
571
581
|
loadOptions = loader;
|
|
582
|
+
// Prewarmed values (from prefetchFacets) → instant open.
|
|
583
|
+
options = facetOptions.get(c.key) ?? [];
|
|
572
584
|
}
|
|
573
585
|
}
|
|
574
586
|
map.set(c.key, {
|
|
575
587
|
filterType,
|
|
576
588
|
filterKey: c.key,
|
|
577
|
-
options,
|
|
589
|
+
options: translateOptionLabels(options, tr),
|
|
578
590
|
selectedValues: dynamicFilters[c.key] || [],
|
|
579
591
|
onFilterChange: handleDynamicFilterChange,
|
|
580
592
|
loading: hasEndpoint && !filterOptionsMap.has(c.searchEndpoint),
|
|
581
593
|
searchEndpoint: c.searchEndpoint,
|
|
582
|
-
loadOptions
|
|
594
|
+
loadOptions: loadOptions
|
|
595
|
+
? (q) => loadOptions(q).then((o) => translateOptionLabels(o, tr))
|
|
596
|
+
: undefined,
|
|
583
597
|
});
|
|
584
598
|
}
|
|
585
599
|
return map;
|
|
586
|
-
}, [metadata, filterOptionsMap, dynamicFilters, handleDynamicFilterChange, facetsBase, getFacetLoader]);
|
|
600
|
+
}, [metadata, filterOptionsMap, dynamicFilters, handleDynamicFilterChange, facetsBase, getFacetLoader, facetOptions, t]);
|
|
601
|
+
// Prewarm every facet field once the configs settle, so a text column's
|
|
602
|
+
// header filter opens instantly with values + counts (same as the kanban).
|
|
603
|
+
const facetFieldsSig = useMemo(() => {
|
|
604
|
+
const keys = [];
|
|
605
|
+
for (const config of columnFilterConfigs.values()) {
|
|
606
|
+
if (config.filterType === 'facet')
|
|
607
|
+
keys.push(config.filterKey);
|
|
608
|
+
}
|
|
609
|
+
return keys.join('|');
|
|
610
|
+
}, [columnFilterConfigs]);
|
|
611
|
+
useEffect(() => {
|
|
612
|
+
if (!facetFieldsSig)
|
|
613
|
+
return;
|
|
614
|
+
prefetchFacets(facetFieldsSig.split('|'));
|
|
615
|
+
}, [facetFieldsSig, prefetchFacets]);
|
|
616
|
+
// Active-filter chips (shared row with the kanban). One chip per field with a
|
|
617
|
+
// selection; the label is the translated filter/column label.
|
|
618
|
+
const activeFilterChips = useMemo(() => {
|
|
619
|
+
if (!metadata)
|
|
620
|
+
return [];
|
|
621
|
+
const out = [];
|
|
622
|
+
for (const [key, config] of columnFilterConfigs) {
|
|
623
|
+
if ((config.selectedValues?.length ?? 0) === 0)
|
|
624
|
+
continue;
|
|
625
|
+
const f = metadata.filters?.find((x) => x.key === key);
|
|
626
|
+
const c = metadata.columns?.find((x) => x.key === key);
|
|
627
|
+
const rawLabel = f?.label || c?.label || key;
|
|
628
|
+
out.push({
|
|
629
|
+
key,
|
|
630
|
+
label: t(rawLabel, { defaultValue: rawLabel }),
|
|
631
|
+
config,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return out;
|
|
635
|
+
}, [metadata, columnFilterConfigs, t]);
|
|
636
|
+
const clearAllDynamicFilters = useCallback(() => {
|
|
637
|
+
setDynamicFilters({});
|
|
638
|
+
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
|
639
|
+
}, []);
|
|
587
640
|
const columns = useMemo(() => {
|
|
588
641
|
if (!viewMetadata)
|
|
589
642
|
return [];
|
|
@@ -629,7 +682,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
629
682
|
if (!metadata) {
|
|
630
683
|
return _jsx("div", { className: "text-center text-muted-foreground py-8", children: "Error al cargar la configuraci\u00F3n de la tabla." });
|
|
631
684
|
}
|
|
632
|
-
return (_jsxs(OptionsContext.Provider, { value: { optionsMap }, children: [_jsxs("div", { className: 'flex flex-col h-full min-h-0 w-full', children: [
|
|
685
|
+
return (_jsxs(OptionsContext.Provider, { value: { optionsMap }, children: [_jsxs("div", { className: 'flex flex-col h-full min-h-0 w-full', children: [_jsxs("div", { className: 'pb-4 shrink-0', children: [_jsx(DataTableToolbar, { table: table, searchPlaceholder: metadata.searchPlaceholder || 'Buscar...', filters: filters, activeFilters: dynamicFilters, onDynamicFilterChange: handleDynamicFilterChange, dateFilter: { value: dateRange, onChange: setDateRange, placeholder: 'Filtrar por fecha' }, perPageOptions: metadata.perPageOptions, onRefresh: handleRefresh, isLoading: loadingData, selectedCount: Object.keys(rowSelection).length, onBulkDelete: () => setShowBulkDeleteConfirm(true), extraActions: _jsxs(_Fragment, { children: [viewMetadata?.canExport && (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => setExportOpen(true), children: [_jsx(Download, { className: "h-4 w-4 mr-1" }), " Exportar"] })), viewMetadata?.canImport && (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => setImportOpen(true), children: [_jsx(Upload, { className: "h-4 w-4 mr-1" }), " Importar"] }))] }) }), activeFilterChips.length > 0 && (_jsx("div", { className: 'pt-2', children: _jsx(FilterChipsRow, { fields: activeFilterChips, onClearAll: clearAllDynamicFilters, "data-testid": 'table-filter-chips' }) }))] }), _jsx("div", { className: 'hidden sm:block flex-1 min-h-0 overflow-auto border rounded-md bg-card', children: _jsxs(Table, { noWrapper: true, className: cn('min-w-max w-full', aggregateColumns.length > 0 && Object.keys(footerTotals).length > 0 && 'h-full'), children: [_jsx(TableHeader, { className: 'sticky top-0 z-10', children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { className: 'border-b-0 hover:bg-transparent', children: headerGroup.headers.map((header) => {
|
|
633
686
|
const isActionsColumn = header.id === 'actions';
|
|
634
687
|
return (_jsx(TableHead, { colSpan: header.colSpan, style: header.column.columnDef.size ? { width: header.column.columnDef.size } : undefined, className: cn('bg-card border-b h-10', isActionsColumn && 'sticky right-0 z-20 bg-card shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]'), children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
|
|
635
688
|
}) }, headerGroup.id))) }), _jsx(TableBody, { children: loadingData && data.length === 0 ? (_jsx(TableSkeleton, {})) : table.getRowModel().rows?.length ? (_jsxs(_Fragment, { children: [table.getRowModel().rows.map((row) => (_jsx(TableRow, { "data-state": row.getIsSelected() && 'selected', className: cn(onRowClick && 'cursor-pointer'), onClick: onRowClick ? () => onRowClick(row.original) : undefined, children: row.getVisibleCells().map((cell) => {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable summary of a field's selected filter values for a chip / row.
|
|
3
|
+
* Resolves option labels, unwraps the wire operators
|
|
4
|
+
* (`IN:`/`ILIKE:`/`RANGE:`/`GTE:`/`LTE:`/date `from_to`), and caps the list at
|
|
5
|
+
* `maxShown` values with a `+n` overflow. Pure — exported for unit tests.
|
|
6
|
+
*/
|
|
7
|
+
export declare function summarizeFilterValues(values: string[] | undefined, options: {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}[] | undefined, maxShown?: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* The color of a filter's first selected value (for the chip's dot) — e.g. a
|
|
13
|
+
* stage's palette color. Returns a resolved CSS color, or undefined for
|
|
14
|
+
* operator/range/free-text values that carry no option color.
|
|
15
|
+
*/
|
|
16
|
+
export declare function chipValueColor(config: {
|
|
17
|
+
selectedValues: string[];
|
|
18
|
+
options: {
|
|
19
|
+
value: string;
|
|
20
|
+
color?: string;
|
|
21
|
+
}[];
|
|
22
|
+
}): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Translates option labels through the app translator (manifest i18n keys →
|
|
25
|
+
* localized text). A raw value with no matching key falls through to itself via
|
|
26
|
+
* `defaultValue`. Pure — exported for unit tests.
|
|
27
|
+
*/
|
|
28
|
+
export declare function translateOptionLabels<T extends {
|
|
29
|
+
label: string;
|
|
30
|
+
}>(options: T[], translate: (key: string) => string): T[];
|
|
31
|
+
export interface FilterChipField {
|
|
32
|
+
key: string;
|
|
33
|
+
label: string;
|
|
34
|
+
config: {
|
|
35
|
+
selectedValues: string[];
|
|
36
|
+
options: {
|
|
37
|
+
label: string;
|
|
38
|
+
value: string;
|
|
39
|
+
color?: string;
|
|
40
|
+
}[];
|
|
41
|
+
filterKey: string;
|
|
42
|
+
onFilterChange: (filterKey: string, values: string[]) => void;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface FilterChipsRowProps {
|
|
46
|
+
/** Fields with an active selection (one chip each). */
|
|
47
|
+
fields: FilterChipField[];
|
|
48
|
+
/** Clears every filter (the trailing "Limpiar todo"). */
|
|
49
|
+
onClearAll: () => void;
|
|
50
|
+
className?: string;
|
|
51
|
+
'data-testid'?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The removable active-filter chip row shown under a board/table toolbar. Each
|
|
55
|
+
* chip: an optional value-color dot, "Campo: valor(es)" (capped + `+n`), and an
|
|
56
|
+
* X that clears that field; a trailing "Limpiar todo". Renders nothing when no
|
|
57
|
+
* field is active.
|
|
58
|
+
*/
|
|
59
|
+
export declare function FilterChipsRow({ fields, onClearAll, className, 'data-testid': testId, }: FilterChipsRowProps): import("react").JSX.Element | null;
|
|
60
|
+
//# sourceMappingURL=filter-chips.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-chips.d.ts","sourceRoot":"","sources":["../src/filter-chips.tsx"],"names":[],"mappings":"AASA;;;;;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,CAsBR;AAYD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE;IACnC,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC/C,GAAG,MAAM,GAAG,SAAS,CAUrB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EAC7D,OAAO,EAAE,CAAC,EAAE,EACZ,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GACnC,CAAC,EAAE,CAEL;AAED,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE;QACJ,cAAc,EAAE,MAAM,EAAE,CAAA;QACxB,OAAO,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;QAC3D,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;KAChE,CAAA;CACJ;AAED,MAAM,WAAW,mBAAmB;IAChC,uDAAuD;IACvD,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,yDAAyD;IACzD,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC3B,MAAM,EACN,UAAU,EACV,SAAS,EACT,aAAa,EAAE,MAAM,GACxB,EAAE,mBAAmB,sCA0DrB"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Shared filter presentation: the removable active-filter chip row + the pure
|
|
3
|
+
// helpers that summarize a filter's value and resolve its color. Used by BOTH
|
|
4
|
+
// DynamicKanban and DynamicTable so the two surfaces render identical chips
|
|
5
|
+
// (one component, no drift).
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { X } from 'lucide-react';
|
|
8
|
+
import { Badge, Button } from '@asteby/metacore-ui/primitives';
|
|
9
|
+
import { resolveColorCss } from '@asteby/metacore-ui/lib';
|
|
10
|
+
/**
|
|
11
|
+
* Human-readable summary of a field's selected filter values for a chip / row.
|
|
12
|
+
* Resolves option labels, unwraps the wire operators
|
|
13
|
+
* (`IN:`/`ILIKE:`/`RANGE:`/`GTE:`/`LTE:`/date `from_to`), and caps the list at
|
|
14
|
+
* `maxShown` values with a `+n` overflow. Pure — exported for unit tests.
|
|
15
|
+
*/
|
|
16
|
+
export function summarizeFilterValues(values, options, maxShown = 2) {
|
|
17
|
+
if (!values || values.length === 0)
|
|
18
|
+
return '';
|
|
19
|
+
const opts = options ?? [];
|
|
20
|
+
const labelFor = (v) => opts.find((o) => o.value === v)?.label ?? v;
|
|
21
|
+
const first = values[0];
|
|
22
|
+
if (values.length === 1) {
|
|
23
|
+
if (first.startsWith('ILIKE:'))
|
|
24
|
+
return `"${first.slice(6)}"`;
|
|
25
|
+
if (first.startsWith('IN:')) {
|
|
26
|
+
return summarizeList(first.slice(3).split(','), labelFor, maxShown);
|
|
27
|
+
}
|
|
28
|
+
if (first.startsWith('RANGE:')) {
|
|
29
|
+
const [min, max] = first.slice(6).split(',');
|
|
30
|
+
return `${min || '…'} – ${max || '…'}`;
|
|
31
|
+
}
|
|
32
|
+
if (/^\d{4}-\d{2}-\d{2}_/.test(first))
|
|
33
|
+
return first.replace('_', ' – ');
|
|
34
|
+
}
|
|
35
|
+
if (first.startsWith('GTE:') || first.startsWith('LTE:')) {
|
|
36
|
+
const min = values.find((v) => v.startsWith('GTE:'))?.slice(4) ?? '';
|
|
37
|
+
const max = values.find((v) => v.startsWith('LTE:'))?.slice(4) ?? '';
|
|
38
|
+
return `${min || '…'} – ${max || '…'}`;
|
|
39
|
+
}
|
|
40
|
+
return summarizeList(values, labelFor, maxShown);
|
|
41
|
+
}
|
|
42
|
+
function summarizeList(items, labelFor, maxShown) {
|
|
43
|
+
const labels = items.map(labelFor);
|
|
44
|
+
if (labels.length <= maxShown)
|
|
45
|
+
return labels.join(', ');
|
|
46
|
+
return `${labels.slice(0, maxShown).join(', ')} +${labels.length - maxShown}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The color of a filter's first selected value (for the chip's dot) — e.g. a
|
|
50
|
+
* stage's palette color. Returns a resolved CSS color, or undefined for
|
|
51
|
+
* operator/range/free-text values that carry no option color.
|
|
52
|
+
*/
|
|
53
|
+
export function chipValueColor(config) {
|
|
54
|
+
const sel = config.selectedValues;
|
|
55
|
+
if (!sel || sel.length === 0)
|
|
56
|
+
return undefined;
|
|
57
|
+
const first = sel[0];
|
|
58
|
+
let value = first;
|
|
59
|
+
if (first.startsWith('IN:'))
|
|
60
|
+
value = first.slice(3).split(',')[0];
|
|
61
|
+
else if (/^(ILIKE|RANGE|GTE|LTE):/.test(first))
|
|
62
|
+
return undefined;
|
|
63
|
+
else if (/^\d{4}-\d{2}-\d{2}_/.test(first))
|
|
64
|
+
return undefined;
|
|
65
|
+
const opt = config.options.find((o) => o.value === value);
|
|
66
|
+
return opt?.color ? resolveColorCss(opt.color) : undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Translates option labels through the app translator (manifest i18n keys →
|
|
70
|
+
* localized text). A raw value with no matching key falls through to itself via
|
|
71
|
+
* `defaultValue`. Pure — exported for unit tests.
|
|
72
|
+
*/
|
|
73
|
+
export function translateOptionLabels(options, translate) {
|
|
74
|
+
return options.map((o) => ({ ...o, label: translate(o.label) }));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The removable active-filter chip row shown under a board/table toolbar. Each
|
|
78
|
+
* chip: an optional value-color dot, "Campo: valor(es)" (capped + `+n`), and an
|
|
79
|
+
* X that clears that field; a trailing "Limpiar todo". Renders nothing when no
|
|
80
|
+
* field is active.
|
|
81
|
+
*/
|
|
82
|
+
export function FilterChipsRow({ fields, onClearAll, className, 'data-testid': testId, }) {
|
|
83
|
+
const { t } = useTranslation();
|
|
84
|
+
if (fields.length === 0)
|
|
85
|
+
return null;
|
|
86
|
+
return (_jsxs("div", { className: `flex flex-wrap items-center gap-1.5${className ? ` ${className}` : ''}`, "data-testid": testId, children: [fields.map((field) => {
|
|
87
|
+
const summary = summarizeFilterValues(field.config.selectedValues, field.config.options);
|
|
88
|
+
const dot = chipValueColor(field.config);
|
|
89
|
+
return (_jsxs(Badge, { variant: "secondary", className: "h-6 gap-1.5 rounded-md pl-2 pr-1 text-xs font-normal", children: [dot && (_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { backgroundColor: dot } })), _jsxs("span", { className: "font-medium", children: [field.label, ":"] }), _jsx("span", { className: "max-w-[180px] truncate text-muted-foreground", children: summary }), _jsx("button", { type: "button", onClick: () => field.config.onFilterChange(field.config.filterKey, []), className: "ml-0.5 rounded-sm p-0.5 transition-colors hover:bg-muted-foreground/20", "aria-label": t('filters.removeFilter', {
|
|
90
|
+
defaultValue: 'Quitar filtro',
|
|
91
|
+
}), children: _jsx(X, { className: "h-3 w-3" }) })] }, field.key));
|
|
92
|
+
}), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 gap-1 px-2 text-xs text-muted-foreground", onClick: onClearAll, children: t('filters.clearAll', { defaultValue: 'Limpiar todo' }) })] }));
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.1.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -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-
|
|
71
|
-
"@asteby/metacore-
|
|
70
|
+
"@asteby/metacore-sdk": "3.2.0",
|
|
71
|
+
"@asteby/metacore-ui": "2.9.0"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"build": "tsc -p tsconfig.json",
|