@asteby/metacore-runtime-react 13.5.2 → 13.6.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  3. package/dist/action-modal-dispatcher.js +6 -0
  4. package/dist/dynamic-form-schema.d.ts +10 -0
  5. package/dist/dynamic-form-schema.d.ts.map +1 -1
  6. package/dist/dynamic-form-schema.js +21 -0
  7. package/dist/dynamic-form.d.ts +1 -0
  8. package/dist/dynamic-form.d.ts.map +1 -1
  9. package/dist/dynamic-form.js +7 -0
  10. package/dist/dynamic-relation-helpers.d.ts +1 -1
  11. package/dist/dynamic-relation-helpers.d.ts.map +1 -1
  12. package/dist/dynamic-relation-helpers.js +17 -2
  13. package/dist/dynamic-relation.d.ts +8 -0
  14. package/dist/dynamic-relation.d.ts.map +1 -1
  15. package/dist/dynamic-relation.js +26 -12
  16. package/dist/dynamic-relations.d.ts +51 -0
  17. package/dist/dynamic-relations.d.ts.map +1 -0
  18. package/dist/dynamic-relations.js +76 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/types.d.ts +57 -1
  23. package/dist/types.d.ts.map +1 -1
  24. package/dist/upload-field.d.ts +15 -0
  25. package/dist/upload-field.d.ts.map +1 -0
  26. package/dist/upload-field.js +109 -0
  27. package/package.json +3 -3
  28. package/src/__tests__/dynamic-relation.test.ts +28 -0
  29. package/src/__tests__/dynamic-relations.test.ts +60 -0
  30. package/src/__tests__/upload-field.test.ts +74 -0
  31. package/src/action-modal-dispatcher.tsx +6 -0
  32. package/src/dynamic-form-schema.ts +27 -0
  33. package/src/dynamic-form.tsx +7 -0
  34. package/src/dynamic-relation-helpers.ts +15 -1
  35. package/src/dynamic-relation.tsx +35 -10
  36. package/src/dynamic-relations.tsx +160 -0
  37. package/src/index.ts +6 -0
  38. package/src/types.ts +58 -0
  39. package/src/upload-field.tsx +168 -0
@@ -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;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEjF,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;;;;OAIG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,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;IAC3E;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;CAC/B;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;AASD,MAAM,WAAW,eAAe;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAID,MAAM,MAAM,WAAW,GACjB,MAAM,GACN,UAAU,GACV,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,gBAAgB,GAChB,QAAQ,CAAA;AAEd,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC5C,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,MAAM,CAAC,EAAE,WAAW,GAAG,MAAM,CAAA;IAC7B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,cAAc,EAAE,CAAA;IAC7B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAA;CAC7B;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sDAAsD;IACtD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;CACzC;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,CAAC,CAAA;IACP,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CAChB;AAKD,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;CACzC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { ActionFieldDef } from './types';
2
+ export interface UploadFieldProps {
3
+ field: ActionFieldDef;
4
+ value: any;
5
+ onChange: (v: any) => void;
6
+ }
7
+ /**
8
+ * Pulls the stored file url/path out of an upload response envelope, tolerating
9
+ * the common key shapes a host might return. Pure — exported for tests.
10
+ */
11
+ export declare function extractUploadedValue(payload: any): string;
12
+ /** Short, human display name for an already-stored file value (a url/path). */
13
+ export declare function uploadedDisplayName(value: unknown): string;
14
+ export declare function UploadField({ field, value, onChange }: UploadFieldProps): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=upload-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-field.d.ts","sourceRoot":"","sources":["../src/upload-field.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAKD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAWzD;AAED,+EAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAK1D;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,gBAAgB,2CAkHvE"}
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // UploadField — the `upload` widget renderer shared by DynamicForm's
3
+ // FieldRenderer and the action-modal-dispatcher's renderField so the two stay
4
+ // in lockstep. Renders a themed Button that proxies a hidden <input type=file>,
5
+ // POSTs the picked file to the host upload endpoint as multipart/form-data, and
6
+ // stores the returned file url/path as the field value.
7
+ //
8
+ // Endpoint assumption: `POST /uploads` (multipart) returning
9
+ // { success: true, data: { file_url?, url?, path?, file_path? } }
10
+ // matching the kernel envelope. A field may override the path via
11
+ // `field.searchEndpoint` (reused as the upload endpoint escape hatch) — kept
12
+ // generic so this carries no host-specific route. Honors field.accept /
13
+ // field.maxSize and forwards field.storagePath as `storage_path`.
14
+ import { useCallback, useRef, useState } from 'react';
15
+ import { Button } from '@asteby/metacore-ui/primitives';
16
+ import { Loader2, Paperclip, X } from 'lucide-react';
17
+ import { useApi } from './api-context';
18
+ import { getUploadConfig } from './dynamic-form-schema';
19
+ /** Default host upload endpoint. Overridable per-field via `searchEndpoint`. */
20
+ const DEFAULT_UPLOAD_ENDPOINT = '/uploads';
21
+ /**
22
+ * Pulls the stored file url/path out of an upload response envelope, tolerating
23
+ * the common key shapes a host might return. Pure — exported for tests.
24
+ */
25
+ export function extractUploadedValue(payload) {
26
+ if (payload === null || payload === undefined)
27
+ return '';
28
+ if (typeof payload === 'string')
29
+ return payload;
30
+ const d = (payload && typeof payload === 'object' && 'data' in payload ? payload.data : payload) ?? payload;
31
+ if (typeof d === 'string')
32
+ return d;
33
+ if (d && typeof d === 'object') {
34
+ const candidate = d.file_url ?? d.fileUrl ?? d.url ?? d.file_path ?? d.filePath ?? d.path;
35
+ if (typeof candidate === 'string')
36
+ return candidate;
37
+ }
38
+ return '';
39
+ }
40
+ /** Short, human display name for an already-stored file value (a url/path). */
41
+ export function uploadedDisplayName(value) {
42
+ if (typeof value !== 'string' || value === '')
43
+ return '';
44
+ const cleaned = value.split('?')[0];
45
+ const parts = cleaned.split('/');
46
+ return parts[parts.length - 1] || cleaned;
47
+ }
48
+ export function UploadField({ field, value, onChange }) {
49
+ const api = useApi();
50
+ const inputRef = useRef(null);
51
+ const [uploading, setUploading] = useState(false);
52
+ const [error, setError] = useState(null);
53
+ const { accept, maxSize, storagePath } = getUploadConfig(field);
54
+ const endpoint = field.searchEndpoint || DEFAULT_UPLOAD_ENDPOINT;
55
+ const handlePick = useCallback(() => {
56
+ if (uploading)
57
+ return;
58
+ inputRef.current?.click();
59
+ }, [uploading]);
60
+ const handleFile = useCallback(async (e) => {
61
+ const file = e.target.files?.[0];
62
+ // Reset the input so picking the same file again re-fires change.
63
+ if (inputRef.current)
64
+ inputRef.current.value = '';
65
+ if (!file)
66
+ return;
67
+ setError(null);
68
+ if (maxSize && file.size > maxSize) {
69
+ const mb = (maxSize / (1024 * 1024)).toFixed(1);
70
+ setError(`Archivo muy grande (máx. ${mb} MB).`);
71
+ return;
72
+ }
73
+ const form = new FormData();
74
+ form.append('file', file);
75
+ if (storagePath)
76
+ form.append('storage_path', storagePath);
77
+ setUploading(true);
78
+ try {
79
+ const res = await api.post(endpoint, form, {
80
+ headers: { 'Content-Type': 'multipart/form-data' },
81
+ });
82
+ const body = res?.data;
83
+ if (body && body.success === false) {
84
+ setError(body.message || 'No se pudo subir el archivo.');
85
+ return;
86
+ }
87
+ const stored = extractUploadedValue(body);
88
+ if (!stored) {
89
+ setError('Respuesta de subida inválida.');
90
+ return;
91
+ }
92
+ onChange(stored);
93
+ }
94
+ catch (err) {
95
+ setError(err?.response?.data?.message || 'No se pudo subir el archivo.');
96
+ }
97
+ finally {
98
+ setUploading(false);
99
+ }
100
+ }, [api, endpoint, maxSize, storagePath, onChange]);
101
+ const handleClear = useCallback(() => {
102
+ if (uploading)
103
+ return;
104
+ setError(null);
105
+ onChange('');
106
+ }, [uploading, onChange]);
107
+ const hasValue = typeof value === 'string' && value !== '';
108
+ return (_jsxs("div", { className: "grid gap-1.5", "data-widget": "upload", children: [_jsx("input", { ref: inputRef, id: field.key, type: "file", accept: accept, className: "sr-only", onChange: handleFile, tabIndex: -1, "aria-hidden": "true" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: handlePick, disabled: uploading, children: [uploading ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Paperclip, { className: "mr-2 h-4 w-4" })), hasValue ? 'Reemplazar' : field.placeholder || 'Subir archivo'] }), hasValue && !uploading && (_jsxs("div", { className: "flex min-w-0 items-center gap-1 text-sm text-muted-foreground", children: [_jsx("span", { className: "truncate", title: String(value), children: uploadedDisplayName(value) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: handleClear, "aria-label": "Quitar archivo", children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }))] }), error && (_jsx("span", { className: "text-sm text-destructive", role: "alert", children: error }))] }));
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "13.5.2",
3
+ "version": "13.6.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -61,8 +61,8 @@
61
61
  "typescript": "^6.0.0",
62
62
  "vitest": "^4.0.0",
63
63
  "zustand": "^5.0.0",
64
- "@asteby/metacore-ui": "2.1.0",
65
- "@asteby/metacore-sdk": "3.1.0"
64
+ "@asteby/metacore-sdk": "3.1.0",
65
+ "@asteby/metacore-ui": "2.1.0"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc -p tsconfig.json",
@@ -25,6 +25,34 @@ describe('buildRelationFilterParams', () => {
25
25
  })
26
26
  })
27
27
 
28
+ it('agrega filtros de scope extra como f_<col>=eq:<val> (caso polimórfico)', () => {
29
+ expect(
30
+ buildRelationFilterParams('owner_id', 'cust_1', { owner_model: 'Customer' }),
31
+ ).toEqual({
32
+ f_owner_id: 'eq:cust_1',
33
+ f_owner_model: 'eq:Customer',
34
+ })
35
+ })
36
+
37
+ it('el foreign-key gana sobre un scope redundante con el mismo key', () => {
38
+ expect(
39
+ buildRelationFilterParams('owner_id', 'cust_1', { owner_id: 'evil', tier: 'gold' }),
40
+ ).toEqual({
41
+ f_owner_id: 'eq:cust_1',
42
+ f_tier: 'eq:gold',
43
+ })
44
+ })
45
+
46
+ it('ignora scope null/undefined sin romper', () => {
47
+ expect(
48
+ // @ts-expect-error testing runtime tolerance
49
+ buildRelationFilterParams('owner_id', 'c1', { a: null, b: undefined, c: 'ok' }),
50
+ ).toEqual({ f_owner_id: 'eq:c1', f_c: 'eq:ok' })
51
+ expect(buildRelationFilterParams('owner_id', 'c1', null)).toEqual({
52
+ f_owner_id: 'eq:c1',
53
+ })
54
+ })
55
+
28
56
  it('rechaza foreignKey vacío', () => {
29
57
  expect(() => buildRelationFilterParams('', 'x')).toThrow(/foreignKey/)
30
58
  })
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { resolveParentId, buildRelationFilters } from '../dynamic-relations'
3
+ import type { RelationMeta } from '../types'
4
+
5
+ describe('resolveParentId', () => {
6
+ it('lee record.id por defecto', () => {
7
+ expect(resolveParentId({ id: 'c_1' })).toBe('c_1')
8
+ expect(resolveParentId({ id: 7 })).toBe(7)
9
+ })
10
+
11
+ it('respeta un parentIdKey custom', () => {
12
+ expect(resolveParentId({ uuid: 'u_1' }, 'uuid')).toBe('u_1')
13
+ })
14
+
15
+ it('devuelve undefined cuando falta / es vacío / no es escalar', () => {
16
+ expect(resolveParentId(null)).toBeUndefined()
17
+ expect(resolveParentId(undefined)).toBeUndefined()
18
+ expect(resolveParentId({})).toBeUndefined()
19
+ expect(resolveParentId({ id: '' })).toBeUndefined()
20
+ expect(resolveParentId({ id: null })).toBeUndefined()
21
+ expect(resolveParentId({ id: { nested: true } })).toBeUndefined()
22
+ })
23
+ })
24
+
25
+ describe('buildRelationFilters', () => {
26
+ it('mergea scope estático + foreign_key=parentId (caso polimórfico)', () => {
27
+ const rel: Pick<RelationMeta, 'foreign_key' | 'scope'> = {
28
+ foreign_key: 'owner_id',
29
+ scope: { owner_model: 'Customer' },
30
+ }
31
+ expect(buildRelationFilters(rel, 'c_1')).toEqual({
32
+ owner_model: 'Customer',
33
+ owner_id: 'c_1',
34
+ })
35
+ })
36
+
37
+ it('funciona sin scope (one_to_many simple)', () => {
38
+ expect(buildRelationFilters({ foreign_key: 'customer_id' }, 42)).toEqual({
39
+ customer_id: '42',
40
+ })
41
+ })
42
+
43
+ it('coerce parentId y valores de scope a string', () => {
44
+ const rel: Pick<RelationMeta, 'foreign_key' | 'scope'> = {
45
+ foreign_key: 'owner_id',
46
+ // @ts-expect-error testing runtime coercion of non-string scope value
47
+ scope: { kind: 5 },
48
+ }
49
+ expect(buildRelationFilters(rel, 9)).toEqual({ kind: '5', owner_id: '9' })
50
+ })
51
+
52
+ it('omite entradas de scope null/undefined', () => {
53
+ const rel: Pick<RelationMeta, 'foreign_key' | 'scope'> = {
54
+ foreign_key: 'owner_id',
55
+ // @ts-expect-error testing runtime tolerance
56
+ scope: { a: null, b: undefined, c: 'ok' },
57
+ }
58
+ expect(buildRelationFilters(rel, 'c_1')).toEqual({ c: 'ok', owner_id: 'c_1' })
59
+ })
60
+ })
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { extractUploadedValue, uploadedDisplayName } from '../upload-field'
3
+ import { resolveWidget, getUploadConfig } from '../dynamic-form-schema'
4
+ import type { ActionFieldDef } from '../types'
5
+
6
+ describe('resolveWidget upload', () => {
7
+ it('infiere upload desde type', () => {
8
+ expect(resolveWidget({ key: 'f', label: 'F', type: 'upload' })).toBe('upload')
9
+ })
10
+ it('respeta widget explícito upload sobre cualquier type', () => {
11
+ expect(resolveWidget({ key: 'f', label: 'F', type: 'string', widget: 'upload' })).toBe('upload')
12
+ })
13
+ })
14
+
15
+ describe('getUploadConfig', () => {
16
+ it('lee la forma camelCase autorada', () => {
17
+ const field: ActionFieldDef = {
18
+ key: 'logo', label: 'Logo', type: 'upload',
19
+ accept: 'image/*', maxSize: 1024, storagePath: 'brand/',
20
+ }
21
+ expect(getUploadConfig(field)).toEqual({ accept: 'image/*', maxSize: 1024, storagePath: 'brand/' })
22
+ })
23
+
24
+ it('tolera la forma snake_case del kernel', () => {
25
+ const field = {
26
+ key: 'doc', label: 'Doc', type: 'upload',
27
+ accept: '.pdf', max_size: 2048, storage_path: 'docs/',
28
+ } as ActionFieldDef
29
+ expect(getUploadConfig(field)).toEqual({ accept: '.pdf', maxSize: 2048, storagePath: 'docs/' })
30
+ })
31
+
32
+ it('descarta maxSize inválido / no positivo y campos vacíos', () => {
33
+ expect(getUploadConfig({ key: 'f', label: 'F', type: 'upload', maxSize: 0 })).toEqual({
34
+ accept: undefined, maxSize: undefined, storagePath: undefined,
35
+ })
36
+ expect(getUploadConfig({ key: 'f', label: 'F', type: 'upload', max_size: -5 } as ActionFieldDef).maxSize).toBeUndefined()
37
+ })
38
+ })
39
+
40
+ describe('extractUploadedValue', () => {
41
+ it('extrae del envelope {success,data:{file_url}}', () => {
42
+ expect(extractUploadedValue({ success: true, data: { file_url: '/u/a.png' } })).toBe('/u/a.png')
43
+ })
44
+ it('soporta url / path / file_path / camelCase', () => {
45
+ expect(extractUploadedValue({ data: { url: '/u/b' } })).toBe('/u/b')
46
+ expect(extractUploadedValue({ data: { path: '/u/c' } })).toBe('/u/c')
47
+ expect(extractUploadedValue({ data: { file_path: '/u/d' } })).toBe('/u/d')
48
+ expect(extractUploadedValue({ data: { fileUrl: '/u/e' } })).toBe('/u/e')
49
+ })
50
+ it('soporta respuesta data como string plano', () => {
51
+ expect(extractUploadedValue({ data: '/u/flat' })).toBe('/u/flat')
52
+ expect(extractUploadedValue('/u/raw')).toBe('/u/raw')
53
+ })
54
+ it('devuelve "" cuando no hay nada usable', () => {
55
+ expect(extractUploadedValue(null)).toBe('')
56
+ expect(extractUploadedValue(undefined)).toBe('')
57
+ expect(extractUploadedValue({ data: { foo: 'bar' } })).toBe('')
58
+ })
59
+ })
60
+
61
+ describe('uploadedDisplayName', () => {
62
+ it('toma el último segmento del path', () => {
63
+ expect(uploadedDisplayName('/uploads/2026/a.png')).toBe('a.png')
64
+ expect(uploadedDisplayName('a.png')).toBe('a.png')
65
+ })
66
+ it('descarta el querystring', () => {
67
+ expect(uploadedDisplayName('/u/a.png?sig=xyz')).toBe('a.png')
68
+ })
69
+ it('devuelve "" para valores no-string o vacíos', () => {
70
+ expect(uploadedDisplayName('')).toBe('')
71
+ expect(uploadedDisplayName(null)).toBe('')
72
+ expect(uploadedDisplayName(42)).toBe('')
73
+ })
74
+ })
@@ -41,6 +41,7 @@ import { DynamicIcon } from './dynamic-icon'
41
41
  import { DynamicLineItems } from './dynamic-line-items'
42
42
  import { DynamicSelectField } from './dynamic-select-field'
43
43
  import { DynamicDateField } from './dynamic-date-field'
44
+ import { UploadField } from './upload-field'
44
45
  import { isLineItemsField, resolveWidget } from './dynamic-form-schema'
45
46
  import type { ActionFieldDef } from './types'
46
47
  // Canonical registry lives in @asteby/metacore-sdk
@@ -322,6 +323,11 @@ function renderField(
322
323
  if (widget === 'dynamic_select') {
323
324
  return <DynamicSelectField field={field} value={value} onChange={onChange} />
324
325
  }
326
+ // File upload → themed picker that POSTs the file to the host upload
327
+ // endpoint and stores the returned url/path. Kept in sync with DynamicForm.
328
+ if (widget === 'upload') {
329
+ return <UploadField field={field} value={value} onChange={onChange} />
330
+ }
325
331
  switch (widget) {
326
332
  case 'textarea':
327
333
  return <Textarea id={field.key} value={value || ''} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)} placeholder={field.placeholder} />
@@ -216,6 +216,33 @@ export function resolveWidget(field: ActionFieldDef): string {
216
216
  case 'boolean': return 'switch'
217
217
  case 'number': return 'number'
218
218
  case 'date': return 'date'
219
+ // File upload: POSTs to the host upload endpoint and stores the returned
220
+ // file url/path as the field value. Rendered by `UploadField`.
221
+ case 'upload': return 'upload'
219
222
  default: return 'text'
220
223
  }
221
224
  }
225
+
226
+ /**
227
+ * Normalizes an upload field's config, tolerating both the camelCase authored
228
+ * SDK shape and the snake_case the kernel serves (`max_size`, `storage_path`).
229
+ * Pure — shared by both field renderers and unit tests.
230
+ */
231
+ export function getUploadConfig(field: ActionFieldDef): {
232
+ accept?: string
233
+ maxSize?: number
234
+ storagePath?: string
235
+ } {
236
+ const accept = field.accept
237
+ const maxSizeRaw = field.maxSize ?? field.max_size
238
+ const maxSize =
239
+ typeof maxSizeRaw === 'number' && Number.isFinite(maxSizeRaw) && maxSizeRaw > 0
240
+ ? maxSizeRaw
241
+ : undefined
242
+ const storagePath = field.storagePath ?? field.storage_path
243
+ return {
244
+ accept: accept || undefined,
245
+ maxSize,
246
+ storagePath: storagePath || undefined,
247
+ }
248
+ }
@@ -25,11 +25,13 @@ import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
25
25
  import { DynamicLineItems } from './dynamic-line-items'
26
26
  import { DynamicSelectField } from './dynamic-select-field'
27
27
  import { DynamicDateField } from './dynamic-date-field'
28
+ import { UploadField } from './upload-field'
28
29
 
29
30
  export { buildZodSchema, resolveWidget }
30
31
  export { DynamicLineItems } from './dynamic-line-items'
31
32
  export { DynamicSelectField } from './dynamic-select-field'
32
33
  export { DynamicDateField } from './dynamic-date-field'
34
+ export { UploadField } from './upload-field'
33
35
 
34
36
  export interface DynamicFormProps {
35
37
  fields: ActionFieldDef[]
@@ -168,6 +170,11 @@ function FieldRenderer({ field, value, onChange }: FieldRendererProps) {
168
170
  if (widget === 'dynamic_select') {
169
171
  return <DynamicSelectField field={field} value={value} onChange={onChange} />
170
172
  }
173
+ // File upload → themed picker that POSTs to the host upload endpoint and
174
+ // stores the returned file url/path as the field value.
175
+ if (widget === 'upload') {
176
+ return <UploadField field={field} value={value} onChange={onChange} />
177
+ }
171
178
  // Ref-driven select: hook into useOptionsResolver so the canonical
172
179
  // /api/options/<ref>?field=id endpoint feeds the dropdown. This is
173
180
  // the path the kernel auto-derives for FK columns; legacy callers
@@ -24,12 +24,26 @@ export interface TargetRowLike {
24
24
  export function buildRelationFilterParams(
25
25
  foreignKey: string,
26
26
  parentId: string | number,
27
+ extraFilters?: Record<string, string> | null,
27
28
  ): Record<string, string> {
28
29
  if (!foreignKey) throw new Error('foreignKey requerido')
29
30
  if (parentId === undefined || parentId === null || parentId === '') {
30
31
  throw new Error('parentId requerido')
31
32
  }
32
- return { [`f_${foreignKey}`]: `eq:${String(parentId)}` }
33
+ const params: Record<string, string> = {
34
+ [`f_${foreignKey}`]: `eq:${String(parentId)}`,
35
+ }
36
+ // Additional static-equality scope columns (polymorphic case: the FK plus
37
+ // e.g. owner_model=Customer). Each becomes its own `f_<col>=eq:<val>` param.
38
+ // The foreign-key entry above wins if a caller redundantly repeats it.
39
+ if (extraFilters) {
40
+ for (const [col, val] of Object.entries(extraFilters)) {
41
+ if (!col || col === foreignKey) continue
42
+ if (val === undefined || val === null) continue
43
+ params[`f_${col}`] = `eq:${String(val)}`
44
+ }
45
+ }
46
+ return params
33
47
  }
34
48
 
35
49
  /**
@@ -86,6 +86,14 @@ const DEFAULT_STRINGS: DynamicRelationStrings = {
86
86
  interface CommonProps {
87
87
  /** id del registro padre. */
88
88
  parentId: string | number
89
+ /**
90
+ * Filtros estáticos extra (igualdad) aplicados ADEMÁS del foreign-key.
91
+ * Caso polimórfico: una tabla de hijos compartida (attachments,
92
+ * addresses) scopeada por `foreign_key=owner_id` Y `owner_model=Customer`.
93
+ * Cada entrada se thread-ea como `f_<col>=eq:<val>` junto al FK en la query
94
+ * de la lista hija. Aditivo: sin filters el comportamiento es idéntico.
95
+ */
96
+ filters?: Record<string, string>
89
97
  /** Hidden columns; el FK siempre se oculta automáticamente. */
90
98
  hiddenColumns?: string[]
91
99
  /** Permisos visibles. Default true. */
@@ -147,6 +155,7 @@ function OneToManyRelation({
147
155
  model,
148
156
  foreignKey,
149
157
  parentId,
158
+ filters,
150
159
  endpoint,
151
160
  hiddenColumns = [],
152
161
  canCreate = true,
@@ -170,11 +179,15 @@ function OneToManyRelation({
170
179
  const [submitting, setSubmitting] = useState(false)
171
180
 
172
181
  const dataEndpoint = endpoint || `/data/${model}`
182
+ // Stable dependency key for the filters object (callers usually pass a fresh
183
+ // literal each render). Keeps fetchAll from re-firing on identity churn while
184
+ // still reacting to real scope changes.
185
+ const filtersKey = useMemo(() => (filters ? JSON.stringify(filters) : ''), [filters])
173
186
 
174
187
  const fetchAll = useCallback(async () => {
175
188
  setLoading(true)
176
189
  try {
177
- const params = buildRelationFilterParams(foreignKey, parentId)
190
+ const params = buildRelationFilterParams(foreignKey, parentId, filters)
178
191
  const [metaRes, dataRes] = await Promise.all([
179
192
  metadata ? Promise.resolve(null) : api.get(`/metadata/table/${model}`),
180
193
  api.get(dataEndpoint, { params }),
@@ -191,7 +204,8 @@ function OneToManyRelation({
191
204
  } finally {
192
205
  setLoading(false)
193
206
  }
194
- }, [api, dataEndpoint, foreignKey, parentId, metadata, model, cacheMetadata])
207
+ // eslint-disable-next-line react-hooks/exhaustive-deps
208
+ }, [api, dataEndpoint, foreignKey, parentId, filtersKey, metadata, model, cacheMetadata])
195
209
 
196
210
  useEffect(() => { fetchAll() }, [fetchAll])
197
211
 
@@ -202,9 +216,11 @@ function OneToManyRelation({
202
216
 
203
217
  const visibleColumns = useMemo(() => {
204
218
  if (!metadata?.columns) return []
205
- const hidden = new Set([foreignKey, ...hiddenColumns])
219
+ // Hide the FK and every scope column — they're fixed for this parent and
220
+ // would just render the same value on every row.
221
+ const hidden = new Set([foreignKey, ...Object.keys(filters || {}), ...hiddenColumns])
206
222
  return metadata.columns.filter(c => !hidden.has(c.key) && !c.hidden)
207
- }, [metadata, foreignKey, hiddenColumns])
223
+ }, [metadata, foreignKey, filtersKey, hiddenColumns])
208
224
 
209
225
  const handleSubmit = useCallback(async (values: Record<string, any>) => {
210
226
  setSubmitting(true)
@@ -213,7 +229,10 @@ function OneToManyRelation({
213
229
  const res = await api.put(`${dataEndpoint}/${editingRow.id}`, values)
214
230
  if (!(res as any).data?.success) throw new Error('update failed')
215
231
  } else {
216
- const payload = buildCreatePayload(foreignKey, parentId, values)
232
+ // Scope columns (polymorphic discriminators like owner_model)
233
+ // are fixed for this relation, so a newly created child must
234
+ // carry them too — otherwise it would not match the list filter.
235
+ const payload = { ...(filters || {}), ...buildCreatePayload(foreignKey, parentId, values) }
217
236
  const res = await api.post(dataEndpoint, payload)
218
237
  if (!(res as any).data?.success) throw new Error('create failed')
219
238
  }
@@ -226,7 +245,8 @@ function OneToManyRelation({
226
245
  } finally {
227
246
  setSubmitting(false)
228
247
  }
229
- }, [api, dataEndpoint, editingRow, fetchAll, foreignKey, onChange, parentId])
248
+ // eslint-disable-next-line react-hooks/exhaustive-deps
249
+ }, [api, dataEndpoint, editingRow, fetchAll, foreignKey, filtersKey, onChange, parentId])
230
250
 
231
251
  const handleDelete = useCallback(async () => {
232
252
  if (!rowToDelete) return
@@ -366,6 +386,7 @@ function ManyToManyRelation({
366
386
  foreignKey,
367
387
  referencesKey,
368
388
  parentId,
389
+ filters,
369
390
  pivotEndpoint,
370
391
  referencesEndpoint,
371
392
  displayKey,
@@ -405,10 +426,12 @@ function ManyToManyRelation({
405
426
  enabled: useResolver,
406
427
  })
407
428
 
429
+ const filtersKey = useMemo(() => (filters ? JSON.stringify(filters) : ''), [filters])
430
+
408
431
  const fetchPivotAndMeta = useCallback(async () => {
409
432
  setLoading(true)
410
433
  try {
411
- const params = buildRelationFilterParams(foreignKey, parentId)
434
+ const params = buildRelationFilterParams(foreignKey, parentId, filters)
412
435
  const tasks: Promise<unknown>[] = [
413
436
  api.get(pivotPath, { params }),
414
437
  ]
@@ -437,7 +460,8 @@ function ManyToManyRelation({
437
460
  } finally {
438
461
  setLoading(false)
439
462
  }
440
- }, [api, pivotPath, foreignKey, parentId, references, targetMeta, cacheMetadata, useResolver, legacyTargetPath])
463
+ // eslint-disable-next-line react-hooks/exhaustive-deps
464
+ }, [api, pivotPath, foreignKey, parentId, filtersKey, references, targetMeta, cacheMetadata, useResolver, legacyTargetPath])
441
465
 
442
466
  useEffect(() => { fetchPivotAndMeta() }, [fetchPivotAndMeta])
443
467
 
@@ -475,7 +499,7 @@ function ManyToManyRelation({
475
499
  setSyncing(true)
476
500
  try {
477
501
  for (const targetId of toAdd) {
478
- const payload = buildPivotAttachPayload(foreignKey, parentId, refKey, targetId)
502
+ const payload = buildPivotAttachPayload(foreignKey, parentId, refKey, targetId, filters || undefined)
479
503
  const res = await api.post(pivotPath, payload)
480
504
  if (!(res as any).data?.success) throw new Error('attach failed')
481
505
  }
@@ -496,7 +520,8 @@ function ManyToManyRelation({
496
520
  } finally {
497
521
  setSyncing(false)
498
522
  }
499
- }, [api, canCreate, canDelete, fetchPivotAndMeta, useResolver, resolved, foreignKey, onChange, parentId, pivotIndex, pivotPath, refKey, selectedIds, syncing])
523
+ // eslint-disable-next-line react-hooks/exhaustive-deps
524
+ }, [api, canCreate, canDelete, fetchPivotAndMeta, useResolver, resolved, foreignKey, filtersKey, onChange, parentId, pivotIndex, pivotPath, refKey, selectedIds, syncing])
500
525
 
501
526
  return (
502
527
  <div