@asteby/metacore-runtime-react 21.0.0 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 22.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bd30e57: Filtros pro para tabla y kanban dinámicos.
8
+ - **Etapa como select (kanban/tabla):** una columna `group_by` sin opciones
9
+ propias hereda las etapas del pipeline (`metadata.stages`, con su color) como
10
+ un select real en lugar de caer a un cuadro de texto "Contiene...".
11
+ - **Filtros por facetas:** las columnas de texto filtrables se convierten en un
12
+ selector de valores (`filterType: 'facet'`) que carga de forma perezosa los
13
+ valores distintos + su conteo desde `GET /data/:model/facets` al abrir el
14
+ popover, con búsqueda server-side. Degrada con gracia al input "Contiene..."
15
+ (ILIKE) si el endpoint falla, devuelve vacío o no está disponible, y mantiene
16
+ ese affordance de texto libre incluso cuando hay opciones. Las columnas de
17
+ texto largo (`body`/`description`, `cellStyle: truncate-text`, tipos
18
+ json/long_text) se quedan como texto plano. Nuevo `ColumnFilterControl`
19
+ `loadOptions` y `FilterOption.count` en `@asteby/metacore-ui`; hook compartido
20
+ `useFacetLoaders` y `filterType: 'facet'` en `@asteby/metacore-runtime-react`.
21
+ - **Búsqueda por columna del kanban:** cada lane tiene un icono de búsqueda que
22
+ expande un input inline (autofocus, colapsa con Escape) filtrando client-side
23
+ las tarjetas de esa columna por título y valores de campo visibles. Convive y
24
+ se combina (AND) con el embudo por campo, que ahora ofrece un select de
25
+ valores cuando el campo tiene opciones conocidas.
26
+ - **Panel de filtros rediseñado (nivel Linear/Notion):** el Sheet deja de ser
27
+ una lista de botones grises idénticos: cada campo es una fila tipo settings
28
+ con icono por tipo de dato (Hash número, Calendar fecha, CircleDot etapa, Tag
29
+ select/facet, ToggleLeft boolean, Type texto), label y resumen del valor
30
+ activo a la derecha ("Cualquiera" en muted si no hay). Nueva variante
31
+ `ColumnFilterControl` `variant='row'` con props `icon` y `valueSummary`.
32
+ Popovers con `rounded-xl`, sombra, header del campo (el input "Contiene..."
33
+ nunca es contenido crudo sin jerarquía), opciones con checkbox + dot de color
34
+ - count a la derecha. Filtros con selección agrupados arriba, resto
35
+ alfabético, footer sticky con conteo. Fila de chips activos removibles bajo la
36
+ toolbar (con dot de color del valor, p.ej. color de la etapa) y "Limpiar
37
+ todo". Header de lane del kanban con acciones (buscar/embudo) en hover-reveal
38
+ para un board limpio. Pensado dark-mode-first (muted/accent del theme).
39
+
40
+ La URL del endpoint de facetas se deriva como el endpoint de agregados de
41
+ DynamicTable: `<endpoint>/facets` (o `/data/<model>/facets` como fallback), así
42
+ funciona tanto con `endpoint="/data/:model/me"` como sin él.
43
+
44
+ ### Patch Changes
45
+
46
+ - Updated dependencies [bd30e57]
47
+ - @asteby/metacore-ui@2.8.0
48
+
49
+ ## 21.1.0
50
+
51
+ ### Minor Changes
52
+
53
+ - 2f21e1e: feat(kanban): agrupar filtros globales en un sheet lateral + filtro por columna
54
+ - **Filtros globales** dejan de spillear como chips inline: ahora un botón "Filtros"
55
+ (con contador de activos) abre un **sheet lateral** con todos los controles apilados
56
+ (mismo motor server-side `useDynamicFilters`). La búsqueda global queda inline.
57
+ - **Filtro por lane (stage):** cada columna tiene un icono de embudo → elige un campo
58
+ - un valor y **filtra solo las cards de ese stage** (client-side, instantáneo, sin
59
+ refetch). Un indicador bajo el header muestra el filtro activo (campo: valor) con
60
+ un botón para limpiarlo; el header muestra `filtradas/total`.
61
+
3
62
  ## 21.0.0
4
63
 
5
64
  ### Minor Changes
@@ -5,15 +5,23 @@ export interface FilterOption {
5
5
  value: string;
6
6
  icon?: string;
7
7
  color?: string;
8
+ /** Occurrence count for `facet` options (rendered muted, right-aligned). */
9
+ count?: number;
8
10
  }
9
11
  export interface ColumnFilterConfig {
10
- filterType: 'select' | 'boolean' | 'date_range' | 'number_range' | 'text' | string;
12
+ filterType: 'select' | 'boolean' | 'date_range' | 'number_range' | 'text' | 'facet' | string;
11
13
  filterKey: string;
12
14
  options: FilterOption[];
13
15
  selectedValues: string[];
14
16
  onFilterChange: (filterKey: string, values: string[]) => void;
15
17
  loading?: boolean;
16
18
  searchEndpoint?: string;
19
+ /**
20
+ * Lazy option loader for `facet` filters — resolves the column's distinct
21
+ * values + counts from the `/facets` endpoint. Passed straight through to
22
+ * `ColumnFilterControl.loadOptions`.
23
+ */
24
+ loadOptions?: (q?: string) => Promise<FilterOption[]>;
17
25
  }
18
26
  /** Signature for the host-provided `getDynamicColumns` factory. */
19
27
  export type GetDynamicColumns = (metadata: TableMetadata, handleAction: (action: string, row: any) => void, t: (key: string, options?: any) => string, language: string, columnFilterConfigs: Map<string, ColumnFilterConfig>, timeZone?: string, currency?: string) => ColumnDef<any>[];
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns-shim.d.ts","sourceRoot":"","sources":["../src/dynamic-columns-shim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;IAClF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAChD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,EACzC,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACpD,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,KAChB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;AAErB,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA"}
1
+ {"version":3,"file":"dynamic-columns-shim.d.ts","sourceRoot":"","sources":["../src/dynamic-columns-shim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;IAC5F,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAAA;CACxD;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAChD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,EACzC,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACpD,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,KAChB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;AAErB,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAiC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAqKhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA6mBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAiC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAqKhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA8mBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -490,6 +490,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
490
490
  filterSearchEndpoint: filterConfig.searchEndpoint,
491
491
  selectedValues: filterConfig.selectedValues,
492
492
  onFilterChange: filterConfig.onFilterChange,
493
+ loadOptions: filterConfig.loadOptions,
493
494
  };
494
495
  Object.assign(columnMeta, fm);
495
496
  }
@@ -40,6 +40,22 @@ export declare function selectCardColumns(metadata: TableMetadata, maxFields?: n
40
40
  title: ColumnDefinition | null;
41
41
  fields: ColumnDefinition[];
42
42
  };
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
+ /**
54
+ * Whether a card matches a free-text lane search: a case-insensitive substring
55
+ * over the card's title + every visible field value (`String(v)`). Empty query
56
+ * matches everything. Pure — exported for unit tests.
57
+ */
58
+ export declare function cardMatchesLaneQuery(card: any, cols: ColumnDefinition[], query: string): boolean;
43
59
  export interface DynamicKanbanProps {
44
60
  /** Model key as registered on the backend (e.g. "issue"). */
45
61
  model: string;
@@ -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;AAuC9B,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,qBAsXpB"}
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,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef, 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, Search, X } from 'lucide-react';
5
+ import { Calendar, CircleDot, Hash, ListFilter, MoreHorizontal, Search, Tag, ToggleLeft, Type, X, } from 'lucide-react';
6
6
  import { toast } from 'sonner';
7
- import { Badge, Button, Card, CardContent, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, 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
8
  import { ColumnFilterControl } from '@asteby/metacore-ui/data-table';
9
- import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib';
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';
12
12
  import { useMetadataCache } from './metadata-cache';
@@ -129,6 +129,77 @@ export function selectCardColumns(metadata, maxFields = 3) {
129
129
  .slice(0, maxFields);
130
130
  return { title, fields };
131
131
  }
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
+ /**
191
+ * Whether a card matches a free-text lane search: a case-insensitive substring
192
+ * over the card's title + every visible field value (`String(v)`). Empty query
193
+ * matches everything. Pure — exported for unit tests.
194
+ */
195
+ export function cardMatchesLaneQuery(card, cols, query) {
196
+ const q = query.trim().toLowerCase();
197
+ if (!q)
198
+ return true;
199
+ return cols.some((c) => String(card?.[c.key] ?? '')
200
+ .toLowerCase()
201
+ .includes(q));
202
+ }
132
203
  // ---------------------------------------------------------------------------
133
204
  // Theme hook (mirrors the private one in dynamic-columns / activity-renderer)
134
205
  // ---------------------------------------------------------------------------
@@ -199,7 +270,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
199
270
  // Shared metadata-driven filter engine — the SAME configs, option prefetch
200
271
  // and `f_<key>` serialization DynamicTable uses, so the board filters
201
272
  // identically to its table sibling.
202
- const { globalFilter, setGlobalFilter, columnFilterConfigs, filterParams, activeFilterCount, clearAll, } = useDynamicFilters(metadata, { defaultFilters });
273
+ const { dynamicFilters, globalFilter, setGlobalFilter, columnFilterConfigs, filterParams, activeFilterCount, handleDynamicFilterChange, clearAll, } = useDynamicFilters(metadata, { defaultFilters, model, endpoint });
203
274
  // ---- records fetch (same path as DynamicTable, single large page) ----
204
275
  const fetchData = useCallback(async () => {
205
276
  if (!metadata)
@@ -245,11 +316,48 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
245
316
  }
246
317
  return out;
247
318
  }, [metadata, columnFilterConfigs, t]);
319
+ // Split filters into active (with a selection) and the rest — the Sheet
320
+ // groups the active ones on top, the rest alphabetically. Also drives the
321
+ // removable chip row below the toolbar.
322
+ const { activeFields, inactiveFields } = useMemo(() => {
323
+ const active = filterFields.filter((f) => (f.config.selectedValues?.length ?? 0) > 0);
324
+ const inactive = filterFields
325
+ .filter((f) => (f.config.selectedValues?.length ?? 0) === 0)
326
+ .sort((a, b) => a.label.localeCompare(b.label));
327
+ return { activeFields: active, inactiveFields: inactive };
328
+ }, [filterFields]);
329
+ // Sheet (grouped global filters) open state + per-lane client-side filters.
330
+ // A lane filter narrows ONLY that stage's already-fetched cards by a field
331
+ // value — instant, no refetch — so a user can drill into one column without
332
+ // touching the rest of the board (the global filters, by contrast, refetch
333
+ // the whole board server-side).
334
+ const [filtersOpen, setFiltersOpen] = useState(false);
335
+ // Per-lane client-side narrowing. Two independent, AND-combined dimensions:
336
+ // - `field`/`value`: the funnel — a field-scoped substring match.
337
+ // - `query`: the lane search — a substring over the card title + every
338
+ // visible field value.
339
+ // A lane with neither is dropped from the map (so it reads as "unfiltered").
340
+ const [laneFilters, setLaneFilters] = useState({});
341
+ const updateLaneFilter = useCallback((stageKey, patch) => {
342
+ setLaneFilters((prev) => {
343
+ const merged = { ...prev[stageKey], ...patch };
344
+ const next = { ...prev };
345
+ const hasFunnel = !!(merged.field && merged.value?.trim());
346
+ const hasQuery = !!merged.query?.trim();
347
+ if (hasFunnel || hasQuery)
348
+ next[stageKey] = merged;
349
+ else
350
+ delete next[stageKey];
351
+ return next;
352
+ });
353
+ }, []);
248
354
  const stages = useMemo(() => (metadata ? deriveStages(metadata) : []), [metadata]);
249
355
  const groupByKey = metadata?.group_by || '';
250
356
  const transitions = metadata?.transitions;
251
357
  const grouped = useMemo(() => groupByStage(records, groupByKey, stages), [records, groupByKey, stages]);
252
358
  const { title: titleCol, fields: fieldCols } = useMemo(() => (metadata ? selectCardColumns(metadata) : { title: null, fields: [] }), [metadata]);
359
+ // Columns the lane search scans: the card title + its visible field cells.
360
+ const searchCols = useMemo(() => [titleCol, ...fieldCols].filter(Boolean), [titleCol, fieldCols]);
253
361
  // Row-placement actions resolved EXACTLY like DynamicTable's action column:
254
362
  // capability-gated (when a <PermissionsProvider> is mounted) and with the
255
363
  // implicit View/Edit/Delete trio materialized for CRUD models. An action the
@@ -345,25 +453,171 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
345
453
  order: Number.MAX_SAFE_INTEGER,
346
454
  });
347
455
  }
348
- 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.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: "h-8 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) => {
349
- const cards = grouped.get(stage.key) ?? [];
456
+ 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-0 p-0 sm:max-w-sm", children: [_jsx(SheetHeader, { className: "space-y-0 border-b px-4 py-3", children: _jsx(SheetTitle, { className: "text-sm", children: t('kanban.filters', { defaultValue: 'Filtros' }) }) }), _jsxs("div", { className: "flex flex-1 flex-col gap-1 overflow-y-auto px-4 py-3", children: [activeFields.length > 0 && (_jsxs(_Fragment, { children: [_jsx("p", { className: "px-0.5 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: t('kanban.activeFilters', {
457
+ defaultValue: 'Con filtros activos',
458
+ }) }), activeFields.map((field) => (_jsx(SheetFilterRow, { field: field, isStage: field.key === groupByKey }, field.key))), inactiveFields.length > 0 && (_jsx("div", { className: "my-2 border-t" }))] })), inactiveFields.map((field) => (_jsx(SheetFilterRow, { field: field, isStage: field.key === groupByKey }, field.key)))] }), _jsxs("div", { className: "sticky bottom-0 flex items-center justify-between gap-2 border-t bg-background px-4 py-3", children: [_jsx("span", { className: "text-xs text-muted-foreground tabular-nums", children: activeFilterCount > 0
459
+ ? t('kanban.activeCount', {
460
+ defaultValue: '{{count}} activos',
461
+ count: activeFilterCount,
462
+ })
463
+ : t('kanban.noActiveFilters', {
464
+ defaultValue: 'Sin filtros',
465
+ }) }), _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.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", "data-testid": "kanban-filter-chips", children: [activeFields.map((field) => {
466
+ const summary = summarizeFilterValues(field.config.selectedValues, field.config.options);
467
+ const dot = chipValueColor(field.config);
468
+ 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', {
469
+ defaultValue: 'Quitar filtro',
470
+ }), children: _jsx(X, { className: "h-3 w-3" }) })] }, field.key));
471
+ }), _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) => {
472
+ const allCards = grouped.get(stage.key) ?? [];
473
+ // Per-lane client-side narrowing (instant, scoped to this
474
+ // stage). The funnel (field/value) and the lane search
475
+ // (query) are AND-combined.
476
+ const laneFilter = laneFilters[stage.key];
477
+ 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));
484
+ }
485
+ if (laneFilter?.query?.trim()) {
486
+ cards = cards.filter((c) => cardMatchesLaneQuery(c, searchCols, laneFilter.query));
487
+ }
350
488
  const droppableAllowed = !activeId ||
351
489
  stage.key === activeStage ||
352
490
  isTransitionAllowed(transitions, activeStage, stage.key);
353
- 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));
491
+ return (_jsx(KanbanLane, { stage: stage, count: cards.length, totalCount: allCards.length, filterFields: filterFields, laneFilter: laneFilter, onFunnelChange: (f) => updateLaneFilter(stage.key, {
492
+ field: f?.field,
493
+ value: f?.value,
494
+ }), 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));
354
495
  }) }), _jsx(DragOverlay, { children: activeCard ? (_jsx(CardPreview, { card: activeCard, titleCol: titleCol, fieldCols: fieldCols, locale: i18n.language, timeZone: timeZone, currency: currency })) : null }), rowActionDialogs] })] }));
355
496
  }
356
- function KanbanLane({ stage, count, isDark, dimmed, disabled, children }) {
497
+ /**
498
+ * A per-data-type glyph for the Filtros panel rows (and their popover header):
499
+ * Hash for numbers, Calendar for dates, CircleDot for the pipeline stage, Tag
500
+ * for value pickers, ToggleLeft for booleans, Type for free text.
501
+ */
502
+ function filterTypeIcon(filterType, isStage) {
503
+ if (isStage)
504
+ return _jsx(CircleDot, { className: "h-3.5 w-3.5" });
505
+ switch (filterType) {
506
+ case 'number_range':
507
+ return _jsx(Hash, { className: "h-3.5 w-3.5" });
508
+ case 'date_range':
509
+ return _jsx(Calendar, { className: "h-3.5 w-3.5" });
510
+ case 'boolean':
511
+ return _jsx(ToggleLeft, { className: "h-3.5 w-3.5" });
512
+ case 'select':
513
+ case 'dynamic_select':
514
+ case 'facet':
515
+ return _jsx(Tag, { className: "h-3.5 w-3.5" });
516
+ default:
517
+ return _jsx(Type, { className: "h-3.5 w-3.5" });
518
+ }
519
+ }
520
+ function SheetFilterRow({ field, isStage, }) {
521
+ const summary = summarizeFilterValues(field.config.selectedValues, field.config.options);
522
+ return (_jsx(ColumnFilterControl, { variant: "row", align: "end", icon: filterTypeIcon(field.config.filterType, isStage), label: field.label, valueSummary: summary, 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, loadOptions: field.config.loadOptions }));
523
+ }
524
+ function KanbanLane({ stage, count, totalCount, filterFields, laneFilter, onFunnelChange, onQueryChange, isDark, dimmed, disabled, children, }) {
357
525
  const { t } = useTranslation();
358
526
  const { setNodeRef, isOver } = useDroppable({ id: stage.key, disabled });
359
527
  const headerStyle = generateBadgeStyles(stage.color || optionColor(stage.key), {
360
528
  isDark,
361
529
  });
362
- return (_jsxs("div", { ref: setNodeRef, className: "flex w-[300px] shrink-0 flex-col rounded-lg border bg-muted/30 transition-opacity", style: {
530
+ const funnelActive = !!(laneFilter?.field && laneFilter.value?.trim());
531
+ const queryActive = !!laneFilter?.query?.trim();
532
+ const laneActive = funnelActive || queryActive;
533
+ const activeFieldLabel = filterFields.find((f) => f.key === laneFilter?.field)?.label ??
534
+ laneFilter?.field;
535
+ // Inline lane search: a Search icon expands an Input; Escape or blur-while-
536
+ // empty collapses it. The query itself lives in the parent's laneFilters so
537
+ // it survives collapse and combines with the funnel.
538
+ const [searchOpen, setSearchOpen] = useState(queryActive);
539
+ const searchRef = useRef(null);
540
+ useEffect(() => {
541
+ if (searchOpen)
542
+ searchRef.current?.focus();
543
+ }, [searchOpen]);
544
+ const funnelValue = laneFilter?.field && laneFilter.value
545
+ ? { field: laneFilter.field, value: laneFilter.value }
546
+ : undefined;
547
+ return (_jsxs("div", { ref: setNodeRef, className: "group/lane flex w-[300px] shrink-0 flex-col rounded-xl border bg-muted/30 transition-opacity", style: {
363
548
  opacity: dimmed ? 0.45 : 1,
364
549
  outline: isOver && !disabled ? '2px solid var(--ring, #3b82f6)' : 'none',
365
550
  outlineOffset: 2,
366
- }, "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 })] }));
551
+ }, "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 transition-opacity focus-within:opacity-100 group-hover/lane:opacity-100 ${laneActive || searchOpen ? 'opacity-100' : 'opacity-0'}`, children: [_jsx("button", { type: "button", onClick: () => setSearchOpen((o) => !o), className: `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', {
552
+ defaultValue: 'Buscar en la columna',
553
+ }), 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) => {
554
+ if (e.key === 'Escape') {
555
+ onQueryChange('');
556
+ setSearchOpen(false);
557
+ }
558
+ }, onBlur: () => {
559
+ if (!laneFilter?.query?.trim())
560
+ setSearchOpen(false);
561
+ }, placeholder: t('kanban.searchLanePlaceholder', {
562
+ 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', {
564
+ defaultValue: 'Limpiar',
565
+ }), 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
+ }
567
+ // LaneFilterButton — the per-column funnel. Picks a field + a value and narrows
568
+ // ONLY this lane's cards (client-side, in the parent). Draft state lives here so
569
+ // typing doesn't refilter mid-keystroke; Apply/Enter commits, Limpiar clears.
570
+ function LaneFilterButton({ fields, value, onChange, }) {
571
+ const { t } = useTranslation();
572
+ const [open, setOpen] = useState(false);
573
+ const [field, setField] = useState(value?.field ?? fields[0]?.key ?? '');
574
+ const [text, setText] = useState(value?.value ?? '');
575
+ // Re-seed the draft from the committed filter each time the popover opens.
576
+ useEffect(() => {
577
+ if (open) {
578
+ setField(value?.field ?? fields[0]?.key ?? '');
579
+ setText(value?.value ?? '');
580
+ }
581
+ }, [open, value, fields]);
582
+ if (fields.length === 0)
583
+ 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;
591
+ const apply = () => {
592
+ if (field && text.trim())
593
+ onChange({ field, value: text });
594
+ else
595
+ onChange(null);
596
+ setOpen(false);
597
+ };
598
+ 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
+ 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) => {
601
+ setField(f);
602
+ // Reset the value when switching fields — a value picked
603
+ // for one field is meaningless for another.
604
+ 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) => {
613
+ if (e.key === 'Enter')
614
+ apply();
615
+ }, 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' }) })] })] })] }));
367
621
  }
368
622
  function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, currency, onClick, onAction, }) {
369
623
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
@@ -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;AAUnF,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,+BA2zBnB"}
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;AAWnF,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,+BA02BnB"}