@asteby/metacore-runtime-react 13.10.1 → 13.10.2
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 +28 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +15 -1
- package/dist/dynamic-relation-helpers.d.ts.map +1 -1
- package/dist/dynamic-relation-helpers.js +9 -0
- package/package.json +3 -3
- package/src/__tests__/dynamic-relation.test.ts +31 -0
- package/src/dialogs/dynamic-record.tsx +35 -1
- package/src/dynamic-relation-helpers.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 13.10.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2813c61: fix(native-form): render rich widgets from column metadata (ref→searchable picker, image/upload→dropzone)
|
|
8
|
+
|
|
9
|
+
The native create/edit modal (`DynamicRecordDialog`, the one `CreateRecordDialog`
|
|
10
|
+
wraps and fetches `/metadata/modal/:model` for) only routed FK columns to its
|
|
11
|
+
searchable picker when they shipped a legacy `searchEndpoint` / `type: "search"`.
|
|
12
|
+
Now that the kernel serves a belongs_to column's `ref` (and an explicit
|
|
13
|
+
`widget`) on modal fields, a plain `ref` column degraded to a raw uuid text input.
|
|
14
|
+
|
|
15
|
+
`EditField` now honors:
|
|
16
|
+
- `field.ref` (or the snake_case `source`/`relation` aliases, or
|
|
17
|
+
`widget: "dynamic_select"`) → renders `DynamicSelectField`: an async typeahead
|
|
18
|
+
against `/api/options/<ref>?field=id` with option thumbnails when the remote
|
|
19
|
+
rows carry an `image` (e.g. a brand logo). Static inline `options` still take
|
|
20
|
+
the enum `<Select>` path — a `ref` column ships no inline options, so the FK
|
|
21
|
+
branch never shadows a static enum.
|
|
22
|
+
- `widget: "upload"` (alongside the existing `type: "image"`) → the themed file
|
|
23
|
+
dropzone, same control as the Brand logo.
|
|
24
|
+
|
|
25
|
+
Also fixes `deriveRelationFormFields` (the column→field mapper for
|
|
26
|
+
`DynamicRelation` inline child forms): it now carries `col.ref` through to the
|
|
27
|
+
field so a belongs_to column resolves to `dynamic_select`, and maps
|
|
28
|
+
`image`/`media-gallery` columns to media field types so they resolve to the
|
|
29
|
+
`upload` widget instead of a text input.
|
|
30
|
+
|
|
3
31
|
## 13.10.1
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAwF1C,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAsDD,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,MAAM,GACT,EAAE,wBAAwB,2CAsP1B"}
|
|
@@ -12,6 +12,8 @@ import { format, parseISO } from 'date-fns';
|
|
|
12
12
|
import { es } from 'date-fns/locale';
|
|
13
13
|
import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon } from 'lucide-react';
|
|
14
14
|
import { useApi } from '../api-context';
|
|
15
|
+
import { DynamicSelectField } from '../dynamic-select-field';
|
|
16
|
+
import { getFieldRef } from '../dynamic-form-schema';
|
|
15
17
|
function resolvePath(obj, path) {
|
|
16
18
|
return path.split('.').reduce((acc, part) => acc?.[part], obj);
|
|
17
19
|
}
|
|
@@ -260,9 +262,21 @@ function EditField({ field, value, onChange }) {
|
|
|
260
262
|
if (field.type === 'textarea') {
|
|
261
263
|
return (_jsx(Textarea, { value: value ?? '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, rows: 4 }));
|
|
262
264
|
}
|
|
263
|
-
|
|
265
|
+
// Media widgets: the kernel may serve an explicit `widget: 'upload'` (or the
|
|
266
|
+
// `image` type) for a file/photo column. Both render the themed dropzone
|
|
267
|
+
// that POSTs to the host upload endpoint — same control as the Brand logo.
|
|
268
|
+
if (field.type === 'image' || field.widget === 'upload') {
|
|
264
269
|
return _jsx(ImageUploadField, { field: field, value: value, onChange: onChange });
|
|
265
270
|
}
|
|
271
|
+
// FK columns: a `ref` (kernel-derived belongs_to target) or an explicit
|
|
272
|
+
// `widget: 'dynamic_select'` renders the async searchable picker against
|
|
273
|
+
// /api/options/<ref>?field=id — with option thumbnails when the remote rows
|
|
274
|
+
// carry an `image` — instead of a raw FK uuid text input. Static inline
|
|
275
|
+
// `options` are handled by the enum <Select> branch below; a ref column does
|
|
276
|
+
// not ship inline options, so this never shadows a static enum.
|
|
277
|
+
if ((getFieldRef(field) || field.widget === 'dynamic_select') && !field.options?.length) {
|
|
278
|
+
return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
|
|
279
|
+
}
|
|
266
280
|
if (field.type === 'search' && field.searchEndpoint) {
|
|
267
281
|
return _jsx(SearchField, { field: field, value: value, onChange: onChange });
|
|
268
282
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-relation-helpers.d.ts","sourceRoot":"","sources":["../src/dynamic-relation-helpers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE9E,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,cAAc,CAAA;AAEhE,MAAM,WAAW,YAAY;IACzB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAC7C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAGrB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACpC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3D,UAAU,EAAE,MAAM,GACnB,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"dynamic-relation-helpers.d.ts","sourceRoot":"","sources":["../src/dynamic-relation-helpers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE9E,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,cAAc,CAAA;AAEhE,MAAM,WAAW,YAAY;IACzB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GAC7C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAGrB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACpC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3D,UAAU,EAAE,MAAM,GACnB,cAAc,EAAE,CAoBlB;AAkBD;;;;GAIG;AACH,wBAAgB,cAAc,CAC1B,GAAG,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,SAAS,EAChD,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,GACnB,MAAM,CAKR;AAMD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACnC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAcrB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACpC,SAAS,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EACzD,aAAa,EAAE,MAAM,GACtB,MAAM,EAAE,CASV;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAC9B,SAAS,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EACzD,aAAa,EAAE,MAAM,GACtB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAU9B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CACzB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAC3B,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAC5B;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAYzC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC3B,GAAG,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACrC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,OAAO,EAAE,aAAa,CAAC,gBAAgB,CAAC,GAAG,IAAI,GAAG,SAAS,GAC5D,MAAM,CAiBR"}
|
|
@@ -59,6 +59,11 @@ export function deriveRelationFormFields(metadata, foreignKey) {
|
|
|
59
59
|
type: columnTypeToFieldType(col),
|
|
60
60
|
required: false,
|
|
61
61
|
options: col.options?.map(o => ({ value: String(o.value), label: o.label })),
|
|
62
|
+
// Carry the FK target through so a belongs_to column renders the
|
|
63
|
+
// async searchable picker (resolveWidget → 'dynamic_select') rather
|
|
64
|
+
// than a raw uuid text input. Without this the column lost its `ref`
|
|
65
|
+
// crossing the column→field boundary and degraded to plain text.
|
|
66
|
+
ref: col.ref,
|
|
62
67
|
});
|
|
63
68
|
}
|
|
64
69
|
return out;
|
|
@@ -69,6 +74,10 @@ function columnTypeToFieldType(col) {
|
|
|
69
74
|
case 'boolean': return 'boolean';
|
|
70
75
|
case 'date': return 'date';
|
|
71
76
|
case 'select': return 'select';
|
|
77
|
+
// Media columns map to the upload widget (resolveWidget → 'upload') so an
|
|
78
|
+
// image/photo column gets the file dropzone instead of a text input.
|
|
79
|
+
case 'image': return 'image';
|
|
80
|
+
case 'media-gallery': return 'media';
|
|
72
81
|
case 'text':
|
|
73
82
|
default:
|
|
74
83
|
return 'string';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "13.10.
|
|
3
|
+
"version": "13.10.2",
|
|
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-
|
|
65
|
-
"@asteby/metacore-
|
|
64
|
+
"@asteby/metacore-ui": "2.1.2",
|
|
65
|
+
"@asteby/metacore-sdk": "3.1.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsc -p tsconfig.json",
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
pickOptionLabel,
|
|
11
11
|
relationRowKey,
|
|
12
12
|
} from '../dynamic-relation-helpers'
|
|
13
|
+
import { resolveWidget } from '../dynamic-form-schema'
|
|
13
14
|
import type { ColumnDefinition, TableMetadata } from '../types'
|
|
14
15
|
|
|
15
16
|
describe('buildRelationFilterParams', () => {
|
|
@@ -134,6 +135,36 @@ describe('deriveRelationFormFields', () => {
|
|
|
134
135
|
expect(deriveRelationFormFields(undefined, 'invoice_id')).toEqual([])
|
|
135
136
|
expect(deriveRelationFormFields({ columns: [] }, 'invoice_id')).toEqual([])
|
|
136
137
|
})
|
|
138
|
+
|
|
139
|
+
// The column→field boundary must carry `ref` (FK target) so a belongs_to
|
|
140
|
+
// column renders the searchable picker instead of a raw uuid text input.
|
|
141
|
+
it('propaga el ref (FK target) de la columna al field', () => {
|
|
142
|
+
const meta: Pick<TableMetadata, 'columns'> = {
|
|
143
|
+
columns: [
|
|
144
|
+
{ key: 'product_id', label: 'Producto', type: 'text', sortable: true, filterable: false, ref: 'product' },
|
|
145
|
+
],
|
|
146
|
+
}
|
|
147
|
+
const fields = deriveRelationFormFields(meta, 'invoice_id')
|
|
148
|
+
const prod = fields.find(f => f.key === 'product_id')
|
|
149
|
+
expect(prod?.ref).toBe('product')
|
|
150
|
+
// …and that ref drives the widget to the async searchable picker.
|
|
151
|
+
expect(resolveWidget(prod!)).toBe('dynamic_select')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Media columns must map to a media-bearing field type so the form renders
|
|
155
|
+
// the upload dropzone (resolveWidget → 'upload'), not a text input.
|
|
156
|
+
it('mapea columnas image/media-gallery a tipos que resuelven a upload', () => {
|
|
157
|
+
const meta: Pick<TableMetadata, 'columns'> = {
|
|
158
|
+
columns: [
|
|
159
|
+
{ key: 'logo', label: 'Logo', type: 'image', sortable: false, filterable: false },
|
|
160
|
+
{ key: 'gallery', label: 'Galería', type: 'media-gallery', sortable: false, filterable: false },
|
|
161
|
+
],
|
|
162
|
+
}
|
|
163
|
+
const fields = deriveRelationFormFields(meta, 'invoice_id')
|
|
164
|
+
const byKey = Object.fromEntries(fields.map(f => [f.key, f]))
|
|
165
|
+
expect(resolveWidget(byKey['logo'])).toBe('upload')
|
|
166
|
+
expect(resolveWidget(byKey['gallery'])).toBe('upload')
|
|
167
|
+
})
|
|
137
168
|
})
|
|
138
169
|
|
|
139
170
|
describe('relationRowKey', () => {
|
|
@@ -40,6 +40,9 @@ import { format, parseISO } from 'date-fns'
|
|
|
40
40
|
import { es } from 'date-fns/locale'
|
|
41
41
|
import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon } from 'lucide-react'
|
|
42
42
|
import { useApi } from '../api-context'
|
|
43
|
+
import { DynamicSelectField } from '../dynamic-select-field'
|
|
44
|
+
import { getFieldRef } from '../dynamic-form-schema'
|
|
45
|
+
import type { ActionFieldDef } from '../types'
|
|
43
46
|
|
|
44
47
|
interface FieldOption {
|
|
45
48
|
value: string
|
|
@@ -58,6 +61,24 @@ interface FieldDef {
|
|
|
58
61
|
hidden?: boolean
|
|
59
62
|
searchEndpoint?: string
|
|
60
63
|
filterBy?: string
|
|
64
|
+
/**
|
|
65
|
+
* FK target model the kernel auto-derives for a belongs_to column (>=
|
|
66
|
+
* v0.46.x serves it on modal fields, not just action fields). When present
|
|
67
|
+
* the native form renders an async searchable picker (`DynamicSelectField`)
|
|
68
|
+
* against `/api/options/<ref>?field=id` — with option thumbnails when the
|
|
69
|
+
* remote rows carry an `image` — instead of a raw FK text input. Tolerates
|
|
70
|
+
* the snake_case `source`/`relation` aliases the manifest may serve.
|
|
71
|
+
*/
|
|
72
|
+
ref?: string
|
|
73
|
+
source?: string
|
|
74
|
+
relation?: string
|
|
75
|
+
/**
|
|
76
|
+
* Explicit renderer hint. Wins over the `type` switch: `dynamic_select`
|
|
77
|
+
* forces the searchable picker, `upload` forces the file dropzone. Lets the
|
|
78
|
+
* kernel opt a plain text/uuid column into a rich widget without changing
|
|
79
|
+
* its SQL type. Unknown values fall through to the `type`-based default.
|
|
80
|
+
*/
|
|
81
|
+
widget?: string
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
// Permissive shape: the wire payload may omit some fields (e.g. `title` is
|
|
@@ -566,10 +587,23 @@ function EditField({ field, value, onChange }: {
|
|
|
566
587
|
)
|
|
567
588
|
}
|
|
568
589
|
|
|
569
|
-
|
|
590
|
+
// Media widgets: the kernel may serve an explicit `widget: 'upload'` (or the
|
|
591
|
+
// `image` type) for a file/photo column. Both render the themed dropzone
|
|
592
|
+
// that POSTs to the host upload endpoint — same control as the Brand logo.
|
|
593
|
+
if (field.type === 'image' || field.widget === 'upload') {
|
|
570
594
|
return <ImageUploadField field={field} value={value} onChange={onChange} />
|
|
571
595
|
}
|
|
572
596
|
|
|
597
|
+
// FK columns: a `ref` (kernel-derived belongs_to target) or an explicit
|
|
598
|
+
// `widget: 'dynamic_select'` renders the async searchable picker against
|
|
599
|
+
// /api/options/<ref>?field=id — with option thumbnails when the remote rows
|
|
600
|
+
// carry an `image` — instead of a raw FK uuid text input. Static inline
|
|
601
|
+
// `options` are handled by the enum <Select> branch below; a ref column does
|
|
602
|
+
// not ship inline options, so this never shadows a static enum.
|
|
603
|
+
if ((getFieldRef(field as ActionFieldDef) || field.widget === 'dynamic_select') && !field.options?.length) {
|
|
604
|
+
return <DynamicSelectField field={field as ActionFieldDef} value={value} onChange={onChange} />
|
|
605
|
+
}
|
|
606
|
+
|
|
573
607
|
if (field.type === 'search' && field.searchEndpoint) {
|
|
574
608
|
return <SearchField field={field} value={value} onChange={onChange} />
|
|
575
609
|
}
|
|
@@ -82,6 +82,11 @@ export function deriveRelationFormFields(
|
|
|
82
82
|
type: columnTypeToFieldType(col),
|
|
83
83
|
required: false,
|
|
84
84
|
options: col.options?.map(o => ({ value: String(o.value), label: o.label })),
|
|
85
|
+
// Carry the FK target through so a belongs_to column renders the
|
|
86
|
+
// async searchable picker (resolveWidget → 'dynamic_select') rather
|
|
87
|
+
// than a raw uuid text input. Without this the column lost its `ref`
|
|
88
|
+
// crossing the column→field boundary and degraded to plain text.
|
|
89
|
+
ref: col.ref,
|
|
85
90
|
})
|
|
86
91
|
}
|
|
87
92
|
return out
|
|
@@ -93,6 +98,10 @@ function columnTypeToFieldType(col: ColumnDefinition): string {
|
|
|
93
98
|
case 'boolean': return 'boolean'
|
|
94
99
|
case 'date': return 'date'
|
|
95
100
|
case 'select': return 'select'
|
|
101
|
+
// Media columns map to the upload widget (resolveWidget → 'upload') so an
|
|
102
|
+
// image/photo column gets the file dropzone instead of a text input.
|
|
103
|
+
case 'image': return 'image'
|
|
104
|
+
case 'media-gallery': return 'media'
|
|
96
105
|
case 'text':
|
|
97
106
|
default:
|
|
98
107
|
return 'string'
|