@asteby/metacore-runtime-react 9.0.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/column-visibility.d.ts +22 -0
- package/dist/column-visibility.d.ts.map +1 -0
- package/dist/column-visibility.js +40 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +4 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +17 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/column-visibility.test.ts +116 -0
- package/src/column-visibility.ts +43 -0
- package/src/dynamic-columns.tsx +4 -1
- package/src/dynamic-table.tsx +20 -2
- package/src/index.ts +4 -0
- package/src/types.ts +25 -0
- package/tsconfig.json +2 -1
- package/dist/__tests__/dynamic-form.test.d.ts +0 -2
- package/dist/__tests__/dynamic-form.test.d.ts.map +0 -1
- package/dist/__tests__/dynamic-form.test.js +0 -93
- package/dist/__tests__/dynamic-relation.test.d.ts +0 -2
- package/dist/__tests__/dynamic-relation.test.d.ts.map +0 -1
- package/dist/__tests__/dynamic-relation.test.js +0 -228
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 9.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2e50839: feat(runtime-react): leer `visibility` y `searchable` en metadata de columnas.
|
|
8
|
+
- `ColumnDefinition` tipa los nuevos campos `visibility?` (`"all" | "table" | "modal" | "list"`) y `searchable?` que el kernel ya emite (`manifest.ColumnDef`). Backwards compat: zero-value preserva el comportamiento previo.
|
|
9
|
+
- `<DynamicTable>` ahora oculta del listado las columnas con `visibility === "modal"` (y `"list"`) además del legacy `hidden`. Las columnas sin `visibility` o con `"all" | "table"` siguen visibles.
|
|
10
|
+
- Cuando al menos una columna declara `searchable` el SDK acota el global search a esas columnas vía el nuevo query param `search_columns=<keys>`. Si todas las columnas se opt-out (`searchable: false`), el SDK deja de mandar `search` al backend. Si ninguna columna trae el flag (kernel anterior a v0.8.x), no se cambia nada.
|
|
11
|
+
- Nuevos helpers públicos `isColumnVisibleInTable(col)` y `getSearchableColumnKeys(metadata)` exportados desde el barrel; tests con metadata mock cubren los pasos legacy + opt-in + opt-out total.
|
|
12
|
+
|
|
3
13
|
## 9.0.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ColumnDefinition, TableMetadata } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a column should render in a list/index table view.
|
|
4
|
+
*
|
|
5
|
+
* A column is hidden when its `visibility` is scoped away from the table
|
|
6
|
+
* (`'modal'`: only the create/edit dialog; `'list'`: only API payloads) or
|
|
7
|
+
* when the legacy `hidden` boolean is set. Empty / `'all'` / `'table'` keep
|
|
8
|
+
* the column visible — preserving zero-value behaviour for metadata emitted
|
|
9
|
+
* by older kernels that don't set `visibility` at all.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isColumnVisibleInTable(col: ColumnDefinition): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the keys of columns that opt into the model's full-text search,
|
|
14
|
+
* or `null` when no column declares `searchable` at all.
|
|
15
|
+
*
|
|
16
|
+
* `null` is the legacy signal: the host should NOT narrow the search request
|
|
17
|
+
* (every column participates, matching pre-Searchable kernels). An empty
|
|
18
|
+
* array is meaningful — it means every column has been explicitly opted out
|
|
19
|
+
* and the host should disable the global search input.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getSearchableColumnKeys(metadata: Pick<TableMetadata, 'columns'>): string[] | null;
|
|
22
|
+
//# sourceMappingURL=column-visibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column-visibility.d.ts","sourceRoot":"","sources":["../src/column-visibility.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE9D;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAKrE;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACnC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GACzC,MAAM,EAAE,GAAG,IAAI,CAKjB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Pure helpers that map kernel `manifest.ColumnDef` metadata flags
|
|
2
|
+
// (Visibility, Searchable) into client-side decisions:
|
|
3
|
+
// - which columns the dynamic table should render
|
|
4
|
+
// - which column keys are in scope for the global search
|
|
5
|
+
//
|
|
6
|
+
// Kept side-effect free and free of React/UI imports so the same logic can
|
|
7
|
+
// be tested with plain unit tests against mock metadata.
|
|
8
|
+
/**
|
|
9
|
+
* Whether a column should render in a list/index table view.
|
|
10
|
+
*
|
|
11
|
+
* A column is hidden when its `visibility` is scoped away from the table
|
|
12
|
+
* (`'modal'`: only the create/edit dialog; `'list'`: only API payloads) or
|
|
13
|
+
* when the legacy `hidden` boolean is set. Empty / `'all'` / `'table'` keep
|
|
14
|
+
* the column visible — preserving zero-value behaviour for metadata emitted
|
|
15
|
+
* by older kernels that don't set `visibility` at all.
|
|
16
|
+
*/
|
|
17
|
+
export function isColumnVisibleInTable(col) {
|
|
18
|
+
if (col.hidden)
|
|
19
|
+
return false;
|
|
20
|
+
const v = col.visibility;
|
|
21
|
+
if (!v)
|
|
22
|
+
return true;
|
|
23
|
+
return v === 'all' || v === 'table';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns the keys of columns that opt into the model's full-text search,
|
|
27
|
+
* or `null` when no column declares `searchable` at all.
|
|
28
|
+
*
|
|
29
|
+
* `null` is the legacy signal: the host should NOT narrow the search request
|
|
30
|
+
* (every column participates, matching pre-Searchable kernels). An empty
|
|
31
|
+
* array is meaningful — it means every column has been explicitly opted out
|
|
32
|
+
* and the host should disable the global search input.
|
|
33
|
+
*/
|
|
34
|
+
export function getSearchableColumnKeys(metadata) {
|
|
35
|
+
const cols = metadata.columns ?? [];
|
|
36
|
+
const declared = cols.some(c => typeof c.searchable === 'boolean');
|
|
37
|
+
if (!declared)
|
|
38
|
+
return null;
|
|
39
|
+
return cols.filter(c => c.searchable === true).map(c => c.key);
|
|
40
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAsCA,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;AAwHD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAqXnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -18,6 +18,7 @@ import { DataTableColumnHeader, FilterableColumnHeader, } from '@asteby/metacore
|
|
|
18
18
|
import { generateBadgeStyles, getInitials } from '@asteby/metacore-ui/lib';
|
|
19
19
|
import { OptionsContext } from './options-context';
|
|
20
20
|
import { DynamicIcon } from './dynamic-icon';
|
|
21
|
+
import { isColumnVisibleInTable } from './column-visibility';
|
|
21
22
|
const defaultGetImageUrl = (path) => path;
|
|
22
23
|
const getNestedValue = (obj, path) => path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
|
23
24
|
const lowerFirst = (value) => {
|
|
@@ -123,7 +124,9 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
123
124
|
},
|
|
124
125
|
];
|
|
125
126
|
metadata.columns.forEach((col) => {
|
|
126
|
-
|
|
127
|
+
// Honors both the legacy `hidden` boolean and the kernel's
|
|
128
|
+
// `visibility` scope (skips `'modal'` and `'list'`).
|
|
129
|
+
if (!isColumnVisibleInTable(col))
|
|
127
130
|
return;
|
|
128
131
|
const translatedLabel = col.label;
|
|
129
132
|
const filterConfig = filterConfigs?.get(col.key);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAUnF,UAAU,iBAAiB;IACvB,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,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;CACxC;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,GAC/C,EAAE,iBAAiB,2CA+sBnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -27,6 +27,7 @@ import { useApi, useCurrentBranch } from './api-context';
|
|
|
27
27
|
import { defaultGetDynamicColumns } from './dynamic-columns';
|
|
28
28
|
import { OptionsContext } from './options-context';
|
|
29
29
|
import { ActionModalDispatcher } from './action-modal-dispatcher';
|
|
30
|
+
import { getSearchableColumnKeys } from './column-visibility';
|
|
30
31
|
import { DynamicRecordDialog } from './dialogs/dynamic-record';
|
|
31
32
|
import { ExportDialog } from './dialogs/export';
|
|
32
33
|
import { ImportDialog } from './dialogs/import';
|
|
@@ -253,14 +254,27 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
253
254
|
};
|
|
254
255
|
initMetadataAndOptions();
|
|
255
256
|
}, [model]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
257
|
+
// Derived from `metadata.columns[].searchable`. `null` means the kernel
|
|
258
|
+
// didn't emit the flag for any column → preserve legacy "search every
|
|
259
|
+
// column" behaviour by not narrowing the request. An empty array means
|
|
260
|
+
// every column was explicitly opted out → skip sending `search` at all.
|
|
261
|
+
const searchableKeys = useMemo(() => (metadata ? getSearchableColumnKeys(metadata) : null), [metadata]);
|
|
256
262
|
const buildFilterParams = useCallback(() => {
|
|
257
263
|
const params = {};
|
|
258
264
|
if (sorting.length > 0) {
|
|
259
265
|
params.sortBy = sorting[0].id;
|
|
260
266
|
params.order = sorting[0].desc ? 'desc' : 'asc';
|
|
261
267
|
}
|
|
262
|
-
if (globalFilter)
|
|
263
|
-
|
|
268
|
+
if (globalFilter) {
|
|
269
|
+
if (searchableKeys === null) {
|
|
270
|
+
params.search = globalFilter;
|
|
271
|
+
}
|
|
272
|
+
else if (searchableKeys.length > 0) {
|
|
273
|
+
params.search = globalFilter;
|
|
274
|
+
params.search_columns = searchableKeys.join(',');
|
|
275
|
+
}
|
|
276
|
+
// searchableKeys === [] → drop the search request entirely
|
|
277
|
+
}
|
|
264
278
|
columnFilters.forEach((filter) => { params[`f_${filter.id}`] = filter.value; });
|
|
265
279
|
if (defaultFilters)
|
|
266
280
|
Object.entries(defaultFilters).forEach(([key, value]) => { params[`f_${key}`] = value; });
|
|
@@ -286,7 +300,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
286
300
|
params['f_created_at'] = `${startDate}_${endDate}`;
|
|
287
301
|
}
|
|
288
302
|
return params;
|
|
289
|
-
}, [sorting, globalFilter, columnFilters, defaultFilters, dynamicFilters, dateRange]);
|
|
303
|
+
}, [sorting, globalFilter, columnFilters, defaultFilters, dynamicFilters, dateRange, searchableKeys]);
|
|
290
304
|
const hasActiveFilters = useMemo(() => {
|
|
291
305
|
if (globalFilter)
|
|
292
306
|
return true;
|
package/dist/index.d.ts
CHANGED
|
@@ -19,4 +19,5 @@ export { ImportDialog } from './dialogs/import';
|
|
|
19
19
|
export { DynamicCRUDPage, type DynamicCRUDPageProps, type DynamicCRUDPageStrings, type DynamicCRUDPageClasses, } from './dynamic-crud-page';
|
|
20
20
|
export { DynamicRelation, type DynamicRelationProps, type DynamicRelationStrings, type DynamicRelationKind, buildRelationFilterParams, buildCreatePayload, deriveRelationFormFields, relationRowKey, } from './dynamic-relation';
|
|
21
21
|
export { registerModelExtension, getModelExtension, clearModelExtensions, type ModelExtension, type ModelExtensionProps, } from './model-extension-registry';
|
|
22
|
+
export { isColumnVisibleInTable, getSearchableColumnKeys, } from './column-visibility';
|
|
22
23
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GAC3B,MAAM,4BAA4B,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GAC3B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACH,sBAAsB,EACtB,uBAAuB,GAC1B,MAAM,qBAAqB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -23,3 +23,4 @@ export { ImportDialog } from './dialogs/import';
|
|
|
23
23
|
export { DynamicCRUDPage, } from './dynamic-crud-page';
|
|
24
24
|
export { DynamicRelation, buildRelationFilterParams, buildCreatePayload, deriveRelationFormFields, relationRowKey, } from './dynamic-relation';
|
|
25
25
|
export { registerModelExtension, getModelExtension, clearModelExtensions, } from './model-extension-registry';
|
|
26
|
+
export { isColumnVisibleInTable, getSearchableColumnKeys, } from './column-visibility';
|
package/dist/types.d.ts
CHANGED
|
@@ -26,6 +26,17 @@ export interface FilterDefinition {
|
|
|
26
26
|
}[];
|
|
27
27
|
searchEndpoint?: string;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Where a column is rendered. Mirrors `manifest.ColumnDef.Visibility` in the
|
|
31
|
+
* kernel:
|
|
32
|
+
* - `''` / `'all'` — visible everywhere (default).
|
|
33
|
+
* - `'table'` — only the list/index page.
|
|
34
|
+
* - `'modal'` — only the create/edit modal.
|
|
35
|
+
* - `'list'` — only API list payloads (omitted from UI).
|
|
36
|
+
* Hosts may extend the union with their own scopes; the SDK only acts on the
|
|
37
|
+
* canonical values above.
|
|
38
|
+
*/
|
|
39
|
+
export type ColumnVisibility = 'all' | 'table' | 'modal' | 'list' | (string & {});
|
|
29
40
|
export interface ColumnDefinition {
|
|
30
41
|
key: string;
|
|
31
42
|
label: string;
|
|
@@ -33,6 +44,19 @@ export interface ColumnDefinition {
|
|
|
33
44
|
sortable: boolean;
|
|
34
45
|
filterable: boolean;
|
|
35
46
|
hidden?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Scopes where this column is rendered. When `'modal'` (or `'list'`) the
|
|
49
|
+
* column is hidden from the table even if `hidden` is unset. Empty/`'all'`/
|
|
50
|
+
* `'table'` keep the column visible. See `column-visibility.ts`.
|
|
51
|
+
*/
|
|
52
|
+
visibility?: ColumnVisibility;
|
|
53
|
+
/**
|
|
54
|
+
* Opts the column into the model's full-text/contains search. Independent
|
|
55
|
+
* of `filterable` (which drives column-level filter chips). When at least
|
|
56
|
+
* one column declares `searchable`, the SDK narrows the global search to
|
|
57
|
+
* those columns; otherwise legacy "search every column" behaviour applies.
|
|
58
|
+
*/
|
|
59
|
+
searchable?: boolean;
|
|
36
60
|
styleConfig?: Record<string, any>;
|
|
37
61
|
tooltip?: string;
|
|
38
62
|
description?: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.1.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"react-dom": "^19.2.4",
|
|
57
57
|
"react-i18next": "^17.0.0",
|
|
58
58
|
"sonner": "^2.0.0",
|
|
59
|
+
"tsx": "^4.21.0",
|
|
59
60
|
"typescript": "^5.6.0",
|
|
60
61
|
"vitest": "^4.0.0",
|
|
61
62
|
"zustand": "^5.0.0",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isColumnVisibleInTable,
|
|
5
|
+
getSearchableColumnKeys,
|
|
6
|
+
} from '../column-visibility'
|
|
7
|
+
import type { ColumnDefinition, TableMetadata } from '../types'
|
|
8
|
+
|
|
9
|
+
const baseCol = (overrides: Partial<ColumnDefinition> = {}): ColumnDefinition => ({
|
|
10
|
+
key: 'name',
|
|
11
|
+
label: 'Name',
|
|
12
|
+
type: 'text',
|
|
13
|
+
sortable: true,
|
|
14
|
+
filterable: false,
|
|
15
|
+
...overrides,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const baseMeta = (columns: ColumnDefinition[]): TableMetadata => ({
|
|
19
|
+
title: 'Mock',
|
|
20
|
+
endpoint: '/data/mock',
|
|
21
|
+
columns,
|
|
22
|
+
actions: [],
|
|
23
|
+
perPageOptions: [10],
|
|
24
|
+
defaultPerPage: 10,
|
|
25
|
+
searchPlaceholder: 'Search…',
|
|
26
|
+
enableCRUDActions: true,
|
|
27
|
+
hasActions: false,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('isColumnVisibleInTable', () => {
|
|
31
|
+
it('keeps columns with no visibility flag (legacy zero-value)', () => {
|
|
32
|
+
expect(isColumnVisibleInTable(baseCol())).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('keeps columns with visibility="all"', () => {
|
|
36
|
+
expect(isColumnVisibleInTable(baseCol({ visibility: 'all' }))).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('keeps columns with visibility="table"', () => {
|
|
40
|
+
expect(isColumnVisibleInTable(baseCol({ visibility: 'table' }))).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('hides columns with visibility="modal"', () => {
|
|
44
|
+
expect(isColumnVisibleInTable(baseCol({ visibility: 'modal' }))).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('hides columns with visibility="list"', () => {
|
|
48
|
+
expect(isColumnVisibleInTable(baseCol({ visibility: 'list' }))).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('hides columns with the legacy hidden boolean even if visibility="all"', () => {
|
|
52
|
+
expect(isColumnVisibleInTable(baseCol({ hidden: true, visibility: 'all' }))).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('hides columns with an unknown visibility value (fail-closed)', () => {
|
|
56
|
+
expect(isColumnVisibleInTable(baseCol({ visibility: 'detail' as any }))).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('getSearchableColumnKeys', () => {
|
|
61
|
+
it('returns null when no column declares searchable (legacy metadata)', () => {
|
|
62
|
+
const meta = baseMeta([
|
|
63
|
+
baseCol({ key: 'name' }),
|
|
64
|
+
baseCol({ key: 'email' }),
|
|
65
|
+
])
|
|
66
|
+
expect(getSearchableColumnKeys(meta)).toBe(null)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('returns only the searchable keys when at least one column declares it', () => {
|
|
70
|
+
const meta = baseMeta([
|
|
71
|
+
baseCol({ key: 'name', searchable: true }),
|
|
72
|
+
baseCol({ key: 'email', searchable: true }),
|
|
73
|
+
baseCol({ key: 'phone', searchable: false }),
|
|
74
|
+
baseCol({ key: 'created_at' }), // undefined → not searchable
|
|
75
|
+
])
|
|
76
|
+
expect(getSearchableColumnKeys(meta)).toEqual(['name', 'email'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('returns an empty array when every column is explicitly opted out', () => {
|
|
80
|
+
const meta = baseMeta([
|
|
81
|
+
baseCol({ key: 'name', searchable: false }),
|
|
82
|
+
baseCol({ key: 'email', searchable: false }),
|
|
83
|
+
])
|
|
84
|
+
expect(getSearchableColumnKeys(meta)).toEqual([])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('treats searchable=true on a single column as the explicit allowlist', () => {
|
|
88
|
+
const meta = baseMeta([
|
|
89
|
+
baseCol({ key: 'name', searchable: true }),
|
|
90
|
+
baseCol({ key: 'internal_notes' }),
|
|
91
|
+
])
|
|
92
|
+
expect(getSearchableColumnKeys(meta)).toEqual(['name'])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('handles missing columns array defensively', () => {
|
|
96
|
+
const meta = { columns: undefined as unknown as ColumnDefinition[] }
|
|
97
|
+
expect(getSearchableColumnKeys(meta as any)).toBe(null)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('column-visibility integration with mock metadata', () => {
|
|
102
|
+
it('filtering and search keys can be derived from a single mock', () => {
|
|
103
|
+
const meta = baseMeta([
|
|
104
|
+
baseCol({ key: 'id', visibility: 'list', searchable: false }),
|
|
105
|
+
baseCol({ key: 'name', visibility: 'all', searchable: true }),
|
|
106
|
+
baseCol({ key: 'email', visibility: 'table', searchable: true }),
|
|
107
|
+
baseCol({ key: 'password_hash', visibility: 'modal', searchable: false }),
|
|
108
|
+
baseCol({ key: 'profile', visibility: 'modal', searchable: false }),
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
const tableColumns = meta.columns.filter(isColumnVisibleInTable).map(c => c.key)
|
|
112
|
+
expect(tableColumns).toEqual(['name', 'email'])
|
|
113
|
+
|
|
114
|
+
expect(getSearchableColumnKeys(meta)).toEqual(['name', 'email'])
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Pure helpers that map kernel `manifest.ColumnDef` metadata flags
|
|
2
|
+
// (Visibility, Searchable) into client-side decisions:
|
|
3
|
+
// - which columns the dynamic table should render
|
|
4
|
+
// - which column keys are in scope for the global search
|
|
5
|
+
//
|
|
6
|
+
// Kept side-effect free and free of React/UI imports so the same logic can
|
|
7
|
+
// be tested with plain unit tests against mock metadata.
|
|
8
|
+
|
|
9
|
+
import type { ColumnDefinition, TableMetadata } from './types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether a column should render in a list/index table view.
|
|
13
|
+
*
|
|
14
|
+
* A column is hidden when its `visibility` is scoped away from the table
|
|
15
|
+
* (`'modal'`: only the create/edit dialog; `'list'`: only API payloads) or
|
|
16
|
+
* when the legacy `hidden` boolean is set. Empty / `'all'` / `'table'` keep
|
|
17
|
+
* the column visible — preserving zero-value behaviour for metadata emitted
|
|
18
|
+
* by older kernels that don't set `visibility` at all.
|
|
19
|
+
*/
|
|
20
|
+
export function isColumnVisibleInTable(col: ColumnDefinition): boolean {
|
|
21
|
+
if (col.hidden) return false
|
|
22
|
+
const v = col.visibility
|
|
23
|
+
if (!v) return true
|
|
24
|
+
return v === 'all' || v === 'table'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the keys of columns that opt into the model's full-text search,
|
|
29
|
+
* or `null` when no column declares `searchable` at all.
|
|
30
|
+
*
|
|
31
|
+
* `null` is the legacy signal: the host should NOT narrow the search request
|
|
32
|
+
* (every column participates, matching pre-Searchable kernels). An empty
|
|
33
|
+
* array is meaningful — it means every column has been explicitly opted out
|
|
34
|
+
* and the host should disable the global search input.
|
|
35
|
+
*/
|
|
36
|
+
export function getSearchableColumnKeys(
|
|
37
|
+
metadata: Pick<TableMetadata, 'columns'>,
|
|
38
|
+
): string[] | null {
|
|
39
|
+
const cols = metadata.columns ?? []
|
|
40
|
+
const declared = cols.some(c => typeof c.searchable === 'boolean')
|
|
41
|
+
if (!declared) return null
|
|
42
|
+
return cols.filter(c => c.searchable === true).map(c => c.key)
|
|
43
|
+
}
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -35,6 +35,7 @@ import { generateBadgeStyles, getInitials } from '@asteby/metacore-ui/lib'
|
|
|
35
35
|
import { OptionsContext } from './options-context'
|
|
36
36
|
import { DynamicIcon } from './dynamic-icon'
|
|
37
37
|
import type { TableMetadata, ColumnDefinition } from './types'
|
|
38
|
+
import { isColumnVisibleInTable } from './column-visibility'
|
|
38
39
|
import type {
|
|
39
40
|
ColumnFilterConfig,
|
|
40
41
|
GetDynamicColumns,
|
|
@@ -221,7 +222,9 @@ export function makeDefaultGetDynamicColumns(
|
|
|
221
222
|
]
|
|
222
223
|
|
|
223
224
|
metadata.columns.forEach((col) => {
|
|
224
|
-
|
|
225
|
+
// Honors both the legacy `hidden` boolean and the kernel's
|
|
226
|
+
// `visibility` scope (skips `'modal'` and `'list'`).
|
|
227
|
+
if (!isColumnVisibleInTable(col)) return
|
|
225
228
|
|
|
226
229
|
const translatedLabel = col.label
|
|
227
230
|
const filterConfig = filterConfigs?.get(col.key)
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -69,6 +69,7 @@ import { defaultGetDynamicColumns } from './dynamic-columns'
|
|
|
69
69
|
import { OptionsContext } from './options-context'
|
|
70
70
|
import { ActionModalDispatcher } from './action-modal-dispatcher'
|
|
71
71
|
import type { TableMetadata, ApiResponse, ActionMetadata } from './types'
|
|
72
|
+
import { getSearchableColumnKeys } from './column-visibility'
|
|
72
73
|
import { DynamicRecordDialog } from './dialogs/dynamic-record'
|
|
73
74
|
import { ExportDialog } from './dialogs/export'
|
|
74
75
|
import { ImportDialog } from './dialogs/import'
|
|
@@ -324,13 +325,30 @@ export function DynamicTable({
|
|
|
324
325
|
initMetadataAndOptions()
|
|
325
326
|
}, [model]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
326
327
|
|
|
328
|
+
// Derived from `metadata.columns[].searchable`. `null` means the kernel
|
|
329
|
+
// didn't emit the flag for any column → preserve legacy "search every
|
|
330
|
+
// column" behaviour by not narrowing the request. An empty array means
|
|
331
|
+
// every column was explicitly opted out → skip sending `search` at all.
|
|
332
|
+
const searchableKeys = useMemo(
|
|
333
|
+
() => (metadata ? getSearchableColumnKeys(metadata) : null),
|
|
334
|
+
[metadata],
|
|
335
|
+
)
|
|
336
|
+
|
|
327
337
|
const buildFilterParams = useCallback(() => {
|
|
328
338
|
const params: Record<string, any> = {}
|
|
329
339
|
if (sorting.length > 0) {
|
|
330
340
|
params.sortBy = sorting[0].id
|
|
331
341
|
params.order = sorting[0].desc ? 'desc' : 'asc'
|
|
332
342
|
}
|
|
333
|
-
if (globalFilter)
|
|
343
|
+
if (globalFilter) {
|
|
344
|
+
if (searchableKeys === null) {
|
|
345
|
+
params.search = globalFilter
|
|
346
|
+
} else if (searchableKeys.length > 0) {
|
|
347
|
+
params.search = globalFilter
|
|
348
|
+
params.search_columns = searchableKeys.join(',')
|
|
349
|
+
}
|
|
350
|
+
// searchableKeys === [] → drop the search request entirely
|
|
351
|
+
}
|
|
334
352
|
columnFilters.forEach((filter: { id: string; value: unknown }) => { params[`f_${filter.id}`] = filter.value })
|
|
335
353
|
if (defaultFilters) Object.entries(defaultFilters).forEach(([key, value]) => { params[`f_${key}`] = value })
|
|
336
354
|
Object.entries(dynamicFilters).forEach(([key, values]) => {
|
|
@@ -352,7 +370,7 @@ export function DynamicTable({
|
|
|
352
370
|
params['f_created_at'] = `${startDate}_${endDate}`
|
|
353
371
|
}
|
|
354
372
|
return params
|
|
355
|
-
}, [sorting, globalFilter, columnFilters, defaultFilters, dynamicFilters, dateRange])
|
|
373
|
+
}, [sorting, globalFilter, columnFilters, defaultFilters, dynamicFilters, dateRange, searchableKeys])
|
|
356
374
|
|
|
357
375
|
const hasActiveFilters = useMemo(() => {
|
|
358
376
|
if (globalFilter) return true
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -26,6 +26,18 @@ export interface FilterDefinition {
|
|
|
26
26
|
searchEndpoint?: string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Where a column is rendered. Mirrors `manifest.ColumnDef.Visibility` in the
|
|
31
|
+
* kernel:
|
|
32
|
+
* - `''` / `'all'` — visible everywhere (default).
|
|
33
|
+
* - `'table'` — only the list/index page.
|
|
34
|
+
* - `'modal'` — only the create/edit modal.
|
|
35
|
+
* - `'list'` — only API list payloads (omitted from UI).
|
|
36
|
+
* Hosts may extend the union with their own scopes; the SDK only acts on the
|
|
37
|
+
* canonical values above.
|
|
38
|
+
*/
|
|
39
|
+
export type ColumnVisibility = 'all' | 'table' | 'modal' | 'list' | (string & {})
|
|
40
|
+
|
|
29
41
|
export interface ColumnDefinition {
|
|
30
42
|
key: string
|
|
31
43
|
label: string
|
|
@@ -33,6 +45,19 @@ export interface ColumnDefinition {
|
|
|
33
45
|
sortable: boolean
|
|
34
46
|
filterable: boolean
|
|
35
47
|
hidden?: boolean
|
|
48
|
+
/**
|
|
49
|
+
* Scopes where this column is rendered. When `'modal'` (or `'list'`) the
|
|
50
|
+
* column is hidden from the table even if `hidden` is unset. Empty/`'all'`/
|
|
51
|
+
* `'table'` keep the column visible. See `column-visibility.ts`.
|
|
52
|
+
*/
|
|
53
|
+
visibility?: ColumnVisibility
|
|
54
|
+
/**
|
|
55
|
+
* Opts the column into the model's full-text/contains search. Independent
|
|
56
|
+
* of `filterable` (which drives column-level filter chips). When at least
|
|
57
|
+
* one column declares `searchable`, the SDK narrows the global search to
|
|
58
|
+
* those columns; otherwise legacy "search every column" behaviour applies.
|
|
59
|
+
*/
|
|
60
|
+
searchable?: boolean
|
|
36
61
|
styleConfig?: Record<string, any>
|
|
37
62
|
tooltip?: string
|
|
38
63
|
description?: string
|
package/tsconfig.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/dynamic-form.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { buildZodSchema, resolveWidget } from '../dynamic-form-schema';
|
|
3
|
-
describe('buildZodSchema', () => {
|
|
4
|
-
it('aplica regex de Validation a strings', () => {
|
|
5
|
-
const fields = [
|
|
6
|
-
{ key: 'sku', label: 'SKU', type: 'string', required: true, validation: { regex: '^[A-Z]{3}-\\d{3}$' } },
|
|
7
|
-
];
|
|
8
|
-
const schema = buildZodSchema(fields);
|
|
9
|
-
expect(schema.safeParse({ sku: 'ABC-123' }).success).toBe(true);
|
|
10
|
-
expect(schema.safeParse({ sku: 'abc-123' }).success).toBe(false);
|
|
11
|
-
expect(schema.safeParse({ sku: 'ABCD-123' }).success).toBe(false);
|
|
12
|
-
});
|
|
13
|
-
it('aplica min/max como longitud sobre strings', () => {
|
|
14
|
-
const fields = [
|
|
15
|
-
{ key: 'name', label: 'Nombre', type: 'string', required: true, validation: { min: 3, max: 8 } },
|
|
16
|
-
];
|
|
17
|
-
const schema = buildZodSchema(fields);
|
|
18
|
-
expect(schema.safeParse({ name: 'ab' }).success).toBe(false);
|
|
19
|
-
expect(schema.safeParse({ name: 'abc' }).success).toBe(true);
|
|
20
|
-
expect(schema.safeParse({ name: 'abcdefgh' }).success).toBe(true);
|
|
21
|
-
expect(schema.safeParse({ name: 'abcdefghi' }).success).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
it('aplica min/max como bounds sobre números', () => {
|
|
24
|
-
const fields = [
|
|
25
|
-
{ key: 'age', label: 'Edad', type: 'number', required: true, validation: { min: 18, max: 99 } },
|
|
26
|
-
];
|
|
27
|
-
const schema = buildZodSchema(fields);
|
|
28
|
-
expect(schema.safeParse({ age: 17 }).success).toBe(false);
|
|
29
|
-
expect(schema.safeParse({ age: 18 }).success).toBe(true);
|
|
30
|
-
expect(schema.safeParse({ age: 99 }).success).toBe(true);
|
|
31
|
-
expect(schema.safeParse({ age: 100 }).success).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
it('marca campos requeridos vacíos como inválidos', () => {
|
|
34
|
-
const fields = [
|
|
35
|
-
{ key: 'title', label: 'Título', type: 'string', required: true },
|
|
36
|
-
];
|
|
37
|
-
const schema = buildZodSchema(fields);
|
|
38
|
-
expect(schema.safeParse({ title: '' }).success).toBe(false);
|
|
39
|
-
expect(schema.safeParse({ title: 'ok' }).success).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
it('campos opcionales aceptan vacío o ausente', () => {
|
|
42
|
-
const fields = [
|
|
43
|
-
{ key: 'note', label: 'Nota', type: 'string' },
|
|
44
|
-
{ key: 'qty', label: 'Cantidad', type: 'number' },
|
|
45
|
-
];
|
|
46
|
-
const schema = buildZodSchema(fields);
|
|
47
|
-
expect(schema.safeParse({ note: '', qty: '' }).success).toBe(true);
|
|
48
|
-
expect(schema.safeParse({}).success).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
it('valida email y url por type', () => {
|
|
51
|
-
const fields = [
|
|
52
|
-
{ key: 'mail', label: 'Email', type: 'email', required: true },
|
|
53
|
-
{ key: 'site', label: 'URL', type: 'url', required: true },
|
|
54
|
-
];
|
|
55
|
-
const schema = buildZodSchema(fields);
|
|
56
|
-
expect(schema.safeParse({ mail: 'a@b.co', site: 'https://x.test' }).success).toBe(true);
|
|
57
|
-
expect(schema.safeParse({ mail: 'no-email', site: 'https://x.test' }).success).toBe(false);
|
|
58
|
-
expect(schema.safeParse({ mail: 'a@b.co', site: 'no-url' }).success).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
it('regex inválida no rompe el build (silently skipped)', () => {
|
|
61
|
-
const fields = [
|
|
62
|
-
{ key: 'x', label: 'X', type: 'string', required: true, validation: { regex: '[invalid(' } },
|
|
63
|
-
];
|
|
64
|
-
expect(() => buildZodSchema(fields)).not.toThrow();
|
|
65
|
-
const schema = buildZodSchema(fields);
|
|
66
|
-
expect(schema.safeParse({ x: 'anything' }).success).toBe(true);
|
|
67
|
-
});
|
|
68
|
-
it('booleans requeridos exigen un valor explícito', () => {
|
|
69
|
-
const fields = [
|
|
70
|
-
{ key: 'agree', label: 'Acepto', type: 'boolean', required: true },
|
|
71
|
-
];
|
|
72
|
-
const schema = buildZodSchema(fields);
|
|
73
|
-
expect(schema.safeParse({ agree: true }).success).toBe(true);
|
|
74
|
-
expect(schema.safeParse({ agree: false }).success).toBe(true);
|
|
75
|
-
expect(schema.safeParse({}).success).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
describe('resolveWidget', () => {
|
|
79
|
-
it('respeta widget explícito', () => {
|
|
80
|
-
expect(resolveWidget({ key: 'k', label: 'L', type: 'string', widget: 'textarea' })).toBe('textarea');
|
|
81
|
-
expect(resolveWidget({ key: 'k', label: 'L', type: 'string', widget: 'richtext' })).toBe('richtext');
|
|
82
|
-
expect(resolveWidget({ key: 'k', label: 'L', type: 'string', widget: 'color' })).toBe('color');
|
|
83
|
-
});
|
|
84
|
-
it('cae al inferido por type cuando widget no está', () => {
|
|
85
|
-
expect(resolveWidget({ key: 'k', label: 'L', type: 'textarea' })).toBe('textarea');
|
|
86
|
-
expect(resolveWidget({ key: 'k', label: 'L', type: 'select' })).toBe('select');
|
|
87
|
-
expect(resolveWidget({ key: 'k', label: 'L', type: 'boolean' })).toBe('switch');
|
|
88
|
-
expect(resolveWidget({ key: 'k', label: 'L', type: 'number' })).toBe('number');
|
|
89
|
-
expect(resolveWidget({ key: 'k', label: 'L', type: 'date' })).toBe('date');
|
|
90
|
-
expect(resolveWidget({ key: 'k', label: 'L', type: 'string' })).toBe('text');
|
|
91
|
-
expect(resolveWidget({ key: 'k', label: 'L', type: 'email' })).toBe('text');
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-relation.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/dynamic-relation.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, pickOptionLabel, relationRowKey, } from '../dynamic-relation-helpers';
|
|
3
|
-
describe('buildRelationFilterParams', () => {
|
|
4
|
-
it('produce el filtro f_<fk>=eq:<id> con string parentId', () => {
|
|
5
|
-
expect(buildRelationFilterParams('invoice_id', 'inv_42')).toEqual({
|
|
6
|
-
f_invoice_id: 'eq:inv_42',
|
|
7
|
-
});
|
|
8
|
-
});
|
|
9
|
-
it('coerce parentId numérico a string en el query', () => {
|
|
10
|
-
expect(buildRelationFilterParams('invoice_id', 42)).toEqual({
|
|
11
|
-
f_invoice_id: 'eq:42',
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
it('rechaza foreignKey vacío', () => {
|
|
15
|
-
expect(() => buildRelationFilterParams('', 'x')).toThrow(/foreignKey/);
|
|
16
|
-
});
|
|
17
|
-
it('rechaza parentId vacío / null / undefined', () => {
|
|
18
|
-
expect(() => buildRelationFilterParams('invoice_id', '')).toThrow(/parentId/);
|
|
19
|
-
// @ts-expect-error testing runtime guard
|
|
20
|
-
expect(() => buildRelationFilterParams('invoice_id', null)).toThrow(/parentId/);
|
|
21
|
-
// @ts-expect-error testing runtime guard
|
|
22
|
-
expect(() => buildRelationFilterParams('invoice_id', undefined)).toThrow(/parentId/);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
describe('buildCreatePayload', () => {
|
|
26
|
-
it('inyecta el foreign key sobre los valores del form', () => {
|
|
27
|
-
const payload = buildCreatePayload('invoice_id', 'inv_42', { qty: 3, sku: 'A' });
|
|
28
|
-
expect(payload).toEqual({ qty: 3, sku: 'A', invoice_id: 'inv_42' });
|
|
29
|
-
});
|
|
30
|
-
it('el foreign key sobreescribe lo que venga del form', () => {
|
|
31
|
-
const payload = buildCreatePayload('invoice_id', 'inv_42', { invoice_id: 'inv_OTHER', qty: 1 });
|
|
32
|
-
expect(payload.invoice_id).toBe('inv_42');
|
|
33
|
-
});
|
|
34
|
-
it('preserva el parentId numérico tal cual', () => {
|
|
35
|
-
const payload = buildCreatePayload('invoice_id', 7, { qty: 1 });
|
|
36
|
-
expect(payload.invoice_id).toBe(7);
|
|
37
|
-
});
|
|
38
|
-
it('rechaza foreignKey vacío', () => {
|
|
39
|
-
expect(() => buildCreatePayload('', 'x', {})).toThrow(/foreignKey/);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe('deriveRelationFormFields', () => {
|
|
43
|
-
const baseMeta = {
|
|
44
|
-
columns: [
|
|
45
|
-
{ key: 'id', label: 'ID', type: 'text', sortable: true, filterable: false, hidden: true },
|
|
46
|
-
{ key: 'invoice_id', label: 'Factura', type: 'text', sortable: false, filterable: false },
|
|
47
|
-
{ key: 'sku', label: 'SKU', type: 'text', sortable: true, filterable: true },
|
|
48
|
-
{ key: 'qty', label: 'Cantidad', type: 'number', sortable: true, filterable: false },
|
|
49
|
-
{ key: 'taxable', label: 'Aplica IVA', type: 'boolean', sortable: false, filterable: false },
|
|
50
|
-
{ key: 'category', label: 'Categoría', type: 'select', sortable: false, filterable: true, options: [
|
|
51
|
-
{ value: 'a', label: 'A' }, { value: 'b', label: 'B' },
|
|
52
|
-
] },
|
|
53
|
-
],
|
|
54
|
-
};
|
|
55
|
-
it('omite la foreign key porque está fija al parentId', () => {
|
|
56
|
-
const fields = deriveRelationFormFields(baseMeta, 'invoice_id');
|
|
57
|
-
expect(fields.find(f => f.key === 'invoice_id')).toBeUndefined();
|
|
58
|
-
});
|
|
59
|
-
it('omite columnas marcadas hidden', () => {
|
|
60
|
-
const fields = deriveRelationFormFields(baseMeta, 'invoice_id');
|
|
61
|
-
expect(fields.find(f => f.key === 'id')).toBeUndefined();
|
|
62
|
-
});
|
|
63
|
-
it('mapea types de ColumnDefinition al ActionFieldDef.type', () => {
|
|
64
|
-
const fields = deriveRelationFormFields(baseMeta, 'invoice_id');
|
|
65
|
-
const byKey = Object.fromEntries(fields.map(f => [f.key, f]));
|
|
66
|
-
expect(byKey['sku']?.type).toBe('string');
|
|
67
|
-
expect(byKey['qty']?.type).toBe('number');
|
|
68
|
-
expect(byKey['taxable']?.type).toBe('boolean');
|
|
69
|
-
expect(byKey['category']?.type).toBe('select');
|
|
70
|
-
});
|
|
71
|
-
it('propaga options con value coerced a string', () => {
|
|
72
|
-
const fields = deriveRelationFormFields(baseMeta, 'invoice_id');
|
|
73
|
-
const cat = fields.find(f => f.key === 'category');
|
|
74
|
-
expect(cat?.options).toEqual([
|
|
75
|
-
{ value: 'a', label: 'A' },
|
|
76
|
-
{ value: 'b', label: 'B' },
|
|
77
|
-
]);
|
|
78
|
-
});
|
|
79
|
-
it('devuelve [] cuando no hay metadata', () => {
|
|
80
|
-
expect(deriveRelationFormFields(null, 'invoice_id')).toEqual([]);
|
|
81
|
-
expect(deriveRelationFormFields(undefined, 'invoice_id')).toEqual([]);
|
|
82
|
-
expect(deriveRelationFormFields({ columns: [] }, 'invoice_id')).toEqual([]);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
describe('relationRowKey', () => {
|
|
86
|
-
it('usa row.id como key cuando existe', () => {
|
|
87
|
-
expect(relationRowKey({ id: 'abc' }, 0, 'invoice_id')).toBe('abc');
|
|
88
|
-
expect(relationRowKey({ id: 7 }, 5, 'invoice_id')).toBe('7');
|
|
89
|
-
});
|
|
90
|
-
it('cae a synthetic key cuando id falta o es vacío', () => {
|
|
91
|
-
expect(relationRowKey({}, 2, 'invoice_id')).toBe('__rel-invoice_id-2');
|
|
92
|
-
expect(relationRowKey({ id: '' }, 3, 'invoice_id')).toBe('__rel-invoice_id-3');
|
|
93
|
-
expect(relationRowKey({ id: null }, 1, 'invoice_id')).toBe('__rel-invoice_id-1');
|
|
94
|
-
expect(relationRowKey(undefined, 0, 'invoice_id')).toBe('__rel-invoice_id-0');
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// many_to_many helpers
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
describe('buildPivotAttachPayload', () => {
|
|
101
|
-
it('produce el body con los dos FKs fijos', () => {
|
|
102
|
-
const body = buildPivotAttachPayload('org_id', 'org_1', 'user_id', 'user_42');
|
|
103
|
-
expect(body).toEqual({ org_id: 'org_1', user_id: 'user_42' });
|
|
104
|
-
});
|
|
105
|
-
it('mezcla campos extra del pivot sin pisar los FKs', () => {
|
|
106
|
-
const body = buildPivotAttachPayload('org_id', 1, 'user_id', 2, {
|
|
107
|
-
role: 'owner',
|
|
108
|
-
org_id: 'evil',
|
|
109
|
-
user_id: 'evil',
|
|
110
|
-
});
|
|
111
|
-
expect(body.org_id).toBe(1);
|
|
112
|
-
expect(body.user_id).toBe(2);
|
|
113
|
-
expect(body.role).toBe('owner');
|
|
114
|
-
});
|
|
115
|
-
it('rechaza foreignKey / referencesKey vacíos', () => {
|
|
116
|
-
expect(() => buildPivotAttachPayload('', 'p', 'r', 't')).toThrow(/foreignKey/);
|
|
117
|
-
expect(() => buildPivotAttachPayload('f', 'p', '', 't')).toThrow(/referencesKey/);
|
|
118
|
-
});
|
|
119
|
-
it('rechaza parentId / targetId vacíos', () => {
|
|
120
|
-
expect(() => buildPivotAttachPayload('f', '', 'r', 't')).toThrow(/parentId/);
|
|
121
|
-
expect(() => buildPivotAttachPayload('f', 'p', 'r', '')).toThrow(/targetId/);
|
|
122
|
-
// @ts-expect-error testing runtime guard
|
|
123
|
-
expect(() => buildPivotAttachPayload('f', 'p', 'r', null)).toThrow(/targetId/);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
describe('extractSelectedTargetIds', () => {
|
|
127
|
-
it('mapea pivot rows al set de target ids como strings', () => {
|
|
128
|
-
const ids = extractSelectedTargetIds([
|
|
129
|
-
{ id: 1, org_id: 'org_1', user_id: 'u_1' },
|
|
130
|
-
{ id: 2, org_id: 'org_1', user_id: 7 },
|
|
131
|
-
], 'user_id');
|
|
132
|
-
expect(ids).toEqual(['u_1', '7']);
|
|
133
|
-
});
|
|
134
|
-
it('omite filas sin valor en el referencesKey', () => {
|
|
135
|
-
const ids = extractSelectedTargetIds([
|
|
136
|
-
{ id: 1, user_id: 'u_1' },
|
|
137
|
-
{ id: 2, user_id: null },
|
|
138
|
-
{ id: 3, user_id: '' },
|
|
139
|
-
{ id: 4 },
|
|
140
|
-
], 'user_id');
|
|
141
|
-
expect(ids).toEqual(['u_1']);
|
|
142
|
-
});
|
|
143
|
-
it('devuelve [] cuando no hay rows o referencesKey', () => {
|
|
144
|
-
expect(extractSelectedTargetIds(null, 'user_id')).toEqual([]);
|
|
145
|
-
expect(extractSelectedTargetIds(undefined, 'user_id')).toEqual([]);
|
|
146
|
-
expect(extractSelectedTargetIds([{ user_id: 'x' }], '')).toEqual([]);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
describe('buildPivotRowIndex', () => {
|
|
150
|
-
it('mapea targetId -> pivotRowId', () => {
|
|
151
|
-
const idx = buildPivotRowIndex([
|
|
152
|
-
{ id: 'p1', user_id: 'u_1' },
|
|
153
|
-
{ id: 'p2', user_id: 7 },
|
|
154
|
-
], 'user_id');
|
|
155
|
-
expect(idx.get('u_1')).toBe('p1');
|
|
156
|
-
expect(idx.get('7')).toBe('p2');
|
|
157
|
-
});
|
|
158
|
-
it('omite filas sin id pivot o sin target', () => {
|
|
159
|
-
const idx = buildPivotRowIndex([
|
|
160
|
-
{ id: 'p1', user_id: 'u_1' },
|
|
161
|
-
{ user_id: 'u_2' },
|
|
162
|
-
{ id: 'p3' },
|
|
163
|
-
{ id: 'p4', user_id: null },
|
|
164
|
-
], 'user_id');
|
|
165
|
-
expect(Array.from(idx.keys())).toEqual(['u_1']);
|
|
166
|
-
});
|
|
167
|
-
it('última fila gana cuando hay duplicados', () => {
|
|
168
|
-
const idx = buildPivotRowIndex([
|
|
169
|
-
{ id: 'p1', user_id: 'u_1' },
|
|
170
|
-
{ id: 'p2', user_id: 'u_1' },
|
|
171
|
-
], 'user_id');
|
|
172
|
-
expect(idx.get('u_1')).toBe('p2');
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
describe('diffSelection', () => {
|
|
176
|
-
it('detecta toAdd y toRemove respecto al estado previo', () => {
|
|
177
|
-
const { toAdd, toRemove } = diffSelection(['a', 'b', 'c'], ['b', 'c', 'd']);
|
|
178
|
-
expect(toAdd).toEqual(['d']);
|
|
179
|
-
expect(toRemove).toEqual(['a']);
|
|
180
|
-
});
|
|
181
|
-
it('preserva el orden de aparición en next/prev', () => {
|
|
182
|
-
const { toAdd, toRemove } = diffSelection(['a', 'b'], ['c', 'a', 'd']);
|
|
183
|
-
expect(toAdd).toEqual(['c', 'd']);
|
|
184
|
-
expect(toRemove).toEqual(['b']);
|
|
185
|
-
});
|
|
186
|
-
it('devuelve arrays vacíos cuando no cambia nada', () => {
|
|
187
|
-
const { toAdd, toRemove } = diffSelection(['a', 'b'], ['b', 'a']);
|
|
188
|
-
expect(toAdd).toEqual([]);
|
|
189
|
-
expect(toRemove).toEqual([]);
|
|
190
|
-
});
|
|
191
|
-
it('caso vacío -> next agrega todo', () => {
|
|
192
|
-
const { toAdd, toRemove } = diffSelection([], ['a', 'b']);
|
|
193
|
-
expect(toAdd).toEqual(['a', 'b']);
|
|
194
|
-
expect(toRemove).toEqual([]);
|
|
195
|
-
});
|
|
196
|
-
it('caso prev -> [] remueve todo', () => {
|
|
197
|
-
const { toAdd, toRemove } = diffSelection(['a', 'b'], []);
|
|
198
|
-
expect(toAdd).toEqual([]);
|
|
199
|
-
expect(toRemove).toEqual(['a', 'b']);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
describe('pickOptionLabel', () => {
|
|
203
|
-
const cols = [
|
|
204
|
-
{ key: 'id', label: 'ID', type: 'text', sortable: false, filterable: false, hidden: true },
|
|
205
|
-
{ key: 'name', label: 'Nombre', type: 'text', sortable: true, filterable: true },
|
|
206
|
-
{ key: 'email', label: 'Email', type: 'text', sortable: false, filterable: true },
|
|
207
|
-
];
|
|
208
|
-
it('respeta displayKey cuando existe en la fila', () => {
|
|
209
|
-
expect(pickOptionLabel({ id: 1, name: 'Alice', email: 'a@x' }, 'email', cols)).toBe('a@x');
|
|
210
|
-
});
|
|
211
|
-
it('cae al primer column no-id no-hidden cuando displayKey falta', () => {
|
|
212
|
-
expect(pickOptionLabel({ id: 1, name: 'Alice', email: 'a@x' }, undefined, cols)).toBe('Alice');
|
|
213
|
-
});
|
|
214
|
-
it('salta valores nulos / vacíos al inferir', () => {
|
|
215
|
-
expect(pickOptionLabel({ id: 1, name: '', email: 'a@x' }, undefined, cols)).toBe('a@x');
|
|
216
|
-
});
|
|
217
|
-
it('cae a row.id cuando no hay match en columns', () => {
|
|
218
|
-
expect(pickOptionLabel({ id: 7 }, undefined, cols)).toBe('7');
|
|
219
|
-
expect(pickOptionLabel({ id: 7 }, undefined, undefined)).toBe('7');
|
|
220
|
-
});
|
|
221
|
-
it('devuelve "—" cuando no hay nada usable', () => {
|
|
222
|
-
expect(pickOptionLabel(null, undefined, cols)).toBe('—');
|
|
223
|
-
expect(pickOptionLabel({}, undefined, cols)).toBe('—');
|
|
224
|
-
});
|
|
225
|
-
it('ignora valores object al inferir', () => {
|
|
226
|
-
expect(pickOptionLabel({ id: 1, name: { nested: true }, email: 'a@x' }, undefined, cols)).toBe('a@x');
|
|
227
|
-
});
|
|
228
|
-
});
|