@asteby/metacore-runtime-react 13.9.0 → 13.10.1
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 +33 -0
- package/dist/action-modal-dispatcher.js +10 -1
- package/dist/dynamic-form-schema.d.ts +9 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +29 -0
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +42 -11
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/dynamic-form.test.ts +28 -0
- package/src/action-modal-dispatcher.tsx +11 -2
- package/src/dynamic-form-schema.ts +29 -0
- package/src/dynamic-select-field.tsx +77 -16
- package/src/types.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 13.10.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9107b10: Fix create-placement action submit hitting `/me/undefined/action/...` (400 Invalid record ID). `buildActionUrl` now omits the record segment when there is no record, posting to the collection route `/data/:model/me/action/:action`, so create modals declared as `placement:create` actions work.
|
|
8
|
+
|
|
9
|
+
## 13.10.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- da8139d: feat(dynamic-form,nav): FK→searchable picker, image thumbnails, media→upload, query-aware nav
|
|
14
|
+
- **resolveWidget**: a field that declares an FK target (`ref`, or the
|
|
15
|
+
snake_case `source`/`relation` the kernel may serve) now resolves to
|
|
16
|
+
`dynamic_select` BEFORE the type switch, so any declared relation renders a
|
|
17
|
+
searchable picker instead of a raw text input — regardless of the column's
|
|
18
|
+
SQL type. `image`/`media`/`file` types resolve to the `upload` widget.
|
|
19
|
+
- **DynamicSelectField**: renders the option's `image` as a small thumbnail in
|
|
20
|
+
the trigger (selected option) and in each dropdown row, with a neutral
|
|
21
|
+
placeholder fallback. Thumbnails only appear when the resolved options carry
|
|
22
|
+
images, so image-less relations keep their plain text list. Also tolerates
|
|
23
|
+
the `source`/`relation` ref aliases for option resolution and inline-create.
|
|
24
|
+
- **NavGroup.checkIsActive**: now query-aware. Order-status style items that
|
|
25
|
+
share a path but differ only by a query param (`?status=reception` vs
|
|
26
|
+
`?status=delivery`) light up one at a time instead of all together; an item
|
|
27
|
+
that declares query params must match the current href's query exactly
|
|
28
|
+
(after normalization, with transient `f_` filter params stripped), while
|
|
29
|
+
query-less links keep matching on path alone.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [da8139d]
|
|
34
|
+
- @asteby/metacore-ui@2.1.2
|
|
35
|
+
|
|
3
36
|
## 13.9.0
|
|
4
37
|
|
|
5
38
|
### Minor Changes
|
|
@@ -35,7 +35,16 @@ export function ActionModalDispatcher({ open, onOpenChange, action, model, recor
|
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
37
|
function buildActionUrl(endpoint, model, recordId, actionKey) {
|
|
38
|
-
|
|
38
|
+
// A create-placement (collection) action has no record yet, so `recordId`
|
|
39
|
+
// is undefined. Omit the `/{id}` segment so the request hits the collection
|
|
40
|
+
// route (`/data/:model/me/action/:action`) instead of the per-record route
|
|
41
|
+
// (`/data/:model/me/:id/action/:action`), which would reject the literal
|
|
42
|
+
// "undefined" as an invalid record ID (ops dynamic.go ExecuteAction → 400).
|
|
43
|
+
const hasRecord = recordId != null && recordId !== '' && recordId !== 'undefined';
|
|
44
|
+
if (endpoint) {
|
|
45
|
+
return hasRecord ? `${endpoint}/${recordId}/action/${actionKey}` : `${endpoint}/action/${actionKey}`;
|
|
46
|
+
}
|
|
47
|
+
return hasRecord ? `/data/${model}/me/${recordId}/action/${actionKey}` : `/data/${model}/me/action/${actionKey}`;
|
|
39
48
|
}
|
|
40
49
|
function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
41
50
|
const { t } = useTranslation();
|
|
@@ -54,6 +54,15 @@ export interface BalanceState {
|
|
|
54
54
|
*/
|
|
55
55
|
export declare function evaluateBalance(field: ActionFieldDef, rows: any[] | undefined): BalanceState | undefined;
|
|
56
56
|
export declare function resolveWidget(field: ActionFieldDef): string;
|
|
57
|
+
/**
|
|
58
|
+
* Resolves a field's FK target, tolerating the camelCase `ref` (authored SDK
|
|
59
|
+
* shape) and the snake_case `source` / `relation` aliases the kernel manifest
|
|
60
|
+
* may serve for a belongs_to column. Returns the trimmed model key, or
|
|
61
|
+
* `undefined` when the field declares no relation.
|
|
62
|
+
*/
|
|
63
|
+
export declare function getFieldRef(field: ActionFieldDef): string | undefined;
|
|
64
|
+
/** True when a field declares an FK target the SDK can resolve options against. */
|
|
65
|
+
export declare function fieldHasRef(field: ActionFieldDef): boolean;
|
|
57
66
|
/**
|
|
58
67
|
* Normalizes an upload field's config, tolerating both the camelCase authored
|
|
59
68
|
* SDK shape and the snake_case the kernel serves (`max_size`, `storage_path`).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,SAAS,CAAA;AAiB9D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,CAEzF;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;;kBAMtD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,CAGrE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,cAAc,GACtB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAatG;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAO3C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,YAAY,GAAG,SAAS,CAgB1B;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,SAAS,CAAA;AAiB9D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,CAEzF;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;;kBAMtD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,CAGrE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,cAAc,GACtB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAatG;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAO3C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACjC,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC3B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,GACxB,YAAY,GAAG,SAAS,CAgB1B;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CA4B3D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAIrE;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG;IACpD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB,CAaA"}
|
|
@@ -190,6 +190,13 @@ function fieldToZod(field) {
|
|
|
190
190
|
export function resolveWidget(field) {
|
|
191
191
|
if (field.widget)
|
|
192
192
|
return field.widget;
|
|
193
|
+
// S1: any field that declares an FK target (`ref`, or the snake_case
|
|
194
|
+
// `source`/`relation` the kernel may serve) renders as an async searchable
|
|
195
|
+
// single-select — NOT a raw text input. This wins over the `type` switch so
|
|
196
|
+
// a declared FK column is a picker regardless of its SQL column type
|
|
197
|
+
// (uuid/text/etc), matching the kernel's option-resolution semantics.
|
|
198
|
+
if (fieldHasRef(field))
|
|
199
|
+
return 'dynamic_select';
|
|
193
200
|
switch (field.type) {
|
|
194
201
|
case 'textarea': return 'textarea';
|
|
195
202
|
case 'select': return 'select';
|
|
@@ -202,9 +209,31 @@ export function resolveWidget(field) {
|
|
|
202
209
|
// File upload: POSTs to the host upload endpoint and stores the returned
|
|
203
210
|
// file url/path as the field value. Rendered by `UploadField`.
|
|
204
211
|
case 'upload': return 'upload';
|
|
212
|
+
// S2: media-bearing types resolve to the upload widget so an `image`
|
|
213
|
+
// (logo/photo) or generic `file`/`media` field gets a real file picker
|
|
214
|
+
// instead of a free-text input.
|
|
215
|
+
case 'image': return 'upload';
|
|
216
|
+
case 'media': return 'upload';
|
|
217
|
+
case 'file': return 'upload';
|
|
205
218
|
default: return 'text';
|
|
206
219
|
}
|
|
207
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Resolves a field's FK target, tolerating the camelCase `ref` (authored SDK
|
|
223
|
+
* shape) and the snake_case `source` / `relation` aliases the kernel manifest
|
|
224
|
+
* may serve for a belongs_to column. Returns the trimmed model key, or
|
|
225
|
+
* `undefined` when the field declares no relation.
|
|
226
|
+
*/
|
|
227
|
+
export function getFieldRef(field) {
|
|
228
|
+
const ref = field.ref ?? field.source ?? field.relation;
|
|
229
|
+
if (typeof ref === 'string' && ref.trim() !== '')
|
|
230
|
+
return ref.trim();
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
/** True when a field declares an FK target the SDK can resolve options against. */
|
|
234
|
+
export function fieldHasRef(field) {
|
|
235
|
+
return getFieldRef(field) !== undefined;
|
|
236
|
+
}
|
|
208
237
|
/**
|
|
209
238
|
* Normalizes an upload field's config, tolerating both the camelCase authored
|
|
210
239
|
* SDK shape and the snake_case the kernel serves (`max_size`, `storage_path`).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgD7C,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,2CA2KrF;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -24,8 +24,28 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
24
24
|
// case — start empty and never hit this.
|
|
25
25
|
import { useEffect, useState } from 'react';
|
|
26
26
|
import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives';
|
|
27
|
-
import { Check, ChevronsUpDown, Loader2, Plus } from 'lucide-react';
|
|
27
|
+
import { Check, ChevronsUpDown, ImageIcon, Loader2, Plus } from 'lucide-react';
|
|
28
28
|
import { useOptionsResolver } from './use-options-resolver';
|
|
29
|
+
import { getFieldRef } from './dynamic-form-schema';
|
|
30
|
+
/**
|
|
31
|
+
* Small square thumbnail for an option's `image`. Falls back to a neutral
|
|
32
|
+
* placeholder icon when the option has no image so rows/triggers stay aligned.
|
|
33
|
+
* `size` is in pixels (kept small — 20–24px — so the picker reads as a list,
|
|
34
|
+
* not a gallery). Inline style for the box dimensions: arbitrary Tailwind
|
|
35
|
+
* classes from a federated addon don't always survive the host's class scan.
|
|
36
|
+
*/
|
|
37
|
+
function OptionThumb({ image, size = 20 }) {
|
|
38
|
+
const box = { width: size, height: size };
|
|
39
|
+
if (!image) {
|
|
40
|
+
return (_jsx("span", { className: "text-muted-foreground bg-muted flex shrink-0 items-center justify-center rounded-sm", style: box, "aria-hidden": true, children: _jsx(ImageIcon, { className: "size-3 opacity-60" }) }));
|
|
41
|
+
}
|
|
42
|
+
return (_jsx("img", { src: image, alt: "", "aria-hidden": true, loading: "lazy", className: "shrink-0 rounded-sm object-cover", style: box,
|
|
43
|
+
// A broken image url shouldn't leave a torn-icon glyph; collapse to
|
|
44
|
+
// the neutral placeholder background instead.
|
|
45
|
+
onError: (e) => {
|
|
46
|
+
e.currentTarget.style.visibility = 'hidden';
|
|
47
|
+
} }));
|
|
48
|
+
}
|
|
29
49
|
function useDebounced(value, ms) {
|
|
30
50
|
const [debounced, setDebounced] = useState(value);
|
|
31
51
|
useEffect(() => {
|
|
@@ -41,22 +61,33 @@ export function DynamicSelectField({ field, value, onChange }) {
|
|
|
41
61
|
// Remember the label of the option the user actually picked so the trigger
|
|
42
62
|
// shows a name (not a UUID) without a round-trip.
|
|
43
63
|
const [picked, setPicked] = useState(null);
|
|
64
|
+
// Tolerate the snake_case `source`/`relation` aliases the kernel may serve
|
|
65
|
+
// for the FK target, not just camelCase `ref`.
|
|
66
|
+
const fieldRef = getFieldRef(field);
|
|
44
67
|
const { options, loading } = useOptionsResolver({
|
|
45
68
|
modelKey: '',
|
|
46
69
|
fieldKey: 'id',
|
|
47
|
-
ref:
|
|
70
|
+
ref: fieldRef,
|
|
48
71
|
// searchEndpoint only drives the URL when there's no ref — ref is the
|
|
49
72
|
// canonical, kernel-derived path and wins.
|
|
50
|
-
endpoint:
|
|
73
|
+
endpoint: fieldRef ? undefined : field.searchEndpoint,
|
|
51
74
|
query: debounced,
|
|
52
75
|
limit: 20,
|
|
53
76
|
// Don't fetch until the popover opens (and keep fetching as the query
|
|
54
77
|
// changes while open).
|
|
55
78
|
enabled: open,
|
|
56
79
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
// The currently-selected option, resolved either from what the user picked
|
|
81
|
+
// (cached in `picked`) or from the loaded page. Drives both the trigger
|
|
82
|
+
// label and its thumbnail.
|
|
83
|
+
const selectedOption = (picked && String(picked.id) === String(value) ? picked : null) ??
|
|
84
|
+
options.find((o) => String(o.id) === String(value)) ??
|
|
85
|
+
null;
|
|
86
|
+
const selectedLabel = selectedOption?.label ?? (value ? String(value) : '');
|
|
87
|
+
// Only switch the picker into "with thumbnails" mode when the data actually
|
|
88
|
+
// carries images — a relation whose options have no `image` keeps the plain
|
|
89
|
+
// text list it had before (no empty placeholder column).
|
|
90
|
+
const hasImages = !!selectedOption?.image || options.some((o) => !!o.image);
|
|
60
91
|
const handlePick = (opt) => {
|
|
61
92
|
setPicked(opt);
|
|
62
93
|
onChange(String(opt.id));
|
|
@@ -69,11 +100,11 @@ export function DynamicSelectField({ field, value, onChange }) {
|
|
|
69
100
|
// hands back the new record and we select it immediately. No host import →
|
|
70
101
|
// no circular dependency; works for ANY dynamic_select with a `ref`.
|
|
71
102
|
const openCreate = () => {
|
|
72
|
-
if (!
|
|
103
|
+
if (!fieldRef || typeof window === 'undefined')
|
|
73
104
|
return;
|
|
74
105
|
window.dispatchEvent(new CustomEvent('metacore:create-record', {
|
|
75
106
|
detail: {
|
|
76
|
-
model:
|
|
107
|
+
model: fieldRef,
|
|
77
108
|
onCreated: (rec) => {
|
|
78
109
|
if (rec && rec.id != null) {
|
|
79
110
|
const id = String(rec.id);
|
|
@@ -88,12 +119,12 @@ export function DynamicSelectField({ field, value, onChange }) {
|
|
|
88
119
|
// to the cell. Without min-w-0 the combobox+button row sizes to its content
|
|
89
120
|
// (the long empty-state placeholder) and overflows the column, pushing the
|
|
90
121
|
// "+" off-screen — it only "fit" once a short value was selected.
|
|
91
|
-
return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "min-w-0 flex-1 justify-between font-normal", "data-empty": !value, children: [
|
|
122
|
+
return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "min-w-0 flex-1 justify-between font-normal", "data-empty": !value, children: [_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [hasImages && value ? (_jsx(OptionThumb, { image: selectedOption?.image, size: 20 })) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || field.placeholder || 'Buscar…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
|
|
92
123
|
// Match the trigger width without an arbitrary Tailwind class
|
|
93
124
|
// (those don't always survive a consuming app's Tailwind scan).
|
|
94
125
|
style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: field.placeholder || 'Buscar…', value: search, onValueChange: setSearch }), _jsxs(CommandList, { children: [loading && (_jsxs("div", { className: "text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm", children: [_jsx(Loader2, { className: "size-4 animate-spin" }), "Buscando\u2026"] })), !loading && options.length === 0 && (_jsx(CommandEmpty, { children: debounced ? 'Sin resultados' : 'Escribí para buscar…' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
|
|
95
126
|
const isSel = String(opt.id) === String(value);
|
|
96
|
-
return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 ' + (isSel ? 'opacity-100' : 'opacity-0') }), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] })] }, String(opt.id)));
|
|
97
|
-
}) }))] })] }) })] }),
|
|
127
|
+
return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0') }), hasImages && (_jsx(OptionThumb, { image: opt.image, size: 24 })), _jsxs("div", { className: "ml-2 flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] })] }, String(opt.id)));
|
|
128
|
+
}) }))] })] }) })] }), fieldRef && (_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: openCreate, title: `Crear ${field.label ?? fieldRef}`, "aria-label": `Crear ${field.label ?? fieldRef}`, children: _jsx(Plus, { className: "size-4" }) }))] }));
|
|
98
129
|
}
|
|
99
130
|
export default DynamicSelectField;
|
package/dist/types.d.ts
CHANGED
|
@@ -156,6 +156,14 @@ export interface ActionFieldDef {
|
|
|
156
156
|
* `useOptionsResolver` against `/api/options/<ref>?field=id`.
|
|
157
157
|
*/
|
|
158
158
|
ref?: string;
|
|
159
|
+
/**
|
|
160
|
+
* snake_case aliases the kernel manifest may serve for a belongs_to FK
|
|
161
|
+
* target instead of `ref`. Treated as equivalent to `ref` by the SDK so a
|
|
162
|
+
* declared relation renders a searchable picker regardless of which key the
|
|
163
|
+
* backend emits.
|
|
164
|
+
*/
|
|
165
|
+
source?: string;
|
|
166
|
+
relation?: string;
|
|
159
167
|
/**
|
|
160
168
|
* Columns of a repeatable line-items group. Mirrors the kernel v3
|
|
161
169
|
* `ActionField.item_fields` (json `item_fields`). Present on a field
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.10.1",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"date-fns": ">=3",
|
|
35
35
|
"react-day-picker": ">=8",
|
|
36
36
|
"@asteby/metacore-sdk": "^3.1.0",
|
|
37
|
-
"@asteby/metacore-ui": "^2.1.
|
|
37
|
+
"@asteby/metacore-ui": "^2.1.2"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@tanstack/react-router": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"vitest": "^4.0.0",
|
|
63
63
|
"zustand": "^5.0.0",
|
|
64
64
|
"@asteby/metacore-sdk": "3.1.0",
|
|
65
|
-
"@asteby/metacore-ui": "2.1.
|
|
65
|
+
"@asteby/metacore-ui": "2.1.2"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsc -p tsconfig.json",
|
|
@@ -101,6 +101,34 @@ describe('resolveWidget', () => {
|
|
|
101
101
|
expect(resolveWidget({ key: 'k', label: 'L', type: 'string' })).toBe('text')
|
|
102
102
|
expect(resolveWidget({ key: 'k', label: 'L', type: 'email' })).toBe('text')
|
|
103
103
|
})
|
|
104
|
+
|
|
105
|
+
// S1: a declared FK target makes the field a searchable picker regardless
|
|
106
|
+
// of its SQL column type — wins over the type→text fallback.
|
|
107
|
+
it('un field con ref (FK) resuelve a dynamic_select antes del switch por type', () => {
|
|
108
|
+
expect(resolveWidget({ key: 'k', label: 'L', type: 'string', ref: 'product' })).toBe('dynamic_select')
|
|
109
|
+
expect(resolveWidget({ key: 'k', label: 'L', type: 'uuid', ref: 'customer' })).toBe('dynamic_select')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('tolera los alias snake_case source/relation como ref', () => {
|
|
113
|
+
expect(resolveWidget({ key: 'k', label: 'L', type: 'string', source: 'product' })).toBe('dynamic_select')
|
|
114
|
+
expect(resolveWidget({ key: 'k', label: 'L', type: 'string', relation: 'vendor' })).toBe('dynamic_select')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('un ref en blanco NO fuerza dynamic_select', () => {
|
|
118
|
+
expect(resolveWidget({ key: 'k', label: 'L', type: 'string', ref: '' })).toBe('text')
|
|
119
|
+
expect(resolveWidget({ key: 'k', label: 'L', type: 'string', ref: ' ' })).toBe('text')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// S2: media-bearing types render the upload widget (file picker), not text.
|
|
123
|
+
it('image/media/file resuelven a upload', () => {
|
|
124
|
+
expect(resolveWidget({ key: 'k', label: 'L', type: 'image' })).toBe('upload')
|
|
125
|
+
expect(resolveWidget({ key: 'k', label: 'L', type: 'media' })).toBe('upload')
|
|
126
|
+
expect(resolveWidget({ key: 'k', label: 'L', type: 'file' })).toBe('upload')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('widget explícito sigue ganando incluso con ref', () => {
|
|
130
|
+
expect(resolveWidget({ key: 'k', label: 'L', type: 'string', ref: 'product', widget: 'textarea' })).toBe('textarea')
|
|
131
|
+
})
|
|
104
132
|
})
|
|
105
133
|
|
|
106
134
|
describe('line-items (repeatable group)', () => {
|
|
@@ -112,8 +112,17 @@ export function ActionModalDispatcher({
|
|
|
112
112
|
return null
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function buildActionUrl(endpoint: string | undefined, model: string, recordId: string, actionKey: string) {
|
|
116
|
-
|
|
115
|
+
function buildActionUrl(endpoint: string | undefined, model: string, recordId: string | undefined, actionKey: string) {
|
|
116
|
+
// A create-placement (collection) action has no record yet, so `recordId`
|
|
117
|
+
// is undefined. Omit the `/{id}` segment so the request hits the collection
|
|
118
|
+
// route (`/data/:model/me/action/:action`) instead of the per-record route
|
|
119
|
+
// (`/data/:model/me/:id/action/:action`), which would reject the literal
|
|
120
|
+
// "undefined" as an invalid record ID (ops dynamic.go ExecuteAction → 400).
|
|
121
|
+
const hasRecord = recordId != null && recordId !== '' && recordId !== 'undefined'
|
|
122
|
+
if (endpoint) {
|
|
123
|
+
return hasRecord ? `${endpoint}/${recordId}/action/${actionKey}` : `${endpoint}/action/${actionKey}`
|
|
124
|
+
}
|
|
125
|
+
return hasRecord ? `/data/${model}/me/${recordId}/action/${actionKey}` : `/data/${model}/me/action/${actionKey}`
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }: ActionModalProps) {
|
|
@@ -207,6 +207,12 @@ function fieldToZod(field: ActionFieldDef): ZodTypeAny {
|
|
|
207
207
|
// same render as before).
|
|
208
208
|
export function resolveWidget(field: ActionFieldDef): string {
|
|
209
209
|
if (field.widget) return field.widget
|
|
210
|
+
// S1: any field that declares an FK target (`ref`, or the snake_case
|
|
211
|
+
// `source`/`relation` the kernel may serve) renders as an async searchable
|
|
212
|
+
// single-select — NOT a raw text input. This wins over the `type` switch so
|
|
213
|
+
// a declared FK column is a picker regardless of its SQL column type
|
|
214
|
+
// (uuid/text/etc), matching the kernel's option-resolution semantics.
|
|
215
|
+
if (fieldHasRef(field)) return 'dynamic_select'
|
|
210
216
|
switch (field.type) {
|
|
211
217
|
case 'textarea': return 'textarea'
|
|
212
218
|
case 'select': return 'select'
|
|
@@ -219,10 +225,33 @@ export function resolveWidget(field: ActionFieldDef): string {
|
|
|
219
225
|
// File upload: POSTs to the host upload endpoint and stores the returned
|
|
220
226
|
// file url/path as the field value. Rendered by `UploadField`.
|
|
221
227
|
case 'upload': return 'upload'
|
|
228
|
+
// S2: media-bearing types resolve to the upload widget so an `image`
|
|
229
|
+
// (logo/photo) or generic `file`/`media` field gets a real file picker
|
|
230
|
+
// instead of a free-text input.
|
|
231
|
+
case 'image': return 'upload'
|
|
232
|
+
case 'media': return 'upload'
|
|
233
|
+
case 'file': return 'upload'
|
|
222
234
|
default: return 'text'
|
|
223
235
|
}
|
|
224
236
|
}
|
|
225
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Resolves a field's FK target, tolerating the camelCase `ref` (authored SDK
|
|
240
|
+
* shape) and the snake_case `source` / `relation` aliases the kernel manifest
|
|
241
|
+
* may serve for a belongs_to column. Returns the trimmed model key, or
|
|
242
|
+
* `undefined` when the field declares no relation.
|
|
243
|
+
*/
|
|
244
|
+
export function getFieldRef(field: ActionFieldDef): string | undefined {
|
|
245
|
+
const ref = field.ref ?? field.source ?? field.relation
|
|
246
|
+
if (typeof ref === 'string' && ref.trim() !== '') return ref.trim()
|
|
247
|
+
return undefined
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** True when a field declares an FK target the SDK can resolve options against. */
|
|
251
|
+
export function fieldHasRef(field: ActionFieldDef): boolean {
|
|
252
|
+
return getFieldRef(field) !== undefined
|
|
253
|
+
}
|
|
254
|
+
|
|
226
255
|
/**
|
|
227
256
|
* Normalizes an upload field's config, tolerating both the camelCase authored
|
|
228
257
|
* SDK shape and the snake_case the kernel serves (`max_size`, `storage_path`).
|
|
@@ -34,10 +34,48 @@ import {
|
|
|
34
34
|
PopoverContent,
|
|
35
35
|
PopoverTrigger,
|
|
36
36
|
} from '@asteby/metacore-ui/primitives'
|
|
37
|
-
import { Check, ChevronsUpDown, Loader2, Plus } from 'lucide-react'
|
|
37
|
+
import { Check, ChevronsUpDown, ImageIcon, Loader2, Plus } from 'lucide-react'
|
|
38
38
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
39
|
+
import { getFieldRef } from './dynamic-form-schema'
|
|
39
40
|
import type { ActionFieldDef } from './types'
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Small square thumbnail for an option's `image`. Falls back to a neutral
|
|
44
|
+
* placeholder icon when the option has no image so rows/triggers stay aligned.
|
|
45
|
+
* `size` is in pixels (kept small — 20–24px — so the picker reads as a list,
|
|
46
|
+
* not a gallery). Inline style for the box dimensions: arbitrary Tailwind
|
|
47
|
+
* classes from a federated addon don't always survive the host's class scan.
|
|
48
|
+
*/
|
|
49
|
+
function OptionThumb({ image, size = 20 }: { image?: string | null; size?: number }) {
|
|
50
|
+
const box = { width: size, height: size }
|
|
51
|
+
if (!image) {
|
|
52
|
+
return (
|
|
53
|
+
<span
|
|
54
|
+
className="text-muted-foreground bg-muted flex shrink-0 items-center justify-center rounded-sm"
|
|
55
|
+
style={box}
|
|
56
|
+
aria-hidden
|
|
57
|
+
>
|
|
58
|
+
<ImageIcon className="size-3 opacity-60" />
|
|
59
|
+
</span>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
return (
|
|
63
|
+
<img
|
|
64
|
+
src={image}
|
|
65
|
+
alt=""
|
|
66
|
+
aria-hidden
|
|
67
|
+
loading="lazy"
|
|
68
|
+
className="shrink-0 rounded-sm object-cover"
|
|
69
|
+
style={box}
|
|
70
|
+
// A broken image url shouldn't leave a torn-icon glyph; collapse to
|
|
71
|
+
// the neutral placeholder background instead.
|
|
72
|
+
onError={(e) => {
|
|
73
|
+
e.currentTarget.style.visibility = 'hidden'
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
41
79
|
function useDebounced<T>(value: T, ms: number): T {
|
|
42
80
|
const [debounced, setDebounced] = useState(value)
|
|
43
81
|
useEffect(() => {
|
|
@@ -61,13 +99,17 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
61
99
|
// shows a name (not a UUID) without a round-trip.
|
|
62
100
|
const [picked, setPicked] = useState<ResolvedOption | null>(null)
|
|
63
101
|
|
|
102
|
+
// Tolerate the snake_case `source`/`relation` aliases the kernel may serve
|
|
103
|
+
// for the FK target, not just camelCase `ref`.
|
|
104
|
+
const fieldRef = getFieldRef(field)
|
|
105
|
+
|
|
64
106
|
const { options, loading } = useOptionsResolver({
|
|
65
107
|
modelKey: '',
|
|
66
108
|
fieldKey: 'id',
|
|
67
|
-
ref:
|
|
109
|
+
ref: fieldRef,
|
|
68
110
|
// searchEndpoint only drives the URL when there's no ref — ref is the
|
|
69
111
|
// canonical, kernel-derived path and wins.
|
|
70
|
-
endpoint:
|
|
112
|
+
endpoint: fieldRef ? undefined : field.searchEndpoint,
|
|
71
113
|
query: debounced,
|
|
72
114
|
limit: 20,
|
|
73
115
|
// Don't fetch until the popover opens (and keep fetching as the query
|
|
@@ -75,10 +117,21 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
75
117
|
enabled: open,
|
|
76
118
|
})
|
|
77
119
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
120
|
+
// The currently-selected option, resolved either from what the user picked
|
|
121
|
+
// (cached in `picked`) or from the loaded page. Drives both the trigger
|
|
122
|
+
// label and its thumbnail.
|
|
123
|
+
const selectedOption =
|
|
124
|
+
(picked && String(picked.id) === String(value) ? picked : null) ??
|
|
125
|
+
options.find((o) => String(o.id) === String(value)) ??
|
|
126
|
+
null
|
|
127
|
+
|
|
128
|
+
const selectedLabel = selectedOption?.label ?? (value ? String(value) : '')
|
|
129
|
+
|
|
130
|
+
// Only switch the picker into "with thumbnails" mode when the data actually
|
|
131
|
+
// carries images — a relation whose options have no `image` keeps the plain
|
|
132
|
+
// text list it had before (no empty placeholder column).
|
|
133
|
+
const hasImages =
|
|
134
|
+
!!selectedOption?.image || options.some((o) => !!o.image)
|
|
82
135
|
|
|
83
136
|
const handlePick = (opt: ResolvedOption) => {
|
|
84
137
|
setPicked(opt)
|
|
@@ -93,11 +146,11 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
93
146
|
// hands back the new record and we select it immediately. No host import →
|
|
94
147
|
// no circular dependency; works for ANY dynamic_select with a `ref`.
|
|
95
148
|
const openCreate = () => {
|
|
96
|
-
if (!
|
|
149
|
+
if (!fieldRef || typeof window === 'undefined') return
|
|
97
150
|
window.dispatchEvent(
|
|
98
151
|
new CustomEvent('metacore:create-record', {
|
|
99
152
|
detail: {
|
|
100
|
-
model:
|
|
153
|
+
model: fieldRef,
|
|
101
154
|
onCreated: (rec: any) => {
|
|
102
155
|
if (rec && rec.id != null) {
|
|
103
156
|
const id = String(rec.id)
|
|
@@ -127,8 +180,13 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
127
180
|
className="min-w-0 flex-1 justify-between font-normal"
|
|
128
181
|
data-empty={!value}
|
|
129
182
|
>
|
|
130
|
-
<span className=
|
|
131
|
-
{
|
|
183
|
+
<span className="flex min-w-0 flex-1 items-center gap-2 text-left">
|
|
184
|
+
{hasImages && value ? (
|
|
185
|
+
<OptionThumb image={selectedOption?.image} size={20} />
|
|
186
|
+
) : null}
|
|
187
|
+
<span className={'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground')}>
|
|
188
|
+
{selectedLabel || field.placeholder || 'Buscar…'}
|
|
189
|
+
</span>
|
|
132
190
|
</span>
|
|
133
191
|
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
|
|
134
192
|
</Button>
|
|
@@ -168,8 +226,11 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
168
226
|
value={String(opt.id)}
|
|
169
227
|
onSelect={() => handlePick(opt)}
|
|
170
228
|
>
|
|
171
|
-
<Check className={'mr-2 size-4 ' + (isSel ? 'opacity-100' : 'opacity-0')} />
|
|
172
|
-
|
|
229
|
+
<Check className={'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0')} />
|
|
230
|
+
{hasImages && (
|
|
231
|
+
<OptionThumb image={opt.image} size={24} />
|
|
232
|
+
)}
|
|
233
|
+
<div className="ml-2 flex min-w-0 flex-col">
|
|
173
234
|
<span className="truncate">{opt.label}</span>
|
|
174
235
|
{opt.description && (
|
|
175
236
|
<span className="text-muted-foreground truncate text-xs">
|
|
@@ -186,15 +247,15 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
186
247
|
</Command>
|
|
187
248
|
</PopoverContent>
|
|
188
249
|
</Popover>
|
|
189
|
-
{
|
|
250
|
+
{fieldRef && (
|
|
190
251
|
<Button
|
|
191
252
|
type="button"
|
|
192
253
|
variant="outline"
|
|
193
254
|
size="icon"
|
|
194
255
|
className="size-9 shrink-0"
|
|
195
256
|
onClick={openCreate}
|
|
196
|
-
title={`Crear ${field.label ??
|
|
197
|
-
aria-label={`Crear ${field.label ??
|
|
257
|
+
title={`Crear ${field.label ?? fieldRef}`}
|
|
258
|
+
aria-label={`Crear ${field.label ?? fieldRef}`}
|
|
198
259
|
>
|
|
199
260
|
<Plus className="size-4" />
|
|
200
261
|
</Button>
|
package/src/types.ts
CHANGED
|
@@ -199,6 +199,14 @@ export interface ActionFieldDef {
|
|
|
199
199
|
* `useOptionsResolver` against `/api/options/<ref>?field=id`.
|
|
200
200
|
*/
|
|
201
201
|
ref?: string
|
|
202
|
+
/**
|
|
203
|
+
* snake_case aliases the kernel manifest may serve for a belongs_to FK
|
|
204
|
+
* target instead of `ref`. Treated as equivalent to `ref` by the SDK so a
|
|
205
|
+
* declared relation renders a searchable picker regardless of which key the
|
|
206
|
+
* backend emits.
|
|
207
|
+
*/
|
|
208
|
+
source?: string
|
|
209
|
+
relation?: string
|
|
202
210
|
/**
|
|
203
211
|
* Columns of a repeatable line-items group. Mirrors the kernel v3
|
|
204
212
|
* `ActionField.item_fields` (json `item_fields`). Present on a field
|