@asteby/metacore-runtime-react 7.1.5 → 9.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 +72 -0
- package/dist/__tests__/dynamic-form.test.d.ts +2 -0
- package/dist/__tests__/dynamic-form.test.d.ts.map +1 -0
- package/dist/__tests__/dynamic-form.test.js +93 -0
- package/dist/__tests__/dynamic-relation.test.d.ts +2 -0
- package/dist/__tests__/dynamic-relation.test.d.ts.map +1 -0
- package/dist/__tests__/dynamic-relation.test.js +228 -0
- package/dist/dynamic-form-schema.d.ts +7 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -0
- package/dist/dynamic-form-schema.js +68 -0
- package/dist/dynamic-form.d.ts +2 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +28 -9
- package/dist/dynamic-relation-helpers.d.ts +77 -0
- package/dist/dynamic-relation-helpers.d.ts.map +1 -0
- package/dist/dynamic-relation-helpers.js +186 -0
- package/dist/dynamic-relation.d.ts +64 -0
- package/dist/dynamic-relation.d.ts.map +1 -0
- package/dist/dynamic-relation.js +226 -0
- 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 +9 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/relations.md +290 -0
- package/package.json +10 -5
- package/src/__tests__/dynamic-form.test.ts +104 -0
- package/src/__tests__/dynamic-relation.test.ts +293 -0
- package/src/dynamic-form-schema.ts +66 -0
- package/src/dynamic-form.tsx +34 -9
- package/src/dynamic-relation-helpers.ts +226 -0
- package/src/dynamic-relation.tsx +497 -0
- package/src/index.ts +10 -0
- package/src/types.ts +24 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ActionFieldDef, ColumnDefinition, TableMetadata } from './types';
|
|
2
|
+
export type DynamicRelationKind = 'one_to_many' | 'many_to_many';
|
|
3
|
+
export interface PivotRowLike {
|
|
4
|
+
id?: string | number | null;
|
|
5
|
+
[k: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface TargetRowLike {
|
|
8
|
+
id?: string | number | null;
|
|
9
|
+
[k: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Builds the query params used by `<DynamicRelation kind="one_to_many">` to
|
|
13
|
+
* scope a child list to a single parent record. Mirrors the
|
|
14
|
+
* `f_<column>=eq:<value>` convention enforced by `query/params.go` in the
|
|
15
|
+
* kernel.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildRelationFilterParams(foreignKey: string, parentId: string | number): Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Builds the POST body for creating a child row. The foreign key is forced to
|
|
20
|
+
* `parentId` regardless of what the form returned — the inline form hides the
|
|
21
|
+
* FK input but a misbehaving caller (or a manual override) should not be able
|
|
22
|
+
* to redirect the row to a different parent.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildCreatePayload(foreignKey: string, parentId: string | number, formValues: Record<string, any>): Record<string, any>;
|
|
25
|
+
/**
|
|
26
|
+
* Maps a `TableMetadata.columns` shape into `ActionFieldDef[]` so the inline
|
|
27
|
+
* form can be rendered with `<DynamicForm>`. Excludes the foreign-key column
|
|
28
|
+
* (already fixed to parentId) and any column flagged `hidden`. Falls back to
|
|
29
|
+
* sensible widget hints derived from `ColumnDefinition.type`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function deriveRelationFormFields(metadata: Pick<TableMetadata, 'columns'> | null | undefined, foreignKey: string): ActionFieldDef[];
|
|
32
|
+
/**
|
|
33
|
+
* Stable id for relation rows. Falls back to a synthetic
|
|
34
|
+
* `__rel-<foreignKey>-<index>` when the row has no id — keeps React keys
|
|
35
|
+
* stable and lets optimistic creates render before the backend assigns an id.
|
|
36
|
+
*/
|
|
37
|
+
export declare function relationRowKey(row: {
|
|
38
|
+
id?: string | number | null;
|
|
39
|
+
} | undefined, index: number, foreignKey: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Builds the POST body for attaching a target row to the parent through the
|
|
42
|
+
* pivot table. Both FKs are required: `foreignKey -> parentId` (pivot side)
|
|
43
|
+
* and `referencesKey -> targetId` (target side). Extra pivot fields can be
|
|
44
|
+
* passed via `extra` (e.g. `role`, `position`); never override the two FKs.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildPivotAttachPayload(foreignKey: string, parentId: string | number, referencesKey: string, targetId: string | number, extra?: Record<string, any>): Record<string, any>;
|
|
47
|
+
/**
|
|
48
|
+
* From a list of pivot rows, returns the set of currently-attached target ids
|
|
49
|
+
* coerced to string (the form expected by `<MultiSelect>` `selected`).
|
|
50
|
+
* Skips rows missing the FK (defensive — the kernel should never return them).
|
|
51
|
+
*/
|
|
52
|
+
export declare function extractSelectedTargetIds(pivotRows: ReadonlyArray<PivotRowLike> | null | undefined, referencesKey: string): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Builds a `targetId -> pivotRowId` lookup so detach can DELETE the pivot row
|
|
55
|
+
* by its own id without re-fetching. When several pivot rows point at the
|
|
56
|
+
* same target (shouldn't happen with a unique constraint but the kernel does
|
|
57
|
+
* not guarantee it), the *last* one wins — detach will only drop one at a
|
|
58
|
+
* time, the next render will surface the leftover.
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildPivotRowIndex(pivotRows: ReadonlyArray<PivotRowLike> | null | undefined, referencesKey: string): Map<string, string | number>;
|
|
61
|
+
/**
|
|
62
|
+
* Diffs the previous selection against the next one and returns the work to
|
|
63
|
+
* apply: target ids that need a POST (toAdd) and ids that need a DELETE
|
|
64
|
+
* (toRemove). Both arrays are deduped and order-stable to make tests
|
|
65
|
+
* deterministic.
|
|
66
|
+
*/
|
|
67
|
+
export declare function diffSelection(prev: ReadonlyArray<string>, next: ReadonlyArray<string>): {
|
|
68
|
+
toAdd: string[];
|
|
69
|
+
toRemove: string[];
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Picks a human-readable label for a target row. Tries `displayKey` first,
|
|
73
|
+
* then walks the metadata columns in order looking for the first non-id /
|
|
74
|
+
* non-FK string-ish value. Falls back to the row id, then to '—'.
|
|
75
|
+
*/
|
|
76
|
+
export declare function pickOptionLabel(row: TargetRowLike | null | undefined, displayKey: string | undefined, columns: ReadonlyArray<ColumnDefinition> | null | undefined): string;
|
|
77
|
+
//# sourceMappingURL=dynamic-relation-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-relation-helpers.d.ts","sourceRoot":"","sources":["../src/dynamic-relation-helpers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE9E,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,cAAc,CAAA;AAEhE,MAAM,WAAW,YAAY;IACzB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAGrB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACpC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3D,UAAU,EAAE,MAAM,GACnB,cAAc,EAAE,CAelB;AAcD;;;;GAIG;AACH,wBAAgB,cAAc,CAC1B,GAAG,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,SAAS,EAChD,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,GACnB,MAAM,CAKR;AAMD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACnC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAcrB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACpC,SAAS,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EACzD,aAAa,EAAE,MAAM,GACtB,MAAM,EAAE,CASV;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAC9B,SAAS,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EACzD,aAAa,EAAE,MAAM,GACtB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAU9B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CACzB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAC3B,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAC5B;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAYzC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC3B,GAAG,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACrC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,EAAE,aAAa,CAAC,gBAAgB,CAAC,GAAG,IAAI,GAAG,SAAS,GAC5D,MAAM,CAiBR"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the query params used by `<DynamicRelation kind="one_to_many">` to
|
|
3
|
+
* scope a child list to a single parent record. Mirrors the
|
|
4
|
+
* `f_<column>=eq:<value>` convention enforced by `query/params.go` in the
|
|
5
|
+
* kernel.
|
|
6
|
+
*/
|
|
7
|
+
export function buildRelationFilterParams(foreignKey, parentId) {
|
|
8
|
+
if (!foreignKey)
|
|
9
|
+
throw new Error('foreignKey requerido');
|
|
10
|
+
if (parentId === undefined || parentId === null || parentId === '') {
|
|
11
|
+
throw new Error('parentId requerido');
|
|
12
|
+
}
|
|
13
|
+
return { [`f_${foreignKey}`]: `eq:${String(parentId)}` };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Builds the POST body for creating a child row. The foreign key is forced to
|
|
17
|
+
* `parentId` regardless of what the form returned — the inline form hides the
|
|
18
|
+
* FK input but a misbehaving caller (or a manual override) should not be able
|
|
19
|
+
* to redirect the row to a different parent.
|
|
20
|
+
*/
|
|
21
|
+
export function buildCreatePayload(foreignKey, parentId, formValues) {
|
|
22
|
+
if (!foreignKey)
|
|
23
|
+
throw new Error('foreignKey requerido');
|
|
24
|
+
return { ...formValues, [foreignKey]: parentId };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Maps a `TableMetadata.columns` shape into `ActionFieldDef[]` so the inline
|
|
28
|
+
* form can be rendered with `<DynamicForm>`. Excludes the foreign-key column
|
|
29
|
+
* (already fixed to parentId) and any column flagged `hidden`. Falls back to
|
|
30
|
+
* sensible widget hints derived from `ColumnDefinition.type`.
|
|
31
|
+
*/
|
|
32
|
+
export function deriveRelationFormFields(metadata, foreignKey) {
|
|
33
|
+
if (!metadata?.columns)
|
|
34
|
+
return [];
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const col of metadata.columns) {
|
|
37
|
+
if (col.key === foreignKey)
|
|
38
|
+
continue;
|
|
39
|
+
if (col.hidden)
|
|
40
|
+
continue;
|
|
41
|
+
out.push({
|
|
42
|
+
key: col.key,
|
|
43
|
+
label: col.label,
|
|
44
|
+
type: columnTypeToFieldType(col),
|
|
45
|
+
required: false,
|
|
46
|
+
options: col.options?.map(o => ({ value: String(o.value), label: o.label })),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function columnTypeToFieldType(col) {
|
|
52
|
+
switch (col.type) {
|
|
53
|
+
case 'number': return 'number';
|
|
54
|
+
case 'boolean': return 'boolean';
|
|
55
|
+
case 'date': return 'date';
|
|
56
|
+
case 'select': return 'select';
|
|
57
|
+
case 'text':
|
|
58
|
+
default:
|
|
59
|
+
return 'string';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Stable id for relation rows. Falls back to a synthetic
|
|
64
|
+
* `__rel-<foreignKey>-<index>` when the row has no id — keeps React keys
|
|
65
|
+
* stable and lets optimistic creates render before the backend assigns an id.
|
|
66
|
+
*/
|
|
67
|
+
export function relationRowKey(row, index, foreignKey) {
|
|
68
|
+
if (row && row.id !== undefined && row.id !== null && row.id !== '') {
|
|
69
|
+
return String(row.id);
|
|
70
|
+
}
|
|
71
|
+
return `__rel-${foreignKey}-${index}`;
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// many_to_many helpers
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* Builds the POST body for attaching a target row to the parent through the
|
|
78
|
+
* pivot table. Both FKs are required: `foreignKey -> parentId` (pivot side)
|
|
79
|
+
* and `referencesKey -> targetId` (target side). Extra pivot fields can be
|
|
80
|
+
* passed via `extra` (e.g. `role`, `position`); never override the two FKs.
|
|
81
|
+
*/
|
|
82
|
+
export function buildPivotAttachPayload(foreignKey, parentId, referencesKey, targetId, extra) {
|
|
83
|
+
if (!foreignKey)
|
|
84
|
+
throw new Error('foreignKey requerido');
|
|
85
|
+
if (!referencesKey)
|
|
86
|
+
throw new Error('referencesKey requerido');
|
|
87
|
+
if (parentId === undefined || parentId === null || parentId === '') {
|
|
88
|
+
throw new Error('parentId requerido');
|
|
89
|
+
}
|
|
90
|
+
if (targetId === undefined || targetId === null || targetId === '') {
|
|
91
|
+
throw new Error('targetId requerido');
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
...(extra || {}),
|
|
95
|
+
[foreignKey]: parentId,
|
|
96
|
+
[referencesKey]: targetId,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* From a list of pivot rows, returns the set of currently-attached target ids
|
|
101
|
+
* coerced to string (the form expected by `<MultiSelect>` `selected`).
|
|
102
|
+
* Skips rows missing the FK (defensive — the kernel should never return them).
|
|
103
|
+
*/
|
|
104
|
+
export function extractSelectedTargetIds(pivotRows, referencesKey) {
|
|
105
|
+
if (!pivotRows || !referencesKey)
|
|
106
|
+
return [];
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const row of pivotRows) {
|
|
109
|
+
const v = row[referencesKey];
|
|
110
|
+
if (v === undefined || v === null || v === '')
|
|
111
|
+
continue;
|
|
112
|
+
out.push(String(v));
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Builds a `targetId -> pivotRowId` lookup so detach can DELETE the pivot row
|
|
118
|
+
* by its own id without re-fetching. When several pivot rows point at the
|
|
119
|
+
* same target (shouldn't happen with a unique constraint but the kernel does
|
|
120
|
+
* not guarantee it), the *last* one wins — detach will only drop one at a
|
|
121
|
+
* time, the next render will surface the leftover.
|
|
122
|
+
*/
|
|
123
|
+
export function buildPivotRowIndex(pivotRows, referencesKey) {
|
|
124
|
+
const map = new Map();
|
|
125
|
+
if (!pivotRows || !referencesKey)
|
|
126
|
+
return map;
|
|
127
|
+
for (const row of pivotRows) {
|
|
128
|
+
const target = row[referencesKey];
|
|
129
|
+
if (target === undefined || target === null || target === '')
|
|
130
|
+
continue;
|
|
131
|
+
if (row.id === undefined || row.id === null || row.id === '')
|
|
132
|
+
continue;
|
|
133
|
+
map.set(String(target), row.id);
|
|
134
|
+
}
|
|
135
|
+
return map;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Diffs the previous selection against the next one and returns the work to
|
|
139
|
+
* apply: target ids that need a POST (toAdd) and ids that need a DELETE
|
|
140
|
+
* (toRemove). Both arrays are deduped and order-stable to make tests
|
|
141
|
+
* deterministic.
|
|
142
|
+
*/
|
|
143
|
+
export function diffSelection(prev, next) {
|
|
144
|
+
const prevSet = new Set(prev);
|
|
145
|
+
const nextSet = new Set(next);
|
|
146
|
+
const toAdd = [];
|
|
147
|
+
for (const id of next) {
|
|
148
|
+
if (!prevSet.has(id) && !toAdd.includes(id))
|
|
149
|
+
toAdd.push(id);
|
|
150
|
+
}
|
|
151
|
+
const toRemove = [];
|
|
152
|
+
for (const id of prev) {
|
|
153
|
+
if (!nextSet.has(id) && !toRemove.includes(id))
|
|
154
|
+
toRemove.push(id);
|
|
155
|
+
}
|
|
156
|
+
return { toAdd, toRemove };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Picks a human-readable label for a target row. Tries `displayKey` first,
|
|
160
|
+
* then walks the metadata columns in order looking for the first non-id /
|
|
161
|
+
* non-FK string-ish value. Falls back to the row id, then to '—'.
|
|
162
|
+
*/
|
|
163
|
+
export function pickOptionLabel(row, displayKey, columns) {
|
|
164
|
+
if (!row)
|
|
165
|
+
return '—';
|
|
166
|
+
if (displayKey) {
|
|
167
|
+
const v = row[displayKey];
|
|
168
|
+
if (v !== undefined && v !== null && v !== '')
|
|
169
|
+
return String(v);
|
|
170
|
+
}
|
|
171
|
+
if (columns) {
|
|
172
|
+
for (const col of columns) {
|
|
173
|
+
if (col.key === 'id' || col.hidden)
|
|
174
|
+
continue;
|
|
175
|
+
const v = row[col.key];
|
|
176
|
+
if (v === undefined || v === null || v === '')
|
|
177
|
+
continue;
|
|
178
|
+
if (typeof v === 'object')
|
|
179
|
+
continue;
|
|
180
|
+
return String(v);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (row.id !== undefined && row.id !== null && row.id !== '')
|
|
184
|
+
return String(row.id);
|
|
185
|
+
return '—';
|
|
186
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type { DynamicRelationKind } from './dynamic-relation-helpers';
|
|
2
|
+
export { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
|
|
3
|
+
export interface DynamicRelationStrings {
|
|
4
|
+
title: string;
|
|
5
|
+
emptyState: string;
|
|
6
|
+
addLabel: string;
|
|
7
|
+
editLabel: string;
|
|
8
|
+
removeLabel: string;
|
|
9
|
+
confirmRemoveTitle: string;
|
|
10
|
+
confirmRemoveDescription: string;
|
|
11
|
+
cancelLabel: string;
|
|
12
|
+
saveLabel: string;
|
|
13
|
+
selectPlaceholder: string;
|
|
14
|
+
selectSearchPlaceholder: string;
|
|
15
|
+
selectEmpty: string;
|
|
16
|
+
}
|
|
17
|
+
interface CommonProps {
|
|
18
|
+
/** id del registro padre. */
|
|
19
|
+
parentId: string | number;
|
|
20
|
+
/** Hidden columns; el FK siempre se oculta automáticamente. */
|
|
21
|
+
hiddenColumns?: string[];
|
|
22
|
+
/** Permisos visibles. Default true. */
|
|
23
|
+
canCreate?: boolean;
|
|
24
|
+
canDelete?: boolean;
|
|
25
|
+
canEdit?: boolean;
|
|
26
|
+
/** Strings traducibles. */
|
|
27
|
+
strings?: Partial<DynamicRelationStrings>;
|
|
28
|
+
/** Wrapper className. */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Callback opcional cuando la selección o la lista cambia. */
|
|
31
|
+
onChange?: () => void;
|
|
32
|
+
}
|
|
33
|
+
export interface DynamicRelationOneToManyProps extends CommonProps {
|
|
34
|
+
kind: 'one_to_many';
|
|
35
|
+
/** Modelo hijo (lado N) cuyas filas se listan filtradas por `foreignKey == parentId`. */
|
|
36
|
+
model: string;
|
|
37
|
+
/** Foreign key del lado N que apunta al padre. */
|
|
38
|
+
foreignKey: string;
|
|
39
|
+
/** Endpoint override; default `/data/${model}`. */
|
|
40
|
+
endpoint?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface DynamicRelationManyToManyProps extends CommonProps {
|
|
43
|
+
kind: 'many_to_many';
|
|
44
|
+
/** Tabla pivote (`through`). FK al padre vive acá como `foreignKey`. */
|
|
45
|
+
through: string;
|
|
46
|
+
/** Tabla destino (`references`) sobre la que se hace multi-select. */
|
|
47
|
+
references: string;
|
|
48
|
+
/** FK del pivot al padre. */
|
|
49
|
+
foreignKey: string;
|
|
50
|
+
/** FK del pivot a la tabla destino (default `${references}_id`). */
|
|
51
|
+
referencesKey?: string;
|
|
52
|
+
/** Override del endpoint del pivot; default `/data/${through}`. */
|
|
53
|
+
pivotEndpoint?: string;
|
|
54
|
+
/** Override del endpoint del target; default `/data/${references}`. */
|
|
55
|
+
referencesEndpoint?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Columna del target que se usa como label en el multi-select. Si no se
|
|
58
|
+
* pasa, se infiere de la metadata (primer columna no-id, no-hidden).
|
|
59
|
+
*/
|
|
60
|
+
displayKey?: string;
|
|
61
|
+
}
|
|
62
|
+
export type DynamicRelationProps = DynamicRelationOneToManyProps | DynamicRelationManyToManyProps;
|
|
63
|
+
export declare function DynamicRelation(props: DynamicRelationProps): import("react/jsx-runtime").JSX.Element;
|
|
64
|
+
//# sourceMappingURL=dynamic-relation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AAyCA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,eAAe,EACf,cAAc,GACjB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;CACtB;AAiBD,UAAU,WAAW;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,6BAA8B,SAAQ,WAAW;IAC9D,IAAI,EAAE,aAAa,CAAA;IACnB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,8BAA+B,SAAQ,WAAW;IAC/D,IAAI,EAAE,cAAc,CAAA;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC1B,6BAA6B,GAC7B,8BAA8B,CAAA;AAEpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAK1D"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// DynamicRelation — primitivo metadata-driven que renderiza el lado N de una
|
|
3
|
+
// relación 1:N o N:N entre modelos. Cubre dos kinds:
|
|
4
|
+
// - "one_to_many": lista inline editable que cuelga del registro padre.
|
|
5
|
+
// - "many_to_many": multi-select sobre la tabla destino con sync a la pivot.
|
|
6
|
+
// La RFC completa vive en `packages/runtime-react/docs/relations.md`.
|
|
7
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
8
|
+
import { Button, Skeleton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Dialog, DialogContent, DialogHeader, DialogTitle, MultiSelect, } from '@asteby/metacore-ui/primitives';
|
|
9
|
+
import { Plus, Trash2, Pencil } from 'lucide-react';
|
|
10
|
+
import { useApi } from './api-context';
|
|
11
|
+
import { useMetadataCache } from './metadata-cache';
|
|
12
|
+
import { DynamicForm } from './dynamic-form';
|
|
13
|
+
import { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
|
|
14
|
+
export { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
|
|
15
|
+
const DEFAULT_STRINGS = {
|
|
16
|
+
title: '',
|
|
17
|
+
emptyState: 'No hay registros relacionados.',
|
|
18
|
+
addLabel: 'Agregar',
|
|
19
|
+
editLabel: 'Editar',
|
|
20
|
+
removeLabel: 'Quitar',
|
|
21
|
+
confirmRemoveTitle: '¿Quitar el registro?',
|
|
22
|
+
confirmRemoveDescription: 'Esta acción no se puede deshacer.',
|
|
23
|
+
cancelLabel: 'Cancelar',
|
|
24
|
+
saveLabel: 'Guardar',
|
|
25
|
+
selectPlaceholder: 'Seleccionar…',
|
|
26
|
+
selectSearchPlaceholder: 'Buscar…',
|
|
27
|
+
selectEmpty: 'Sin resultados.',
|
|
28
|
+
};
|
|
29
|
+
export function DynamicRelation(props) {
|
|
30
|
+
if (props.kind === 'many_to_many') {
|
|
31
|
+
return _jsx(ManyToManyRelation, { ...props });
|
|
32
|
+
}
|
|
33
|
+
return _jsx(OneToManyRelation, { ...props });
|
|
34
|
+
}
|
|
35
|
+
function OneToManyRelation({ kind, model, foreignKey, parentId, endpoint, hiddenColumns = [], canCreate = true, canDelete = true, canEdit = true, strings, className, onChange, }) {
|
|
36
|
+
const api = useApi();
|
|
37
|
+
const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache();
|
|
38
|
+
const cachedMeta = getMetadata(model);
|
|
39
|
+
const labels = { ...DEFAULT_STRINGS, ...(strings || {}) };
|
|
40
|
+
const [metadata, setMetadata] = useState(cachedMeta || null);
|
|
41
|
+
const [rows, setRows] = useState([]);
|
|
42
|
+
const [loading, setLoading] = useState(true);
|
|
43
|
+
const [formOpen, setFormOpen] = useState(false);
|
|
44
|
+
const [editingRow, setEditingRow] = useState(null);
|
|
45
|
+
const [rowToDelete, setRowToDelete] = useState(null);
|
|
46
|
+
const [submitting, setSubmitting] = useState(false);
|
|
47
|
+
const dataEndpoint = endpoint || `/data/${model}`;
|
|
48
|
+
const fetchAll = useCallback(async () => {
|
|
49
|
+
setLoading(true);
|
|
50
|
+
try {
|
|
51
|
+
const params = buildRelationFilterParams(foreignKey, parentId);
|
|
52
|
+
const [metaRes, dataRes] = await Promise.all([
|
|
53
|
+
metadata ? Promise.resolve(null) : api.get(`/metadata/table/${model}`),
|
|
54
|
+
api.get(dataEndpoint, { params }),
|
|
55
|
+
]);
|
|
56
|
+
if (metaRes && metaRes.data?.success) {
|
|
57
|
+
const fresh = metaRes.data.data;
|
|
58
|
+
setMetadata(fresh);
|
|
59
|
+
cacheMetadata(model, fresh);
|
|
60
|
+
}
|
|
61
|
+
const list = dataRes.data;
|
|
62
|
+
if (list.success)
|
|
63
|
+
setRows(list.data || []);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.error('DynamicRelation fetch error', err);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
setLoading(false);
|
|
70
|
+
}
|
|
71
|
+
}, [api, dataEndpoint, foreignKey, parentId, metadata, model, cacheMetadata]);
|
|
72
|
+
useEffect(() => { fetchAll(); }, [fetchAll]);
|
|
73
|
+
const formFields = useMemo(() => deriveRelationFormFields(metadata, foreignKey), [metadata, foreignKey]);
|
|
74
|
+
const visibleColumns = useMemo(() => {
|
|
75
|
+
if (!metadata?.columns)
|
|
76
|
+
return [];
|
|
77
|
+
const hidden = new Set([foreignKey, ...hiddenColumns]);
|
|
78
|
+
return metadata.columns.filter(c => !hidden.has(c.key) && !c.hidden);
|
|
79
|
+
}, [metadata, foreignKey, hiddenColumns]);
|
|
80
|
+
const handleSubmit = useCallback(async (values) => {
|
|
81
|
+
setSubmitting(true);
|
|
82
|
+
try {
|
|
83
|
+
if (editingRow) {
|
|
84
|
+
const res = await api.put(`${dataEndpoint}/${editingRow.id}`, values);
|
|
85
|
+
if (!res.data?.success)
|
|
86
|
+
throw new Error('update failed');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const payload = buildCreatePayload(foreignKey, parentId, values);
|
|
90
|
+
const res = await api.post(dataEndpoint, payload);
|
|
91
|
+
if (!res.data?.success)
|
|
92
|
+
throw new Error('create failed');
|
|
93
|
+
}
|
|
94
|
+
setFormOpen(false);
|
|
95
|
+
setEditingRow(null);
|
|
96
|
+
await fetchAll();
|
|
97
|
+
onChange?.();
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
console.error('DynamicRelation submit error', err);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
setSubmitting(false);
|
|
104
|
+
}
|
|
105
|
+
}, [api, dataEndpoint, editingRow, fetchAll, foreignKey, onChange, parentId]);
|
|
106
|
+
const handleDelete = useCallback(async () => {
|
|
107
|
+
if (!rowToDelete)
|
|
108
|
+
return;
|
|
109
|
+
setSubmitting(true);
|
|
110
|
+
try {
|
|
111
|
+
const res = await api.delete(`${dataEndpoint}/${rowToDelete.id}`);
|
|
112
|
+
if (!res.data?.success)
|
|
113
|
+
throw new Error('delete failed');
|
|
114
|
+
setRowToDelete(null);
|
|
115
|
+
await fetchAll();
|
|
116
|
+
onChange?.();
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
console.error('DynamicRelation delete error', err);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
setSubmitting(false);
|
|
123
|
+
}
|
|
124
|
+
}, [api, dataEndpoint, fetchAll, onChange, rowToDelete]);
|
|
125
|
+
return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate) && (_jsxs("div", { className: "flex items-center justify-between pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), canCreate && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => { setEditingRow(null); setFormOpen(true); }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), labels.addLabel] }))] })), loading ? (_jsx("div", { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-10 w-full" }, `rel-skeleton-${i}`))) })) : rows.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsx("div", { className: "border rounded-md divide-y bg-card", children: rows.map((row, idx) => (_jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2", children: [_jsx("div", { className: "flex-1 grid grid-cols-[repeat(auto-fit,minmax(0,1fr))] gap-2 text-sm", children: visibleColumns.map(col => (_jsx("span", { className: "truncate", title: String(row[col.key] ?? ''), children: formatCell(row[col.key]) }, col.key))) }), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [canEdit && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => { setEditingRow(row); setFormOpen(true); }, "aria-label": labels.editLabel, children: _jsx(Pencil, { className: "h-4 w-4" }) })), canDelete && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setRowToDelete(row), "aria-label": labels.removeLabel, children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })] }, relationRowKey(row, idx, foreignKey)))) })), _jsx(Dialog, { open: formOpen, onOpenChange: (open) => { setFormOpen(open); if (!open)
|
|
126
|
+
setEditingRow(null); }, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: editingRow ? labels.editLabel : labels.addLabel }) }), _jsx(DynamicForm, { fields: formFields, initialValues: editingRow || undefined, onSubmit: handleSubmit, onCancel: () => { setFormOpen(false); setEditingRow(null); }, submitLabel: labels.saveLabel, cancelLabel: labels.cancelLabel, disabled: submitting })] }) }), _jsx(AlertDialog, { open: !!rowToDelete, onOpenChange: (open) => !open && setRowToDelete(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: labels.confirmRemoveTitle }), _jsx(AlertDialogDescription, { children: labels.confirmRemoveDescription })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: submitting, children: labels.cancelLabel }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); handleDelete(); }, className: "bg-red-600 hover:bg-red-700", disabled: submitting, children: labels.removeLabel })] })] }) })] }));
|
|
127
|
+
}
|
|
128
|
+
function formatCell(value) {
|
|
129
|
+
if (value === null || value === undefined)
|
|
130
|
+
return '—';
|
|
131
|
+
if (typeof value === 'boolean')
|
|
132
|
+
return value ? '✓' : '—';
|
|
133
|
+
if (typeof value === 'object')
|
|
134
|
+
return JSON.stringify(value);
|
|
135
|
+
return String(value);
|
|
136
|
+
}
|
|
137
|
+
function ManyToManyRelation({ kind, through, references, foreignKey, referencesKey, parentId, pivotEndpoint, referencesEndpoint, displayKey, canCreate = true, canDelete = true, strings, className, onChange, }) {
|
|
138
|
+
const api = useApi();
|
|
139
|
+
const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache();
|
|
140
|
+
const labels = { ...DEFAULT_STRINGS, ...(strings || {}) };
|
|
141
|
+
const refKey = referencesKey || `${references}_id`;
|
|
142
|
+
const pivotPath = pivotEndpoint || `/data/${through}`;
|
|
143
|
+
const targetPath = referencesEndpoint || `/data/${references}`;
|
|
144
|
+
const cachedTargetMeta = getMetadata(references);
|
|
145
|
+
const [targetMeta, setTargetMeta] = useState(cachedTargetMeta || null);
|
|
146
|
+
const [targetRows, setTargetRows] = useState([]);
|
|
147
|
+
const [pivotRows, setPivotRows] = useState([]);
|
|
148
|
+
const [loading, setLoading] = useState(true);
|
|
149
|
+
const [syncing, setSyncing] = useState(false);
|
|
150
|
+
const fetchAll = useCallback(async () => {
|
|
151
|
+
setLoading(true);
|
|
152
|
+
try {
|
|
153
|
+
const params = buildRelationFilterParams(foreignKey, parentId);
|
|
154
|
+
const [metaRes, pivotRes, targetRes] = await Promise.all([
|
|
155
|
+
targetMeta ? Promise.resolve(null) : api.get(`/metadata/table/${references}`),
|
|
156
|
+
api.get(pivotPath, { params }),
|
|
157
|
+
api.get(targetPath),
|
|
158
|
+
]);
|
|
159
|
+
if (metaRes && metaRes.data?.success) {
|
|
160
|
+
const fresh = metaRes.data.data;
|
|
161
|
+
setTargetMeta(fresh);
|
|
162
|
+
cacheMetadata(references, fresh);
|
|
163
|
+
}
|
|
164
|
+
const pivotList = pivotRes.data;
|
|
165
|
+
if (pivotList.success)
|
|
166
|
+
setPivotRows(pivotList.data || []);
|
|
167
|
+
const targetList = targetRes.data;
|
|
168
|
+
if (targetList.success)
|
|
169
|
+
setTargetRows(targetList.data || []);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
console.error('DynamicRelation m2m fetch error', err);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
setLoading(false);
|
|
176
|
+
}
|
|
177
|
+
}, [api, pivotPath, targetPath, foreignKey, parentId, references, targetMeta, cacheMetadata]);
|
|
178
|
+
useEffect(() => { fetchAll(); }, [fetchAll]);
|
|
179
|
+
const options = useMemo(() => {
|
|
180
|
+
return targetRows
|
|
181
|
+
.filter(r => r && r.id !== undefined && r.id !== null && r.id !== '')
|
|
182
|
+
.map(r => ({
|
|
183
|
+
value: String(r.id),
|
|
184
|
+
label: pickOptionLabel(r, displayKey, targetMeta?.columns),
|
|
185
|
+
}));
|
|
186
|
+
}, [targetRows, displayKey, targetMeta]);
|
|
187
|
+
const selectedIds = useMemo(() => extractSelectedTargetIds(pivotRows, refKey), [pivotRows, refKey]);
|
|
188
|
+
const pivotIndex = useMemo(() => buildPivotRowIndex(pivotRows, refKey), [pivotRows, refKey]);
|
|
189
|
+
const handleChange = useCallback(async (next) => {
|
|
190
|
+
if (syncing)
|
|
191
|
+
return;
|
|
192
|
+
const { toAdd, toRemove } = diffSelection(selectedIds, next);
|
|
193
|
+
if (toAdd.length === 0 && toRemove.length === 0)
|
|
194
|
+
return;
|
|
195
|
+
if (toAdd.length > 0 && !canCreate)
|
|
196
|
+
return;
|
|
197
|
+
if (toRemove.length > 0 && !canDelete)
|
|
198
|
+
return;
|
|
199
|
+
setSyncing(true);
|
|
200
|
+
try {
|
|
201
|
+
for (const targetId of toAdd) {
|
|
202
|
+
const payload = buildPivotAttachPayload(foreignKey, parentId, refKey, targetId);
|
|
203
|
+
const res = await api.post(pivotPath, payload);
|
|
204
|
+
if (!res.data?.success)
|
|
205
|
+
throw new Error('attach failed');
|
|
206
|
+
}
|
|
207
|
+
for (const targetId of toRemove) {
|
|
208
|
+
const pivotId = pivotIndex.get(targetId);
|
|
209
|
+
if (pivotId === undefined)
|
|
210
|
+
continue;
|
|
211
|
+
const res = await api.delete(`${pivotPath}/${pivotId}`);
|
|
212
|
+
if (!res.data?.success)
|
|
213
|
+
throw new Error('detach failed');
|
|
214
|
+
}
|
|
215
|
+
await fetchAll();
|
|
216
|
+
onChange?.();
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
console.error('DynamicRelation m2m sync error', err);
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
setSyncing(false);
|
|
223
|
+
}
|
|
224
|
+
}, [api, canCreate, canDelete, fetchAll, foreignKey, onChange, parentId, pivotIndex, pivotPath, refKey, selectedIds, syncing]);
|
|
225
|
+
return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-through": through, "data-relation-references": references, children: [labels.title && (_jsx("div", { className: "pb-3", children: _jsx("h3", { className: "text-sm font-medium", children: labels.title }) })), loading ? (_jsx(Skeleton, { className: "h-10 w-full" })) : options.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsx(MultiSelect, { options: options, selected: selectedIds, onChange: handleChange, placeholder: labels.selectPlaceholder, searchPlaceholder: labels.selectSearchPlaceholder, emptyMessage: labels.selectEmpty }))] }));
|
|
226
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,5 +17,6 @@ export { DynamicRecordDialog } from './dialogs/dynamic-record';
|
|
|
17
17
|
export { ExportDialog } from './dialogs/export';
|
|
18
18
|
export { ImportDialog } from './dialogs/import';
|
|
19
19
|
export { DynamicCRUDPage, type DynamicCRUDPageProps, type DynamicCRUDPageStrings, type DynamicCRUDPageClasses, } from './dynamic-crud-page';
|
|
20
|
+
export { DynamicRelation, type DynamicRelationProps, type DynamicRelationStrings, type DynamicRelationKind, buildRelationFilterParams, buildCreatePayload, deriveRelationFormFields, relationRowKey, } from './dynamic-relation';
|
|
20
21
|
export { registerModelExtension, getModelExtension, clearModelExtensions, type ModelExtension, type ModelExtensionProps, } from './model-extension-registry';
|
|
21
22
|
//# 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,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"}
|
package/dist/index.js
CHANGED
|
@@ -21,4 +21,5 @@ export { DynamicRecordDialog } from './dialogs/dynamic-record';
|
|
|
21
21
|
export { ExportDialog } from './dialogs/export';
|
|
22
22
|
export { ImportDialog } from './dialogs/import';
|
|
23
23
|
export { DynamicCRUDPage, } from './dynamic-crud-page';
|
|
24
|
+
export { DynamicRelation, buildRelationFilterParams, buildCreatePayload, deriveRelationFormFields, relationRowKey, } from './dynamic-relation';
|
|
24
25
|
export { registerModelExtension, getModelExtension, clearModelExtensions, } from './model-extension-registry';
|
package/dist/types.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ export interface ActionCondition {
|
|
|
56
56
|
operator: 'eq' | 'neq' | 'in' | 'not_in';
|
|
57
57
|
value: string | string[];
|
|
58
58
|
}
|
|
59
|
+
export interface FieldValidation {
|
|
60
|
+
regex?: string;
|
|
61
|
+
min?: number;
|
|
62
|
+
max?: number;
|
|
63
|
+
custom?: string;
|
|
64
|
+
}
|
|
65
|
+
export type FieldWidget = 'text' | 'textarea' | 'richtext' | 'color' | 'number' | 'date' | 'select' | 'switch';
|
|
59
66
|
export interface ActionFieldDef {
|
|
60
67
|
key: string;
|
|
61
68
|
label: string;
|
|
@@ -68,6 +75,8 @@ export interface ActionFieldDef {
|
|
|
68
75
|
defaultValue?: any;
|
|
69
76
|
placeholder?: string;
|
|
70
77
|
searchEndpoint?: string;
|
|
78
|
+
validation?: FieldValidation;
|
|
79
|
+
widget?: FieldWidget | string;
|
|
71
80
|
}
|
|
72
81
|
export interface ActionDefinition {
|
|
73
82
|
key: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,qBAAqB,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,eAAe,GAAG,OAAO,CAAA;IAC3I,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC9E;AAED,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,QAAQ,CAAA;IACxC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC3B;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|