@asteby/metacore-runtime-react 25.1.4 → 25.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 +16 -0
- package/dist/dialogs/normalize-submit.d.ts +17 -4
- package/dist/dialogs/normalize-submit.d.ts.map +1 -1
- package/dist/dialogs/normalize-submit.js +43 -10
- package/dist/dynamic-relation.d.ts +9 -0
- package/dist/dynamic-relation.d.ts.map +1 -1
- package/dist/dynamic-relation.js +15 -2
- package/dist/dynamic-relations.d.ts.map +1 -1
- package/dist/dynamic-relations.js +6 -2
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/dynamic-relation-readonly.test.tsx +92 -0
- package/src/__tests__/normalize-ref-fields.test.ts +45 -0
- package/src/dialogs/normalize-submit.ts +46 -17
- package/src/dynamic-relation.tsx +26 -0
- package/src/dynamic-relations.tsx +6 -0
- package/src/types.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 25.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 62f273a: DynamicRelation/DynamicRelations: honor a `readonly` flag on the kernel relation metadata (`RelationMeta.readonly`, camelCase alias `readOnly`). When a relation is read-only the panel forces canCreate/canEdit/canDelete = false regardless of the perms the host passes, hiding the "Agregar" (Plus) button and the per-row edit (Pencil) / delete (Trash2) controls. Backwards compatible: when the flag is absent/false the panel keeps deriving its controls from the host props.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 62f273a: normalize-submit: prefer the explicit `nullable` field flag served by the kernel (v0.77.1+) when deciding to null an empty reference, falling back to the type-based heuristic for older hosts that don't serve the flag.
|
|
12
|
+
|
|
13
|
+
## 25.1.5
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 6f20bf5: normalize-submit: prefer the explicit `nullable` field flag served by the kernel (v0.77.1+) when deciding to null an empty reference, falling back to the type-based heuristic for older hosts that don't serve the flag.
|
|
18
|
+
|
|
3
19
|
## 25.1.4
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
|
@@ -7,10 +7,23 @@ type RefFieldMeta = Record<string, any>;
|
|
|
7
7
|
* (e.g. products.category_id, a nullable `ref`) rejects a non-null value that
|
|
8
8
|
* points at no row: sending `""`/nil-UUID triggers
|
|
9
9
|
* insert ... violates foreign key constraint "fk_products_category" (23503)
|
|
10
|
-
* even though the user left the picker empty.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* even though the user left the picker empty.
|
|
11
|
+
*
|
|
12
|
+
* Nullability decision (Fase 4): prefer the EXPLICIT `nullable` flag the kernel
|
|
13
|
+
* (v0.77.1+) now serves per field (`modelbase.FieldDef.Nullable`, `!Required`).
|
|
14
|
+
* When `field.nullable === true` an empty value is submitted as `null`; when
|
|
15
|
+
* `field.nullable === false` the value is respected. When the flag is absent
|
|
16
|
+
* (undefined — older hosts that don't serve it yet) we fall back to the legacy
|
|
17
|
+
* type-based heuristic (`getFieldRef` / dynamic_select / search). This keeps
|
|
18
|
+
* backward-compat while letting the contract, not a guess, drive the decision.
|
|
19
|
+
*
|
|
20
|
+
* The explicit flag also lets us null an empty OPTIONAL ref whose column is NOT
|
|
21
|
+
* a uuid (which the uuid-only heuristic never covered). To avoid nulling a plain
|
|
22
|
+
* optional text field — where `""` is a legitimate value, not "no relation" — we
|
|
23
|
+
* gate the explicit path on the field ALSO looking like a reference/relation
|
|
24
|
+
* (`getFieldRef` / dynamic_select / search). A nullable non-ref scalar keeps its
|
|
25
|
+
* `""`. This is generic — every addon's optional relations benefit. Does not
|
|
26
|
+
* mutate `values`.
|
|
14
27
|
*/
|
|
15
28
|
export declare function normalizeRefFieldsForSubmit(values: Record<string, any>, fields: RefFieldMeta[] | undefined): Record<string, any>;
|
|
16
29
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize-submit.d.ts","sourceRoot":"","sources":["../../src/dialogs/normalize-submit.ts"],"names":[],"mappings":"AAOA;wEACwE;AACxE,KAAK,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAEvC
|
|
1
|
+
{"version":3,"file":"normalize-submit.d.ts","sourceRoot":"","sources":["../../src/dialogs/normalize-submit.ts"],"names":[],"mappings":"AAOA;wEACwE;AACxE,KAAK,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAEvC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,2BAA2B,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,YAAY,EAAE,GAAG,SAAS,GACnC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAuCrB"}
|
|
@@ -9,18 +9,35 @@ import { getFieldRef } from '../dynamic-form-schema';
|
|
|
9
9
|
* (e.g. products.category_id, a nullable `ref`) rejects a non-null value that
|
|
10
10
|
* points at no row: sending `""`/nil-UUID triggers
|
|
11
11
|
* insert ... violates foreign key constraint "fk_products_category" (23503)
|
|
12
|
-
* even though the user left the picker empty.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* even though the user left the picker empty.
|
|
13
|
+
*
|
|
14
|
+
* Nullability decision (Fase 4): prefer the EXPLICIT `nullable` flag the kernel
|
|
15
|
+
* (v0.77.1+) now serves per field (`modelbase.FieldDef.Nullable`, `!Required`).
|
|
16
|
+
* When `field.nullable === true` an empty value is submitted as `null`; when
|
|
17
|
+
* `field.nullable === false` the value is respected. When the flag is absent
|
|
18
|
+
* (undefined — older hosts that don't serve it yet) we fall back to the legacy
|
|
19
|
+
* type-based heuristic (`getFieldRef` / dynamic_select / search). This keeps
|
|
20
|
+
* backward-compat while letting the contract, not a guess, drive the decision.
|
|
21
|
+
*
|
|
22
|
+
* The explicit flag also lets us null an empty OPTIONAL ref whose column is NOT
|
|
23
|
+
* a uuid (which the uuid-only heuristic never covered). To avoid nulling a plain
|
|
24
|
+
* optional text field — where `""` is a legitimate value, not "no relation" — we
|
|
25
|
+
* gate the explicit path on the field ALSO looking like a reference/relation
|
|
26
|
+
* (`getFieldRef` / dynamic_select / search). A nullable non-ref scalar keeps its
|
|
27
|
+
* `""`. This is generic — every addon's optional relations benefit. Does not
|
|
28
|
+
* mutate `values`.
|
|
16
29
|
*/
|
|
17
30
|
export function normalizeRefFieldsForSubmit(values, fields) {
|
|
18
|
-
const
|
|
19
|
-
.filter((f) => !!getFieldRef(f) ||
|
|
31
|
+
const isRefField = (f) => !!getFieldRef(f) ||
|
|
20
32
|
f.type === 'dynamic_select' ||
|
|
21
33
|
f.widget === 'dynamic_select' ||
|
|
22
|
-
f.type === 'search'
|
|
23
|
-
|
|
34
|
+
f.type === 'search';
|
|
35
|
+
// Index fields by key so we can consult the explicit `nullable` flag.
|
|
36
|
+
const fieldByKey = new Map();
|
|
37
|
+
for (const f of fields ?? []) {
|
|
38
|
+
if (f && typeof f.key === 'string')
|
|
39
|
+
fieldByKey.set(f.key, f);
|
|
40
|
+
}
|
|
24
41
|
const out = { ...values };
|
|
25
42
|
for (const [k, v] of Object.entries(out)) {
|
|
26
43
|
// A nil UUID is never a real target row — null it out on any field.
|
|
@@ -28,8 +45,24 @@ export function normalizeRefFieldsForSubmit(values, fields) {
|
|
|
28
45
|
out[k] = null;
|
|
29
46
|
continue;
|
|
30
47
|
}
|
|
31
|
-
|
|
32
|
-
if (
|
|
48
|
+
const isEmpty = v === '' || v == null;
|
|
49
|
+
if (!isEmpty)
|
|
50
|
+
continue;
|
|
51
|
+
const f = fieldByKey.get(k);
|
|
52
|
+
const nullable = f?.nullable;
|
|
53
|
+
if (nullable === true) {
|
|
54
|
+
// Explicit contract flag wins. Only null a reference/relation so a
|
|
55
|
+
// legitimately-empty optional plain-text field keeps its `""`.
|
|
56
|
+
if (f && isRefField(f))
|
|
57
|
+
out[k] = null;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (nullable === false) {
|
|
61
|
+
// Explicitly non-nullable → respect the empty value as-is.
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// No explicit flag (older host): fall back to the type-based heuristic.
|
|
65
|
+
if (f && isRefField(f))
|
|
33
66
|
out[k] = null;
|
|
34
67
|
}
|
|
35
68
|
return out;
|
|
@@ -31,6 +31,15 @@ interface CommonProps {
|
|
|
31
31
|
canCreate?: boolean;
|
|
32
32
|
canDelete?: boolean;
|
|
33
33
|
canEdit?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Relación de solo lectura. Cuando es true fuerza canCreate/canEdit/canDelete
|
|
36
|
+
* = false (AND con lo que pase el host: readonly siempre gana), escondiendo el
|
|
37
|
+
* botón "Agregar", el ícono editar (Pencil) y el de eliminar (Trash2). Tolera
|
|
38
|
+
* el alias camelCase `readOnly`.
|
|
39
|
+
*/
|
|
40
|
+
readonly?: boolean;
|
|
41
|
+
/** Alias camelCase de `readonly`. */
|
|
42
|
+
readOnly?: boolean;
|
|
34
43
|
/** Strings traducibles. */
|
|
35
44
|
strings?: Partial<DynamicRelationStrings>;
|
|
36
45
|
/** Wrapper className. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AA+DA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,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;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,+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,+BAK1D"}
|
|
1
|
+
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AA+DA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,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;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,+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;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,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,+BAK1D"}
|
package/dist/dynamic-relation.js
CHANGED
|
@@ -39,7 +39,15 @@ export function DynamicRelation(props) {
|
|
|
39
39
|
}
|
|
40
40
|
return _jsx(OneToManyRelation, { ...props });
|
|
41
41
|
}
|
|
42
|
-
function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoint, hiddenColumns = [], canCreate = true, canDelete = true, canEdit = true, strings, className, onChange, }) {
|
|
42
|
+
function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoint, hiddenColumns = [], canCreate = true, canDelete = true, canEdit = true, readonly, readOnly, strings, className, onChange, }) {
|
|
43
|
+
// Read-only relation always wins over host-passed perms (AND). Tolerates the
|
|
44
|
+
// camelCase alias the same way lock_rows/lockRows does.
|
|
45
|
+
const isReadonly = readonly === true || readOnly === true;
|
|
46
|
+
if (isReadonly) {
|
|
47
|
+
canCreate = false;
|
|
48
|
+
canDelete = false;
|
|
49
|
+
canEdit = false;
|
|
50
|
+
}
|
|
43
51
|
const api = useApi();
|
|
44
52
|
const getImageUrl = useImageUrl();
|
|
45
53
|
const timeZone = useTimeZone();
|
|
@@ -187,7 +195,12 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
|
|
|
187
195
|
}) }, headerGroup.id))) }), _jsx(TableBody, { children: table.getRowModel().rows.map((row, idx) => (_jsx(TableRow, { children: row.getVisibleCells().map((cell) => (_jsx(TableCell, { style: cell.column.columnDef.size ? { width: cell.column.columnDef.size } : undefined, className: "py-2", children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, relationRowKey(row.original, idx, foreignKey)))) })] }) })), _jsx(Dialog, { open: formOpen, onOpenChange: (open) => { setFormOpen(open); if (!open)
|
|
188
196
|
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 })] })] }) })] }));
|
|
189
197
|
}
|
|
190
|
-
function ManyToManyRelation({ kind, through, references, foreignKey, referencesKey, parentId, filters, pivotEndpoint, referencesEndpoint, displayKey, canCreate = true, canDelete = true, strings, className, onChange, }) {
|
|
198
|
+
function ManyToManyRelation({ kind, through, references, foreignKey, referencesKey, parentId, filters, pivotEndpoint, referencesEndpoint, displayKey, canCreate = true, canDelete = true, readonly, readOnly, strings, className, onChange, }) {
|
|
199
|
+
// Read-only relation always wins over host-passed perms (AND).
|
|
200
|
+
if (readonly === true || readOnly === true) {
|
|
201
|
+
canCreate = false;
|
|
202
|
+
canDelete = false;
|
|
203
|
+
}
|
|
191
204
|
const api = useApi();
|
|
192
205
|
const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache();
|
|
193
206
|
const labels = { ...DEFAULT_STRINGS, ...(strings || {}) };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-relations.d.ts","sourceRoot":"","sources":["../src/dynamic-relations.tsx"],"names":[],"mappings":"AAaA,OAAO,EAAmB,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,MAAM,WAAW,qBAAqB;IAClC;;;OAGG;IACH,MAAM,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,CAAA;IACzE,gEAAgE;IAChE,SAAS,EAAE,YAAY,EAAE,GAAG,IAAI,GAAG,SAAS,CAAA;IAC5C;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;CAC9C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,MAAM,EAAE;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACnD,WAAW,SAAO,GACnB,MAAM,GAAG,MAAM,GAAG,SAAS,CAM7B;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAChC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,GAAG,OAAO,CAAC,EACrD,QAAQ,EAAE,MAAM,GAAG,MAAM,GAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB;AAOD,wBAAgB,gBAAgB,CAAC,EAC7B,MAAM,EACN,SAAS,EACT,WAAkB,EAClB,SAAS,EACT,cAAc,EACd,SAAgB,EAChB,SAAgB,EAChB,OAAc,EACd,OAAO,EACP,QAAQ,GACX,EAAE,qBAAqB,
|
|
1
|
+
{"version":3,"file":"dynamic-relations.d.ts","sourceRoot":"","sources":["../src/dynamic-relations.tsx"],"names":[],"mappings":"AAaA,OAAO,EAAmB,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,MAAM,WAAW,qBAAqB;IAClC;;;OAGG;IACH,MAAM,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,CAAA;IACzE,gEAAgE;IAChE,SAAS,EAAE,YAAY,EAAE,GAAG,IAAI,GAAG,SAAS,CAAA;IAC5C;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;CAC9C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,MAAM,EAAE;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACnD,WAAW,SAAO,GACnB,MAAM,GAAG,MAAM,GAAG,SAAS,CAM7B;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAChC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,GAAG,OAAO,CAAC,EACrD,QAAQ,EAAE,MAAM,GAAG,MAAM,GAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB;AAOD,wBAAgB,gBAAgB,CAAC,EAC7B,MAAM,EACN,SAAS,EACT,WAAkB,EAClB,SAAS,EACT,cAAc,EACd,SAAgB,EAChB,SAAgB,EAChB,OAAc,EACd,OAAO,EACP,QAAQ,GACX,EAAE,qBAAqB,sCAkEvB"}
|
|
@@ -62,6 +62,10 @@ export function DynamicRelations({ record, relations, parentIdKey = 'id', classN
|
|
|
62
62
|
...(strings || {}),
|
|
63
63
|
...(rel.label ? { title: rel.label } : {}),
|
|
64
64
|
};
|
|
65
|
+
// A relation flagged read-only in the kernel metadata forces the
|
|
66
|
+
// panel's mutation controls off regardless of the host perms.
|
|
67
|
+
// Tolerates the camelCase alias.
|
|
68
|
+
const relReadonly = rel.readonly === true || rel.readOnly === true;
|
|
65
69
|
if (rel.kind === 'many_to_many') {
|
|
66
70
|
return (_jsx(DynamicRelation, { kind: "many_to_many", through: rel.through,
|
|
67
71
|
// The pivot's reference table is unknown from
|
|
@@ -69,8 +73,8 @@ export function DynamicRelations({ record, relations, parentIdKey = 'id', classN
|
|
|
69
73
|
// the panel degrades to the pivot list. Hosts with a
|
|
70
74
|
// declared `references` should render DynamicRelation
|
|
71
75
|
// directly. (one_to_many is the common detail-page case.)
|
|
72
|
-
references: rel.through, foreignKey: rel.foreign_key, parentId: parentId, filters: filters, className: panelClassName, canCreate: canCreate, canDelete: canDelete, strings: panelStrings, onChange: onChange ? () => onChange(rel) : undefined }, relationKey(rel, idx)));
|
|
76
|
+
references: rel.through, foreignKey: rel.foreign_key, parentId: parentId, filters: filters, className: panelClassName, canCreate: canCreate, canDelete: canDelete, readonly: relReadonly, strings: panelStrings, onChange: onChange ? () => onChange(rel) : undefined }, relationKey(rel, idx)));
|
|
73
77
|
}
|
|
74
|
-
return (_jsx(DynamicRelation, { kind: "one_to_many", model: rel.through, foreignKey: rel.foreign_key, parentId: parentId, filters: filters, className: panelClassName, canCreate: canCreate, canDelete: canDelete, canEdit: canEdit, strings: panelStrings, onChange: onChange ? () => onChange(rel) : undefined }, relationKey(rel, idx)));
|
|
78
|
+
return (_jsx(DynamicRelation, { kind: "one_to_many", model: rel.through, foreignKey: rel.foreign_key, parentId: parentId, filters: filters, className: panelClassName, canCreate: canCreate, canDelete: canDelete, canEdit: canEdit, readonly: relReadonly, strings: panelStrings, onChange: onChange ? () => onChange(rel) : undefined }, relationKey(rel, idx)));
|
|
75
79
|
}) }));
|
|
76
80
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -158,6 +158,16 @@ export interface RelationMeta {
|
|
|
158
158
|
scope?: Record<string, string>;
|
|
159
159
|
/** Human-readable panel header. */
|
|
160
160
|
label?: string;
|
|
161
|
+
/**
|
|
162
|
+
* Read-only relation: when true the SDK forces canCreate/canEdit/canDelete
|
|
163
|
+
* = false on the rendered `<DynamicRelation>` panel, hiding the "Agregar"
|
|
164
|
+
* button and the per-row edit (Pencil) / delete (Trash2) controls — no
|
|
165
|
+
* matter what perms the host passes. Generic & domain-agnostic. Read from
|
|
166
|
+
* the kernel relation metadata. Tolerates the camelCase alias.
|
|
167
|
+
*/
|
|
168
|
+
readonly?: boolean;
|
|
169
|
+
/** camelCase alias for `readonly`. */
|
|
170
|
+
readOnly?: boolean;
|
|
161
171
|
}
|
|
162
172
|
export interface FilterDefinition {
|
|
163
173
|
key: string;
|
|
@@ -324,6 +334,14 @@ export interface ActionFieldDef {
|
|
|
324
334
|
label: string;
|
|
325
335
|
type: string;
|
|
326
336
|
required?: boolean;
|
|
337
|
+
/**
|
|
338
|
+
* Explicit nullability flag served by the kernel (v0.77.1+) from
|
|
339
|
+
* `modelbase.FieldDef.Nullable` (populated as `!Required`). An optional `ref`
|
|
340
|
+
* arrives as `nullable: true`. When present it authoritatively decides whether
|
|
341
|
+
* an empty value should be submitted as `null`; when absent (older hosts) the
|
|
342
|
+
* SDK falls back to type-based heuristics. Additive / optional.
|
|
343
|
+
*/
|
|
344
|
+
nullable?: boolean;
|
|
327
345
|
options?: OptionDef[];
|
|
328
346
|
defaultValue?: any;
|
|
329
347
|
placeholder?: 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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// `readonly` on a relation fixes the panel read-only: the SDK forces
|
|
4
|
+
// canCreate/canEdit/canDelete = false regardless of the perms the host passes,
|
|
5
|
+
// hiding the "Agregar" (Plus) button and the per-row edit (Pencil) / delete
|
|
6
|
+
// (Trash2) controls. Generic framework primitive — the flag rides on the
|
|
7
|
+
// kernel relation metadata (RelationMeta.readonly, camelCase alias readOnly).
|
|
8
|
+
//
|
|
9
|
+
// The one_to_many list renders plain metadata-driven cells (no Radix Select),
|
|
10
|
+
// so the happy-dom BubbleSelect value-reset gotcha does not apply here.
|
|
11
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
12
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
13
|
+
import { DynamicRelation } from '../dynamic-relation'
|
|
14
|
+
import { ApiProvider, type ApiClient } from '../api-context'
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup)
|
|
17
|
+
|
|
18
|
+
const META = {
|
|
19
|
+
name: 'line_item',
|
|
20
|
+
columns: [
|
|
21
|
+
{ key: 'id', label: 'ID', type: 'text', sortable: true, filterable: false, hidden: true },
|
|
22
|
+
{ key: 'invoice_id', label: 'Factura', type: 'text', sortable: false, filterable: false },
|
|
23
|
+
{ key: 'sku', label: 'SKU', type: 'text', sortable: true, filterable: true },
|
|
24
|
+
{ key: 'qty', label: 'Cantidad', type: 'number', sortable: true, filterable: false },
|
|
25
|
+
],
|
|
26
|
+
actions: [],
|
|
27
|
+
hasActions: false,
|
|
28
|
+
enableCRUDActions: false,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DATA = [
|
|
32
|
+
{ id: 'li_1', invoice_id: 'inv_42', sku: 'A-1', qty: 2 },
|
|
33
|
+
{ id: 'li_2', invoice_id: 'inv_42', sku: 'B-2', qty: 5 },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
function mockApi(): ApiClient {
|
|
37
|
+
return {
|
|
38
|
+
get: vi.fn((url: string) => {
|
|
39
|
+
if (url.startsWith('/metadata/table/')) {
|
|
40
|
+
return Promise.resolve({ data: { success: true, data: META } })
|
|
41
|
+
}
|
|
42
|
+
return Promise.resolve({ data: { success: true, data: DATA } })
|
|
43
|
+
}),
|
|
44
|
+
post: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })),
|
|
45
|
+
put: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })),
|
|
46
|
+
delete: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderPanel(extra: Record<string, unknown>) {
|
|
51
|
+
return render(
|
|
52
|
+
<ApiProvider client={mockApi()}>
|
|
53
|
+
<DynamicRelation
|
|
54
|
+
kind="one_to_many"
|
|
55
|
+
model="line_item"
|
|
56
|
+
foreignKey="invoice_id"
|
|
57
|
+
parentId="inv_42"
|
|
58
|
+
{...extra}
|
|
59
|
+
/>
|
|
60
|
+
</ApiProvider>,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('DynamicRelation — readonly', () => {
|
|
65
|
+
it('con readonly=true esconde "Agregar", Pencil y Trash2 aunque el host pase canCreate/canEdit/canDelete=true', async () => {
|
|
66
|
+
renderPanel({ readonly: true, canCreate: true, canEdit: true, canDelete: true })
|
|
67
|
+
|
|
68
|
+
// Rows loaded → cells rendered.
|
|
69
|
+
await waitFor(() => expect(screen.getByText('A-1')).toBeTruthy())
|
|
70
|
+
|
|
71
|
+
// No add control, no per-row edit/delete controls.
|
|
72
|
+
expect(screen.queryByText('Agregar')).toBeNull()
|
|
73
|
+
expect(screen.queryByLabelText('Editar')).toBeNull()
|
|
74
|
+
expect(screen.queryByLabelText('Quitar')).toBeNull()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('acepta el alias camelCase readOnly', async () => {
|
|
78
|
+
renderPanel({ readOnly: true })
|
|
79
|
+
await waitFor(() => expect(screen.getByText('A-1')).toBeTruthy())
|
|
80
|
+
expect(screen.queryByText('Agregar')).toBeNull()
|
|
81
|
+
expect(screen.queryByLabelText('Editar')).toBeNull()
|
|
82
|
+
expect(screen.queryByLabelText('Quitar')).toBeNull()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('control: sin readonly muestra "Agregar", Pencil y Trash2', async () => {
|
|
86
|
+
renderPanel({})
|
|
87
|
+
await waitFor(() => expect(screen.getByText('A-1')).toBeTruthy())
|
|
88
|
+
expect(screen.getByText('Agregar')).toBeTruthy()
|
|
89
|
+
expect(screen.getAllByLabelText('Editar')).toHaveLength(2)
|
|
90
|
+
expect(screen.getAllByLabelText('Quitar')).toHaveLength(2)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -60,4 +60,49 @@ describe('normalizeRefFieldsForSubmit', () => {
|
|
|
60
60
|
normalizeRefFieldsForSubmit(input, fields)
|
|
61
61
|
expect(input.category_id).toBe('')
|
|
62
62
|
})
|
|
63
|
+
|
|
64
|
+
// --- Fase 4: explicit `nullable` flag from the kernel contract ---
|
|
65
|
+
|
|
66
|
+
it('nulls an empty ref when the explicit nullable:true flag is present', () => {
|
|
67
|
+
const out = normalizeRefFieldsForSubmit(
|
|
68
|
+
{ supplier_id: '' },
|
|
69
|
+
[{ key: 'supplier_id', ref: 'suppliers', nullable: true }],
|
|
70
|
+
)
|
|
71
|
+
expect(out.supplier_id).toBeNull()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('respects an empty value when nullable:false (does not null)', () => {
|
|
75
|
+
const out = normalizeRefFieldsForSubmit(
|
|
76
|
+
{ supplier_id: '' },
|
|
77
|
+
[{ key: 'supplier_id', ref: 'suppliers', nullable: false }],
|
|
78
|
+
)
|
|
79
|
+
expect(out.supplier_id).toBe('')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('nulls an empty NON-uuid optional ref via explicit nullable:true', () => {
|
|
83
|
+
// A ref over a non-uuid column: the uuid-only heuristic never covered
|
|
84
|
+
// this, but the explicit flag + ref-shape does.
|
|
85
|
+
const out = normalizeRefFieldsForSubmit(
|
|
86
|
+
{ warehouse_code: '' },
|
|
87
|
+
[{ key: 'warehouse_code', type: 'dynamic_select', nullable: true }],
|
|
88
|
+
)
|
|
89
|
+
expect(out.warehouse_code).toBeNull()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('does NOT null a nullable:true plain-text field (empty string stays)', () => {
|
|
93
|
+
const out = normalizeRefFieldsForSubmit(
|
|
94
|
+
{ note: '' },
|
|
95
|
+
[{ key: 'note', type: 'text', nullable: true }],
|
|
96
|
+
)
|
|
97
|
+
expect(out.note).toBe('')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('falls back to the heuristic when the flag is undefined (older host)', () => {
|
|
101
|
+
const out = normalizeRefFieldsForSubmit(
|
|
102
|
+
{ category_id: '', name: '' },
|
|
103
|
+
fields, // fields carry no `nullable` flag
|
|
104
|
+
)
|
|
105
|
+
expect(out.category_id).toBeNull() // ref → heuristic nulls it
|
|
106
|
+
expect(out.name).toBe('') // plain text stays
|
|
107
|
+
})
|
|
63
108
|
})
|
|
@@ -15,26 +15,40 @@ type RefFieldMeta = Record<string, any>
|
|
|
15
15
|
* (e.g. products.category_id, a nullable `ref`) rejects a non-null value that
|
|
16
16
|
* points at no row: sending `""`/nil-UUID triggers
|
|
17
17
|
* insert ... violates foreign key constraint "fk_products_category" (23503)
|
|
18
|
-
* even though the user left the picker empty.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* even though the user left the picker empty.
|
|
19
|
+
*
|
|
20
|
+
* Nullability decision (Fase 4): prefer the EXPLICIT `nullable` flag the kernel
|
|
21
|
+
* (v0.77.1+) now serves per field (`modelbase.FieldDef.Nullable`, `!Required`).
|
|
22
|
+
* When `field.nullable === true` an empty value is submitted as `null`; when
|
|
23
|
+
* `field.nullable === false` the value is respected. When the flag is absent
|
|
24
|
+
* (undefined — older hosts that don't serve it yet) we fall back to the legacy
|
|
25
|
+
* type-based heuristic (`getFieldRef` / dynamic_select / search). This keeps
|
|
26
|
+
* backward-compat while letting the contract, not a guess, drive the decision.
|
|
27
|
+
*
|
|
28
|
+
* The explicit flag also lets us null an empty OPTIONAL ref whose column is NOT
|
|
29
|
+
* a uuid (which the uuid-only heuristic never covered). To avoid nulling a plain
|
|
30
|
+
* optional text field — where `""` is a legitimate value, not "no relation" — we
|
|
31
|
+
* gate the explicit path on the field ALSO looking like a reference/relation
|
|
32
|
+
* (`getFieldRef` / dynamic_select / search). A nullable non-ref scalar keeps its
|
|
33
|
+
* `""`. This is generic — every addon's optional relations benefit. Does not
|
|
34
|
+
* mutate `values`.
|
|
22
35
|
*/
|
|
23
36
|
export function normalizeRefFieldsForSubmit(
|
|
24
37
|
values: Record<string, any>,
|
|
25
38
|
fields: RefFieldMeta[] | undefined,
|
|
26
39
|
): Record<string, any> {
|
|
27
|
-
const
|
|
28
|
-
(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
const isRefField = (f: RefFieldMeta): boolean =>
|
|
41
|
+
!!getFieldRef(f as unknown as ActionFieldDef) ||
|
|
42
|
+
f.type === 'dynamic_select' ||
|
|
43
|
+
f.widget === 'dynamic_select' ||
|
|
44
|
+
f.type === 'search'
|
|
45
|
+
|
|
46
|
+
// Index fields by key so we can consult the explicit `nullable` flag.
|
|
47
|
+
const fieldByKey = new Map<string, RefFieldMeta>()
|
|
48
|
+
for (const f of fields ?? []) {
|
|
49
|
+
if (f && typeof f.key === 'string') fieldByKey.set(f.key, f)
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
const out: Record<string, any> = { ...values }
|
|
39
53
|
for (const [k, v] of Object.entries(out)) {
|
|
40
54
|
// A nil UUID is never a real target row — null it out on any field.
|
|
@@ -42,8 +56,23 @@ export function normalizeRefFieldsForSubmit(
|
|
|
42
56
|
out[k] = null
|
|
43
57
|
continue
|
|
44
58
|
}
|
|
45
|
-
|
|
46
|
-
if (
|
|
59
|
+
const isEmpty = v === '' || v == null
|
|
60
|
+
if (!isEmpty) continue
|
|
61
|
+
|
|
62
|
+
const f = fieldByKey.get(k)
|
|
63
|
+
const nullable = f?.nullable
|
|
64
|
+
if (nullable === true) {
|
|
65
|
+
// Explicit contract flag wins. Only null a reference/relation so a
|
|
66
|
+
// legitimately-empty optional plain-text field keeps its `""`.
|
|
67
|
+
if (f && isRefField(f)) out[k] = null
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
if (nullable === false) {
|
|
71
|
+
// Explicitly non-nullable → respect the empty value as-is.
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
// No explicit flag (older host): fall back to the type-based heuristic.
|
|
75
|
+
if (f && isRefField(f)) out[k] = null
|
|
47
76
|
}
|
|
48
77
|
return out
|
|
49
78
|
}
|
package/src/dynamic-relation.tsx
CHANGED
|
@@ -123,6 +123,15 @@ interface CommonProps {
|
|
|
123
123
|
canCreate?: boolean
|
|
124
124
|
canDelete?: boolean
|
|
125
125
|
canEdit?: boolean
|
|
126
|
+
/**
|
|
127
|
+
* Relación de solo lectura. Cuando es true fuerza canCreate/canEdit/canDelete
|
|
128
|
+
* = false (AND con lo que pase el host: readonly siempre gana), escondiendo el
|
|
129
|
+
* botón "Agregar", el ícono editar (Pencil) y el de eliminar (Trash2). Tolera
|
|
130
|
+
* el alias camelCase `readOnly`.
|
|
131
|
+
*/
|
|
132
|
+
readonly?: boolean
|
|
133
|
+
/** Alias camelCase de `readonly`. */
|
|
134
|
+
readOnly?: boolean
|
|
126
135
|
/** Strings traducibles. */
|
|
127
136
|
strings?: Partial<DynamicRelationStrings>
|
|
128
137
|
/** Wrapper className. */
|
|
@@ -184,10 +193,20 @@ function OneToManyRelation({
|
|
|
184
193
|
canCreate = true,
|
|
185
194
|
canDelete = true,
|
|
186
195
|
canEdit = true,
|
|
196
|
+
readonly,
|
|
197
|
+
readOnly,
|
|
187
198
|
strings,
|
|
188
199
|
className,
|
|
189
200
|
onChange,
|
|
190
201
|
}: DynamicRelationOneToManyProps) {
|
|
202
|
+
// Read-only relation always wins over host-passed perms (AND). Tolerates the
|
|
203
|
+
// camelCase alias the same way lock_rows/lockRows does.
|
|
204
|
+
const isReadonly = readonly === true || readOnly === true
|
|
205
|
+
if (isReadonly) {
|
|
206
|
+
canCreate = false
|
|
207
|
+
canDelete = false
|
|
208
|
+
canEdit = false
|
|
209
|
+
}
|
|
191
210
|
const api = useApi()
|
|
192
211
|
const getImageUrl = useImageUrl()
|
|
193
212
|
const timeZone = useTimeZone()
|
|
@@ -490,10 +509,17 @@ function ManyToManyRelation({
|
|
|
490
509
|
displayKey,
|
|
491
510
|
canCreate = true,
|
|
492
511
|
canDelete = true,
|
|
512
|
+
readonly,
|
|
513
|
+
readOnly,
|
|
493
514
|
strings,
|
|
494
515
|
className,
|
|
495
516
|
onChange,
|
|
496
517
|
}: DynamicRelationManyToManyProps) {
|
|
518
|
+
// Read-only relation always wins over host-passed perms (AND).
|
|
519
|
+
if (readonly === true || readOnly === true) {
|
|
520
|
+
canCreate = false
|
|
521
|
+
canDelete = false
|
|
522
|
+
}
|
|
497
523
|
const api = useApi()
|
|
498
524
|
const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache()
|
|
499
525
|
const labels = { ...DEFAULT_STRINGS, ...(strings || {}) }
|
|
@@ -115,6 +115,10 @@ export function DynamicRelations({
|
|
|
115
115
|
...(strings || {}),
|
|
116
116
|
...(rel.label ? { title: rel.label } : {}),
|
|
117
117
|
}
|
|
118
|
+
// A relation flagged read-only in the kernel metadata forces the
|
|
119
|
+
// panel's mutation controls off regardless of the host perms.
|
|
120
|
+
// Tolerates the camelCase alias.
|
|
121
|
+
const relReadonly = rel.readonly === true || rel.readOnly === true
|
|
118
122
|
if (rel.kind === 'many_to_many') {
|
|
119
123
|
return (
|
|
120
124
|
<DynamicRelation
|
|
@@ -133,6 +137,7 @@ export function DynamicRelations({
|
|
|
133
137
|
className={panelClassName}
|
|
134
138
|
canCreate={canCreate}
|
|
135
139
|
canDelete={canDelete}
|
|
140
|
+
readonly={relReadonly}
|
|
136
141
|
strings={panelStrings}
|
|
137
142
|
onChange={onChange ? () => onChange(rel) : undefined}
|
|
138
143
|
/>
|
|
@@ -150,6 +155,7 @@ export function DynamicRelations({
|
|
|
150
155
|
canCreate={canCreate}
|
|
151
156
|
canDelete={canDelete}
|
|
152
157
|
canEdit={canEdit}
|
|
158
|
+
readonly={relReadonly}
|
|
153
159
|
strings={panelStrings}
|
|
154
160
|
onChange={onChange ? () => onChange(rel) : undefined}
|
|
155
161
|
/>
|
package/src/types.ts
CHANGED
|
@@ -153,6 +153,16 @@ export interface RelationMeta {
|
|
|
153
153
|
scope?: Record<string, string>
|
|
154
154
|
/** Human-readable panel header. */
|
|
155
155
|
label?: string
|
|
156
|
+
/**
|
|
157
|
+
* Read-only relation: when true the SDK forces canCreate/canEdit/canDelete
|
|
158
|
+
* = false on the rendered `<DynamicRelation>` panel, hiding the "Agregar"
|
|
159
|
+
* button and the per-row edit (Pencil) / delete (Trash2) controls — no
|
|
160
|
+
* matter what perms the host passes. Generic & domain-agnostic. Read from
|
|
161
|
+
* the kernel relation metadata. Tolerates the camelCase alias.
|
|
162
|
+
*/
|
|
163
|
+
readonly?: boolean
|
|
164
|
+
/** camelCase alias for `readonly`. */
|
|
165
|
+
readOnly?: boolean
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
export interface FilterDefinition {
|
|
@@ -374,6 +384,14 @@ export interface ActionFieldDef {
|
|
|
374
384
|
label: string
|
|
375
385
|
type: string
|
|
376
386
|
required?: boolean
|
|
387
|
+
/**
|
|
388
|
+
* Explicit nullability flag served by the kernel (v0.77.1+) from
|
|
389
|
+
* `modelbase.FieldDef.Nullable` (populated as `!Required`). An optional `ref`
|
|
390
|
+
* arrives as `nullable: true`. When present it authoritatively decides whether
|
|
391
|
+
* an empty value should be submitted as `null`; when absent (older hosts) the
|
|
392
|
+
* SDK falls back to type-based heuristics. Additive / optional.
|
|
393
|
+
*/
|
|
394
|
+
nullable?: boolean
|
|
377
395
|
options?: OptionDef[]
|
|
378
396
|
defaultValue?: any
|
|
379
397
|
placeholder?: string
|