@asteby/metacore-runtime-react 9.0.0 → 9.2.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 +46 -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-form-schema.d.ts +5 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +34 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +18 -2
- package/dist/dynamic-relation.d.ts.map +1 -1
- package/dist/dynamic-relation.js +59 -22
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +17 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-options-resolver.d.ts +87 -0
- package/dist/use-options-resolver.d.ts.map +1 -0
- package/dist/use-options-resolver.js +147 -0
- package/dist/use-org-config-bridge.d.ts +28 -0
- package/dist/use-org-config-bridge.d.ts.map +1 -0
- package/dist/use-org-config-bridge.js +50 -0
- package/package.json +3 -2
- package/src/__tests__/column-visibility.test.ts +116 -0
- package/src/__tests__/use-options-resolver.test.ts +127 -0
- package/src/column-visibility.ts +43 -0
- package/src/dynamic-columns.tsx +4 -1
- package/src/dynamic-form-schema.ts +36 -0
- package/src/dynamic-form.tsx +40 -2
- package/src/dynamic-relation.tsx +55 -20
- package/src/dynamic-table.tsx +20 -2
- package/src/index.ts +19 -0
- package/src/types.ts +49 -0
- package/src/use-options-resolver.ts +232 -0
- package/src/use-org-config-bridge.ts +60 -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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Bridge to `useOrgConfig` from `@asteby/metacore-app-providers` without
|
|
2
|
+
// adding it as a hard dependency of `runtime-react`. The provider package
|
|
3
|
+
// is a peer; in apps that mount it the hook returns the live config, in
|
|
4
|
+
// apps that don't the SDK falls through to a no-op shim that resolves
|
|
5
|
+
// every reference to null. Forms then leave $org.<key> tokens in place
|
|
6
|
+
// rather than crashing — the operator notices the missing config when
|
|
7
|
+
// the validator fails to fire, not at app boot.
|
|
8
|
+
//
|
|
9
|
+
// Why a bridge: runtime-react cannot import `@asteby/metacore-app-providers`
|
|
10
|
+
// directly without inverting the dependency graph (app-providers depends
|
|
11
|
+
// on runtime-react today via peerDependenciesMeta). The shim shape
|
|
12
|
+
// matches `OrgConfigContextValue` so DynamicForm code reads through one
|
|
13
|
+
// stable interface regardless of provider mount.
|
|
14
|
+
|
|
15
|
+
export interface OrgConfigBridge {
|
|
16
|
+
/** Resolves a `$org.<key>` reference (or plain key) to a literal id. */
|
|
17
|
+
resolveValidator: (refOrKey: string) => string | null
|
|
18
|
+
/** When true the app actually has a provider mounted. */
|
|
19
|
+
available: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const NULL_BRIDGE: OrgConfigBridge = {
|
|
23
|
+
resolveValidator: () => null,
|
|
24
|
+
available: false,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let activeBridge: OrgConfigBridge = NULL_BRIDGE
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Apps that consume `runtime-react` AND `@asteby/metacore-app-providers`
|
|
31
|
+
* call this once near the root (typically inside the OrgConfigProvider
|
|
32
|
+
* children) so the SDK reads the same resolver. Hosts without an org
|
|
33
|
+
* provider can ignore this entirely; the SDK's null bridge keeps every
|
|
34
|
+
* call returning `null` so $org.<key> tokens stay verbatim in the form
|
|
35
|
+
* — same fallback the kernel uses for unresolved references.
|
|
36
|
+
*/
|
|
37
|
+
export function setOrgConfigBridge(bridge: OrgConfigBridge | null) {
|
|
38
|
+
activeBridge = bridge ?? NULL_BRIDGE
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns the active bridge. Pure read — no React hook so it can be
|
|
43
|
+
* called from non-component code (zod schema builders, helpers).
|
|
44
|
+
*/
|
|
45
|
+
export function getOrgConfigBridge(): OrgConfigBridge {
|
|
46
|
+
return activeBridge
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolves a Validation token into the validator identifier the SDK
|
|
51
|
+
* should apply. Returns the resolved literal when the org config knows
|
|
52
|
+
* the key, or the original token when it doesn't (so apps can decide).
|
|
53
|
+
* Plain literals (no `$org.` prefix) pass through.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveValidatorToken(token: string | undefined | null): string | null {
|
|
56
|
+
if (!token) return null
|
|
57
|
+
if (!token.startsWith('$org.')) return token
|
|
58
|
+
const resolved = activeBridge.resolveValidator(token)
|
|
59
|
+
return resolved ?? token
|
|
60
|
+
}
|
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
|
-
});
|