@asteby/metacore-runtime-react 13.5.1 → 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 (44) hide show
  1. package/CHANGELOG.md +47 -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-columns.d.ts +13 -0
  5. package/dist/dynamic-columns.d.ts.map +1 -1
  6. package/dist/dynamic-columns.js +22 -0
  7. package/dist/dynamic-form-schema.d.ts +10 -0
  8. package/dist/dynamic-form-schema.d.ts.map +1 -1
  9. package/dist/dynamic-form-schema.js +21 -0
  10. package/dist/dynamic-form.d.ts +1 -0
  11. package/dist/dynamic-form.d.ts.map +1 -1
  12. package/dist/dynamic-form.js +7 -0
  13. package/dist/dynamic-relation-helpers.d.ts +1 -1
  14. package/dist/dynamic-relation-helpers.d.ts.map +1 -1
  15. package/dist/dynamic-relation-helpers.js +17 -2
  16. package/dist/dynamic-relation.d.ts +8 -0
  17. package/dist/dynamic-relation.d.ts.map +1 -1
  18. package/dist/dynamic-relation.js +26 -12
  19. package/dist/dynamic-relations.d.ts +51 -0
  20. package/dist/dynamic-relations.d.ts.map +1 -0
  21. package/dist/dynamic-relations.js +76 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/types.d.ts +57 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/upload-field.d.ts +15 -0
  28. package/dist/upload-field.d.ts.map +1 -0
  29. package/dist/upload-field.js +109 -0
  30. package/package.json +1 -1
  31. package/src/__tests__/action-visibility-by-state.test.ts +51 -0
  32. package/src/__tests__/dynamic-relation.test.ts +28 -0
  33. package/src/__tests__/dynamic-relations.test.ts +60 -0
  34. package/src/__tests__/upload-field.test.ts +74 -0
  35. package/src/action-modal-dispatcher.tsx +6 -0
  36. package/src/dynamic-columns.tsx +21 -0
  37. package/src/dynamic-form-schema.ts +27 -0
  38. package/src/dynamic-form.tsx +7 -0
  39. package/src/dynamic-relation-helpers.ts +15 -1
  40. package/src/dynamic-relation.tsx +35 -10
  41. package/src/dynamic-relations.tsx +160 -0
  42. package/src/index.ts +6 -0
  43. package/src/types.ts +58 -0
  44. package/src/upload-field.tsx +168 -0
package/dist/types.d.ts CHANGED
@@ -12,6 +12,41 @@ export interface TableMetadata {
12
12
  canExport?: boolean;
13
13
  canImport?: boolean;
14
14
  canCreate?: boolean;
15
+ /**
16
+ * Child relations of this model, served by the kernel (>= v0.41.0). A
17
+ * generic detail page renders one `DynamicRelation` panel per entry via
18
+ * `<DynamicRelations>` to surface, e.g., a Customer's vehicles, addresses
19
+ * and attachments. Absent on hosts/older kernels — purely additive.
20
+ */
21
+ relations?: RelationMeta[];
22
+ }
23
+ /**
24
+ * Describes one child relation of a parent model, mirroring the kernel
25
+ * `RelationMeta` shape (>= v0.41.0). Drives the metadata-driven
26
+ * `<DynamicRelations>` panel list. All keys are snake_case as served by the
27
+ * kernel; the SDK reads them as-is.
28
+ */
29
+ export interface RelationMeta {
30
+ /** Stable identifier for the relation (used as a React key / data attr). */
31
+ name: string;
32
+ /** Cardinality. The SDK maps this onto `DynamicRelation.kind`. */
33
+ kind: 'one_to_many' | 'many_to_many';
34
+ /**
35
+ * Child model key (the `through` model). For one_to_many this is the model
36
+ * whose rows are listed; for many_to_many it is the pivot table.
37
+ */
38
+ through: string;
39
+ /** Child column holding the FK back to the parent. */
40
+ foreign_key: string;
41
+ /**
42
+ * Static equality filters applied on top of the foreign-key scope. Used for
43
+ * polymorphic children (e.g. `{ "owner_model": "Customer" }`) so a shared
44
+ * attachments/addresses table is narrowed to this parent's rows. Each entry
45
+ * becomes a `f_<col>=eq:<val>` query param.
46
+ */
47
+ scope?: Record<string, string>;
48
+ /** Human-readable panel header. */
49
+ label?: string;
15
50
  }
16
51
  export interface FilterDefinition {
17
52
  key: string;
@@ -100,7 +135,7 @@ export interface FieldValidation {
100
135
  max?: number;
101
136
  custom?: string;
102
137
  }
103
- export type FieldWidget = 'text' | 'textarea' | 'richtext' | 'color' | 'number' | 'date' | 'select' | 'dynamic_select' | 'switch';
138
+ export type FieldWidget = 'text' | 'textarea' | 'richtext' | 'color' | 'number' | 'date' | 'select' | 'dynamic_select' | 'switch' | 'upload';
104
139
  export interface ActionFieldDef {
105
140
  key: string;
106
141
  label: string;
@@ -146,6 +181,27 @@ export interface ActionFieldDef {
146
181
  * reconcile. Mirrors kernel v3 `ActionField.balance`.
147
182
  */
148
183
  balance?: FieldBalanceRule;
184
+ /**
185
+ * `upload` widget: comma-separated accept list forwarded to the file input
186
+ * `accept` attribute (e.g. `"image/*,.pdf"`). Tolerates the snake_case the
187
+ * kernel may serve. Optional — when absent any file type is allowed.
188
+ */
189
+ accept?: string;
190
+ /**
191
+ * `upload` widget: maximum file size in bytes. The renderer rejects larger
192
+ * files client-side before POSTing. Tolerates kernel snake_case `max_size`.
193
+ */
194
+ maxSize?: number;
195
+ /** snake_case alias served by the kernel manifest for `maxSize`. */
196
+ max_size?: number;
197
+ /**
198
+ * `upload` widget: server-side storage bucket/prefix the host writes the
199
+ * file under, forwarded to the upload endpoint as `storage_path`. Tolerates
200
+ * kernel snake_case `storage_path`.
201
+ */
202
+ storagePath?: string;
203
+ /** snake_case alias served by the kernel manifest for `storagePath`. */
204
+ storage_path?: string;
149
205
  }
150
206
  /**
151
207
  * Declarative reconciliation constraint on a line-items field: the summed value
@@ -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.1",
3
+ "version": "13.6.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import { isActionAllowedForRowState } from '../dynamic-columns'
4
+
5
+ describe('isActionAllowedForRowState', () => {
6
+ it('hides the action when row.status is NOT in requiresState', () => {
7
+ const action = { key: 'start', requiresState: ['reception'] }
8
+ const row = { id: 1, status: 'in_progress' }
9
+ expect(isActionAllowedForRowState(action, row)).toBe(false)
10
+ })
11
+
12
+ it('shows the action when row.status IS in requiresState', () => {
13
+ const action = { key: 'start', requiresState: ['reception'] }
14
+ const row = { id: 1, status: 'reception' }
15
+ expect(isActionAllowedForRowState(action, row)).toBe(true)
16
+ })
17
+
18
+ it('shows the action when row.status matches one of several requiresState entries', () => {
19
+ const action = { key: 'finish', requiresState: ['in_progress', 'paused'] }
20
+ expect(isActionAllowedForRowState(action, { status: 'paused' })).toBe(true)
21
+ })
22
+
23
+ it('always shows the action when requiresState is empty', () => {
24
+ const action = { key: 'view', requiresState: [] as string[] }
25
+ expect(isActionAllowedForRowState(action, { status: 'whatever' })).toBe(true)
26
+ })
27
+
28
+ it('always shows the action when requiresState is absent (no regression)', () => {
29
+ const action = { key: 'view' }
30
+ expect(isActionAllowedForRowState(action, { status: 'in_progress' })).toBe(true)
31
+ })
32
+
33
+ it('shows all actions when the row has no status field (no regression)', () => {
34
+ const action = { key: 'start', requiresState: ['reception'] }
35
+ expect(isActionAllowedForRowState(action, { id: 1 })).toBe(true)
36
+ expect(isActionAllowedForRowState(action, { id: 1, status: null })).toBe(true)
37
+ expect(isActionAllowedForRowState(action, { id: 1, status: '' })).toBe(true)
38
+ })
39
+
40
+ it('tolerates the snake_case requires_state served by the backend', () => {
41
+ const action = { key: 'start', requires_state: ['reception'] }
42
+ expect(isActionAllowedForRowState(action, { status: 'reception' })).toBe(true)
43
+ expect(isActionAllowedForRowState(action, { status: 'in_progress' })).toBe(false)
44
+ })
45
+
46
+ it('coerces numeric status / state values via String() comparison', () => {
47
+ const action = { key: 'advance', requiresState: [1, 2] as unknown as string[] }
48
+ expect(isActionAllowedForRowState(action, { status: 2 })).toBe(true)
49
+ expect(isActionAllowedForRowState(action, { status: '3' })).toBe(false)
50
+ })
51
+ })
@@ -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} />
@@ -62,6 +62,26 @@ const defaultGetImageUrl = (path: string) => path
62
62
  const getNestedValue = (obj: any, path: string) =>
63
63
  path.split('.').reduce((acc, part) => acc && acc[part], obj)
64
64
 
65
+ /**
66
+ * State-machine gate for per-row actions.
67
+ *
68
+ * An action that declares a non-empty `requiresState` (camelCase) / `requires_state`
69
+ * (snake_case, as served by some backends) is only surfaced for rows whose `status`
70
+ * field value is contained in that array. This hides e.g. an "Iniciar trabajo"
71
+ * action (requiresState: ['reception']) on an order already in `in_progress`.
72
+ *
73
+ * Null-safe & non-regressive:
74
+ * - action without requiresState (or empty array) → always shown.
75
+ * - row with no `status` field → all actions shown.
76
+ */
77
+ export const isActionAllowedForRowState = (action: any, row: any): boolean => {
78
+ const requires: unknown = action?.requiresState ?? action?.requires_state
79
+ if (!Array.isArray(requires) || requires.length === 0) return true
80
+ const status = row?.status
81
+ if (status === undefined || status === null || status === '') return true
82
+ return requires.map(String).includes(String(status))
83
+ }
84
+
65
85
  const lowerFirst = (value?: string) => {
66
86
  if (!value) return value
67
87
  return value.charAt(0).toLowerCase() + value.slice(1)
@@ -519,6 +539,7 @@ export function makeDefaultGetDynamicColumns(
519
539
  </DropdownMenuTrigger>
520
540
  <DropdownMenuContent align="end">
521
541
  {resolvedActions
542
+ .filter((action) => isActionAllowedForRowState(action, row.original))
522
543
  .filter((action) => {
523
544
  if (!action.condition) return true
524
545
  const { field, operator, value } = action.condition
@@ -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
  /**