@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 +59 -0
- package/dist/dynamic-columns-shim.d.ts +9 -1
- package/dist/dynamic-columns-shim.d.ts.map +1 -1
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +1 -0
- package/dist/dynamic-kanban.d.ts +16 -0
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +265 -11
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +51 -4
- package/dist/use-dynamic-filters.d.ts +19 -0
- package/dist/use-dynamic-filters.d.ts.map +1 -1
- package/dist/use-dynamic-filters.js +75 -5
- package/dist/use-facet-loaders.d.ts +22 -0
- package/dist/use-facet-loaders.d.ts.map +1 -0
- package/dist/use-facet-loaders.js +67 -0
- package/package.json +3 -3
- package/src/__tests__/dynamic-kanban.test.tsx +106 -5
- package/src/__tests__/use-dynamic-filters.test.tsx +182 -0
- package/src/dynamic-columns-shim.ts +9 -1
- package/src/dynamic-columns.tsx +1 -0
- package/src/dynamic-kanban.tsx +676 -36
- package/src/dynamic-table.tsx +52 -4
- package/src/use-dynamic-filters.ts +100 -5
- package/src/use-facet-loaders.ts +79 -0
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;
|
|
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,
|
|
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"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -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
|
}
|
package/dist/dynamic-kanban.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/dynamic-kanban.js
CHANGED
|
@@ -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.
|
|
349
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 })] }),
|
|
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;
|
|
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"}
|