@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 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
- return endpoint ? `${endpoint}/${recordId}/action/${actionKey}` : `/data/${model}/me/${recordId}/action/${actionKey}`;
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,CAgB3D;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"}
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":"AAsCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAW7C,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,2CAoJrF;AAED,eAAe,kBAAkB,CAAA"}
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: field.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: field.ref ? undefined : field.searchEndpoint,
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
- const selectedLabel = (picked && String(picked.id) === String(value) ? picked.label : null) ??
58
- options.find((o) => String(o.id) === String(value))?.label ??
59
- (value ? String(value) : '');
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 (!field.ref || typeof window === 'undefined')
103
+ if (!fieldRef || typeof window === 'undefined')
73
104
  return;
74
105
  window.dispatchEvent(new CustomEvent('metacore:create-record', {
75
106
  detail: {
76
- model: field.ref,
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: [_jsx("span", { className: 'min-w-0 flex-1 truncate text-left ' + (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",
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
- }) }))] })] }) })] }), field.ref && (_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: openCreate, title: `Crear ${field.label ?? field.ref}`, "aria-label": `Crear ${field.label ?? field.ref}`, children: _jsx(Plus, { className: "size-4" }) }))] }));
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
@@ -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.9.0",
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.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.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
- return endpoint ? `${endpoint}/${recordId}/action/${actionKey}` : `/data/${model}/me/${recordId}/action/${actionKey}`
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: field.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: field.ref ? undefined : field.searchEndpoint,
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
- const selectedLabel =
79
- (picked && String(picked.id) === String(value) ? picked.label : null) ??
80
- options.find((o) => String(o.id) === String(value))?.label ??
81
- (value ? String(value) : '')
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 (!field.ref || typeof window === 'undefined') return
149
+ if (!fieldRef || typeof window === 'undefined') return
97
150
  window.dispatchEvent(
98
151
  new CustomEvent('metacore:create-record', {
99
152
  detail: {
100
- model: field.ref,
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={'min-w-0 flex-1 truncate text-left ' + (selectedLabel ? '' : 'text-muted-foreground')}>
131
- {selectedLabel || field.placeholder || 'Buscar…'}
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
- <div className="flex min-w-0 flex-col">
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
- {field.ref && (
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 ?? field.ref}`}
197
- aria-label={`Crear ${field.label ?? field.ref}`}
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