@asteby/metacore-runtime-react 18.22.0 → 18.23.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.23.0
4
+
5
+ ### Minor Changes
6
+
7
+ - dc5e552: Polish the generic record EDIT dialog (`DynamicRecordDialog` mode='edit'),
8
+ fixing two prod issues that affected every module using it (transfers, orders,
9
+ customers):
10
+ - **jsonb line-items render read-only instead of "[object Object]".** A field
11
+ that is a jsonb line-items column (declares `item_fields`, or its value is an
12
+ array/plain object) is no longer rendered as a broken text input. The edit
13
+ form now renders it read-only with the same inline `CollectionCell` table the
14
+ detail view uses — localized headers + resolved ref labels — plus a
15
+ translatable "Solo lectura" hint (`datatable.readOnly`). These are
16
+ action-built documents; field-by-field array editing stays out of scope.
17
+ - **FK selects show the related record's name, not the raw uuid.** A
18
+ `dynamic_select` / `ref` field in edit mode now seeds its trigger from the
19
+ backend-injected relation sibling (`source_warehouse_id` →
20
+ `source_warehouse: { value, label }`, the key without `_id`) via the existing
21
+ `DynamicSelectField` `seedOption` prop, so an existing selection displays the
22
+ label immediately without waiting for a lookup. Falls back to the raw value
23
+ (today's behaviour) when no sibling is present; creating/changing the
24
+ selection is unchanged.
25
+
26
+ `EditField`, `isLineItemsField`, and `fkSeedOption` are exported from the
27
+ dialogs module.
28
+
3
29
  ## 18.22.0
4
30
 
5
31
  ### Minor Changes
@@ -1,4 +1,5 @@
1
1
  import type { ModelSchema } from './types';
2
+ import { type ResolvedOption } from '../use-options-resolver';
2
3
  import { type ItemField } from '../collection-cell';
3
4
  import { type GetImageUrl } from '../image-url-context';
4
5
  export type { GetImageUrl };
@@ -158,6 +159,8 @@ export interface DynamicRecordDialogProps {
158
159
  */
159
160
  onChange?: () => void;
160
161
  }
162
+ export declare function isLineItemsField(field: FieldDef, value: any): boolean;
163
+ export declare function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null;
161
164
  export declare function isMoneyField(field: FieldDef, value: any): boolean;
162
165
  export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, onChange, }: DynamicRecordDialogProps): import("react").JSX.Element;
163
166
  export declare function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, currency: currencyProp, }: {
@@ -171,4 +174,11 @@ export declare function ViewValue({ field, value: rawValue, record, getImageUrl:
171
174
  /** Optional override; when omitted falls back to the nearest provider. */
172
175
  currency?: string;
173
176
  }): import("react").JSX.Element;
177
+ export declare function EditField({ field, value, onChange, record }: {
178
+ field: FieldDef;
179
+ value: any;
180
+ onChange: (val: any) => void;
181
+ /** The full record being edited — supplies FK relation siblings + line-items. */
182
+ record?: any;
183
+ }): import("react").JSX.Element;
174
184
  //# sourceMappingURL=dynamic-record.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
@@ -85,6 +85,51 @@ function relationSiblingValue(field, record) {
85
85
  }
86
86
  return undefined;
87
87
  }
88
+ // fieldItemFields reads the declared jsonb line-items schema off a field,
89
+ // tolerating the snake_case `item_fields` alias the kernel serves.
90
+ function fieldItemFields(field) {
91
+ return field.itemFields ?? field.item_fields;
92
+ }
93
+ // isLineItemsField — a jsonb line-items column (e.g. Transfer.items): it either
94
+ // declares an `item_fields` schema, or its value is a structured array/object.
95
+ // These are action-built documents; field-by-field editing of the array is out
96
+ // of scope, so the edit dialog renders them read-only (the inline table) rather
97
+ // than an input that would stringify to "[object Object]". Scalars and known
98
+ // editable widgets (media/upload, color, dates) are NOT line-items.
99
+ export function isLineItemsField(field, value) {
100
+ if (field.type === 'image' || field.widget === 'upload')
101
+ return false;
102
+ if (fieldItemFields(field)?.length)
103
+ return true;
104
+ if (Array.isArray(value))
105
+ return true;
106
+ return (value !== null &&
107
+ typeof value === 'object' &&
108
+ !(value instanceof Date));
109
+ }
110
+ // fkSeedOption builds the pre-resolved option for a FK select's CURRENT value
111
+ // from the relation sibling the backend injected alongside the column
112
+ // (`source_warehouse_id` → `source_warehouse = {value,label,image?}`, the key
113
+ // without `_id`; same convention as the jsonb refs). Lets the edit picker show
114
+ // the related record's NAME instead of the raw uuid without waiting for a
115
+ // network lookup. Returns null when there is no usable sibling (the picker then
116
+ // falls back to its existing typeahead/lookup behaviour).
117
+ export function fkSeedOption(field, value, record) {
118
+ if (value === undefined || value === null || value === '')
119
+ return null;
120
+ const sib = relationSiblingValue(field, record);
121
+ const label = typeof sib === 'string' ? sib : objectLabel(sib);
122
+ if (!label)
123
+ return null;
124
+ const id = String(value);
125
+ return {
126
+ id,
127
+ value: id,
128
+ label,
129
+ name: label,
130
+ image: pickImage(sib) ?? null,
131
+ };
132
+ }
88
133
  // servedOption matches a field's served option list (enum/select with
89
134
  // {value,label,color,icon,image}) against the current value.
90
135
  function servedOption(field, value) {
@@ -454,7 +499,7 @@ function LoadingSkeleton() {
454
499
  }
455
500
  function FieldRow({ field, record, value, mode, onChange }) {
456
501
  const isReadonly = field.readonly || mode === 'view';
457
- return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: [field.label, field.required && mode !== 'view' && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), isReadonly ? (_jsx(ViewValue, { field: field, value: value, record: record })) : (_jsx(EditField, { field: field, value: value, onChange: onChange }))] }));
502
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: [field.label, field.required && mode !== 'view' && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), isReadonly ? (_jsx(ViewValue, { field: field, value: value, record: record })) : (_jsx(EditField, { field: field, value: value, onChange: onChange, record: record }))] }));
458
503
  }
459
504
  // RelationViewValue — read-only FK lead. Resolves the relation's label + image
460
505
  // from (1) the sibling object the table served, then (2) the canonical options
@@ -628,7 +673,15 @@ function StructuredViewValue({ value, field, locale, t, }) {
628
673
  }
629
674
  return (_jsx("div", { className: "text-sm py-1", children: _jsx(CollectionCell, { value: value, itemFields: field?.itemFields ?? field?.item_fields, variant: "inline", locale: locale, t: t }) }));
630
675
  }
631
- function EditField({ field, value, onChange }) {
676
+ export function EditField({ field, value, onChange, record }) {
677
+ const { t, i18n } = useTranslation();
678
+ // Jsonb line-items columns (e.g. Transfer.items) are action-built documents:
679
+ // editing the array field-by-field is out of scope. Render them READ-ONLY
680
+ // with the same inline table the detail view uses — a localized, ref-resolved
681
+ // mini-table — instead of an input that stringifies to "[object Object]".
682
+ if (isLineItemsField(field, value)) {
683
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "rounded-md border bg-muted/30 p-2", children: _jsx(CollectionCell, { value: value, itemFields: fieldItemFields(field), variant: "inline", locale: i18n.language, t: t }) }), _jsx("p", { className: "text-[11px] text-muted-foreground", children: t('datatable.readOnly', { defaultValue: 'Solo lectura' }) })] }));
684
+ }
632
685
  if (field.type === 'boolean') {
633
686
  return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, onCheckedChange: onChange }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
634
687
  }
@@ -645,7 +698,12 @@ function EditField({ field, value, onChange }) {
645
698
  // option thumbnails and the inline-create "+" — against /api/options/<ref>.
646
699
  // Static inline `options` are handled by the enum <Select> branch below.
647
700
  if ((getFieldRef(field) || field.widget === 'dynamic_select') && !field.options?.length) {
648
- return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
701
+ return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange,
702
+ // Seed the trigger with the related record's NAME (from the
703
+ // backend-injected FK sibling, key without `_id`) so an existing
704
+ // selection shows the label, not the raw uuid — without waiting
705
+ // for the popover to open and fetch a page.
706
+ seedOption: fkSeedOption(field, value, record) }));
649
707
  }
650
708
  if (field.type === 'search' && field.searchEndpoint) {
651
709
  return _jsx(SearchField, { field: field, value: value, onChange: onChange });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.22.0",
3
+ "version": "18.23.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,8 +64,8 @@
64
64
  "typescript": "^6.0.0",
65
65
  "vitest": "^4.0.0",
66
66
  "zustand": "^5.0.0",
67
- "@asteby/metacore-ui": "2.5.2",
68
- "@asteby/metacore-sdk": "3.2.0"
67
+ "@asteby/metacore-sdk": "3.2.0",
68
+ "@asteby/metacore-ui": "2.5.2"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,170 @@
1
+ // @vitest-environment happy-dom
2
+ //
3
+ // Record EDIT dialog polish (mode='edit'), affecting every module that uses the
4
+ // generic dialog (transfers, orders, customers):
5
+ // PART 1 — a jsonb line-items field renders READ-ONLY as the inline table
6
+ // (the detail-view renderer), never an input that stringifies to
7
+ // "[object Object]".
8
+ // PART 2 — a FK select seeds its trigger from the backend-injected relation
9
+ // sibling (`source_warehouse_id` → `source_warehouse: {value,label}`)
10
+ // so an existing selection shows the related record's NAME, not the
11
+ // raw uuid.
12
+ import { afterEach, describe, expect, it, vi } from 'vitest'
13
+ import { cleanup, render, screen } from '@testing-library/react'
14
+
15
+ // Identity translator (so `defaultValue` surfaces) + Spanish locale.
16
+ vi.mock('react-i18next', () => ({
17
+ useTranslation: () => ({
18
+ t: (k: string, o?: any) => o?.defaultValue ?? k,
19
+ i18n: { language: 'es' },
20
+ }),
21
+ }))
22
+
23
+ import { EditField, isLineItemsField, fkSeedOption } from '../dialogs/dynamic-record'
24
+ import { ApiProvider, type ApiClient } from '../api-context'
25
+
26
+ afterEach(cleanup)
27
+
28
+ const noopApi = { get: vi.fn(async () => ({ data: { data: [] } })) } as unknown as ApiClient
29
+
30
+ describe('isLineItemsField', () => {
31
+ it('matches a field with a declared item_fields schema', () => {
32
+ expect(
33
+ isLineItemsField(
34
+ { key: 'items', label: 'Items', type: 'json', itemFields: [{ key: 'q', label: 'Q' }] },
35
+ null,
36
+ ),
37
+ ).toBe(true)
38
+ // snake_case alias
39
+ expect(
40
+ isLineItemsField(
41
+ { key: 'items', label: 'Items', type: 'json', item_fields: [{ key: 'q', label: 'Q' }] },
42
+ null,
43
+ ),
44
+ ).toBe(true)
45
+ })
46
+
47
+ it('matches any array / plain-object value (would stringify to [object Object])', () => {
48
+ expect(isLineItemsField({ key: 'items', label: 'Items', type: 'json' }, [{ a: 1 }])).toBe(true)
49
+ expect(isLineItemsField({ key: 'data', label: 'Data', type: 'json' }, { a: 1 })).toBe(true)
50
+ })
51
+
52
+ it('does NOT match scalars or dedicated editable widgets', () => {
53
+ expect(isLineItemsField({ key: 'name', label: 'Name', type: 'text' }, 'hi')).toBe(false)
54
+ expect(isLineItemsField({ key: 'n', label: 'N', type: 'number' }, 5)).toBe(false)
55
+ expect(isLineItemsField({ key: 'photo', label: 'Photo', type: 'image' }, { url: 'x' })).toBe(false)
56
+ expect(isLineItemsField({ key: 'f', label: 'F', type: 'text', widget: 'upload' }, {})).toBe(false)
57
+ expect(isLineItemsField({ key: 'when', label: 'When', type: 'date' }, new Date())).toBe(false)
58
+ })
59
+ })
60
+
61
+ describe('fkSeedOption', () => {
62
+ const field = { key: 'source_warehouse_id', label: 'Origen', type: 'text', ref: 'warehouses' }
63
+
64
+ it('builds a seed option from the injected {value,label} sibling', () => {
65
+ const seed = fkSeedOption(field, 'wh-1', {
66
+ source_warehouse_id: 'wh-1',
67
+ source_warehouse: { value: 'wh-1', label: 'Test2', image: 'logo.png' },
68
+ })
69
+ expect(seed).toEqual({ id: 'wh-1', value: 'wh-1', label: 'Test2', name: 'Test2', image: 'logo.png' })
70
+ })
71
+
72
+ it('returns null when there is no usable sibling', () => {
73
+ expect(fkSeedOption(field, 'wh-1', { source_warehouse_id: 'wh-1' })).toBeNull()
74
+ expect(fkSeedOption(field, '', {})).toBeNull()
75
+ expect(fkSeedOption(field, null, { source_warehouse: { label: 'x' } })).toBeNull()
76
+ })
77
+ })
78
+
79
+ describe('EditField — PART 1: jsonb line-items read-only', () => {
80
+ it('renders the inline table (read-only), not an input or [object Object]', () => {
81
+ const onChange = vi.fn()
82
+ const { container } = render(
83
+ <ApiProvider client={noopApi}>
84
+ <EditField
85
+ field={{
86
+ key: 'items',
87
+ label: 'Items',
88
+ type: 'json',
89
+ itemFields: [
90
+ { key: 'product_id', label: 'Producto', ref: 'Product' },
91
+ { key: 'quantity', label: 'Cantidad' },
92
+ ],
93
+ }}
94
+ value={[
95
+ {
96
+ product_id: '550e8400-e29b-41d4-a716-446655440000',
97
+ product: { value: 'x', label: 'Test' },
98
+ quantity: 10,
99
+ },
100
+ ]}
101
+ onChange={onChange}
102
+ record={{}}
103
+ />
104
+ </ApiProvider>,
105
+ )
106
+ // Localized headers + resolved ref label from the inline CollectionCell.
107
+ expect(screen.getByRole('columnheader', { name: 'Producto' })).toBeTruthy()
108
+ expect(screen.getByRole('cell', { name: 'Test' })).toBeTruthy()
109
+ expect(screen.getByRole('cell', { name: '10' })).toBeTruthy()
110
+ // No editable input, no [object Object], no leaked uuid, read-only hint.
111
+ expect(container.querySelector('input')).toBeNull()
112
+ expect(container.querySelector('textarea')).toBeNull()
113
+ expect(container.textContent).not.toContain('[object Object]')
114
+ expect(container.textContent).not.toContain('550e8400-e29b')
115
+ expect(screen.getByText('Solo lectura')).toBeTruthy()
116
+ })
117
+
118
+ it('renders a bare jsonb object read-only (no schema) instead of [object Object]', () => {
119
+ const { container } = render(
120
+ <ApiProvider client={noopApi}>
121
+ <EditField
122
+ field={{ key: 'fiscal_data', label: 'Fiscal', type: 'json' }}
123
+ value={{ price: 10, quantity: 20 }}
124
+ onChange={vi.fn()}
125
+ record={{}}
126
+ />
127
+ </ApiProvider>,
128
+ )
129
+ expect(container.querySelector('input')).toBeNull()
130
+ expect(container.textContent).not.toContain('[object Object]')
131
+ expect(screen.getByText('Precio:')).toBeTruthy()
132
+ })
133
+ })
134
+
135
+ describe('EditField — PART 2: FK select shows the resolved label', () => {
136
+ it('seeds the trigger with the sibling label, not the raw uuid', () => {
137
+ render(
138
+ <ApiProvider client={noopApi}>
139
+ <EditField
140
+ field={{ key: 'source_warehouse_id', label: 'Origen', type: 'text', ref: 'warehouses' }}
141
+ value={'550e8400-e29b-41d4-a716-446655440000'}
142
+ onChange={vi.fn()}
143
+ record={{
144
+ source_warehouse_id: '550e8400-e29b-41d4-a716-446655440000',
145
+ source_warehouse: { value: '550e8400-e29b-41d4-a716-446655440000', label: 'Test2' },
146
+ }}
147
+ />
148
+ </ApiProvider>,
149
+ )
150
+ // The combobox trigger shows the resolved label.
151
+ expect(screen.getByText('Test2')).toBeTruthy()
152
+ // The raw uuid is not shown.
153
+ expect(screen.queryByText('550e8400-e29b-41d4-a716-446655440000')).toBeNull()
154
+ })
155
+
156
+ it('keeps the raw value as fallback when no sibling was injected', () => {
157
+ render(
158
+ <ApiProvider client={noopApi}>
159
+ <EditField
160
+ field={{ key: 'source_warehouse_id', label: 'Origen', type: 'text', ref: 'warehouses' }}
161
+ value={'wh-xyz'}
162
+ onChange={vi.fn()}
163
+ record={{ source_warehouse_id: 'wh-xyz' }}
164
+ />
165
+ </ApiProvider>,
166
+ )
167
+ // No sibling → existing behaviour: the raw value is shown on the trigger.
168
+ expect(screen.getByText('wh-xyz')).toBeTruthy()
169
+ })
170
+ })
@@ -294,6 +294,51 @@ function relationSiblingValue(field: FieldDef, record: any): any {
294
294
  return undefined
295
295
  }
296
296
 
297
+ // fieldItemFields reads the declared jsonb line-items schema off a field,
298
+ // tolerating the snake_case `item_fields` alias the kernel serves.
299
+ function fieldItemFields(field: FieldDef): ItemField[] | undefined {
300
+ return field.itemFields ?? field.item_fields
301
+ }
302
+
303
+ // isLineItemsField — a jsonb line-items column (e.g. Transfer.items): it either
304
+ // declares an `item_fields` schema, or its value is a structured array/object.
305
+ // These are action-built documents; field-by-field editing of the array is out
306
+ // of scope, so the edit dialog renders them read-only (the inline table) rather
307
+ // than an input that would stringify to "[object Object]". Scalars and known
308
+ // editable widgets (media/upload, color, dates) are NOT line-items.
309
+ export function isLineItemsField(field: FieldDef, value: any): boolean {
310
+ if (field.type === 'image' || field.widget === 'upload') return false
311
+ if (fieldItemFields(field)?.length) return true
312
+ if (Array.isArray(value)) return true
313
+ return (
314
+ value !== null &&
315
+ typeof value === 'object' &&
316
+ !(value instanceof Date)
317
+ )
318
+ }
319
+
320
+ // fkSeedOption builds the pre-resolved option for a FK select's CURRENT value
321
+ // from the relation sibling the backend injected alongside the column
322
+ // (`source_warehouse_id` → `source_warehouse = {value,label,image?}`, the key
323
+ // without `_id`; same convention as the jsonb refs). Lets the edit picker show
324
+ // the related record's NAME instead of the raw uuid without waiting for a
325
+ // network lookup. Returns null when there is no usable sibling (the picker then
326
+ // falls back to its existing typeahead/lookup behaviour).
327
+ export function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null {
328
+ if (value === undefined || value === null || value === '') return null
329
+ const sib = relationSiblingValue(field, record)
330
+ const label = typeof sib === 'string' ? sib : objectLabel(sib)
331
+ if (!label) return null
332
+ const id = String(value)
333
+ return {
334
+ id,
335
+ value: id,
336
+ label,
337
+ name: label,
338
+ image: pickImage(sib) ?? null,
339
+ }
340
+ }
341
+
297
342
  // servedOption matches a field's served option list (enum/select with
298
343
  // {value,label,color,icon,image}) against the current value.
299
344
  function servedOption(field: FieldDef, value: any): FieldOption | undefined {
@@ -846,7 +891,7 @@ function FieldRow({ field, record, value, mode, onChange }: FieldRowProps) {
846
891
  {isReadonly ? (
847
892
  <ViewValue field={field} value={value} record={record} />
848
893
  ) : (
849
- <EditField field={field} value={value} onChange={onChange} />
894
+ <EditField field={field} value={value} onChange={onChange} record={record} />
850
895
  )}
851
896
  </div>
852
897
  )
@@ -1161,11 +1206,38 @@ function StructuredViewValue({
1161
1206
  )
1162
1207
  }
1163
1208
 
1164
- function EditField({ field, value, onChange }: {
1209
+ export function EditField({ field, value, onChange, record }: {
1165
1210
  field: FieldDef
1166
1211
  value: any
1167
1212
  onChange: (val: any) => void
1213
+ /** The full record being edited — supplies FK relation siblings + line-items. */
1214
+ record?: any
1168
1215
  }) {
1216
+ const { t, i18n } = useTranslation()
1217
+
1218
+ // Jsonb line-items columns (e.g. Transfer.items) are action-built documents:
1219
+ // editing the array field-by-field is out of scope. Render them READ-ONLY
1220
+ // with the same inline table the detail view uses — a localized, ref-resolved
1221
+ // mini-table — instead of an input that stringifies to "[object Object]".
1222
+ if (isLineItemsField(field, value)) {
1223
+ return (
1224
+ <div className="space-y-1">
1225
+ <div className="rounded-md border bg-muted/30 p-2">
1226
+ <CollectionCell
1227
+ value={value}
1228
+ itemFields={fieldItemFields(field)}
1229
+ variant="inline"
1230
+ locale={i18n.language}
1231
+ t={t}
1232
+ />
1233
+ </div>
1234
+ <p className="text-[11px] text-muted-foreground">
1235
+ {t('datatable.readOnly', { defaultValue: 'Solo lectura' })}
1236
+ </p>
1237
+ </div>
1238
+ )
1239
+ }
1240
+
1169
1241
  if (field.type === 'boolean') {
1170
1242
  return (
1171
1243
  <div className="flex items-center gap-2 py-1">
@@ -1202,7 +1274,18 @@ function EditField({ field, value, onChange }: {
1202
1274
  // option thumbnails and the inline-create "+" — against /api/options/<ref>.
1203
1275
  // Static inline `options` are handled by the enum <Select> branch below.
1204
1276
  if ((getFieldRef(field as ActionFieldDef) || field.widget === 'dynamic_select') && !field.options?.length) {
1205
- return <DynamicSelectField field={field as ActionFieldDef} value={value} onChange={onChange} />
1277
+ return (
1278
+ <DynamicSelectField
1279
+ field={field as ActionFieldDef}
1280
+ value={value}
1281
+ onChange={onChange}
1282
+ // Seed the trigger with the related record's NAME (from the
1283
+ // backend-injected FK sibling, key without `_id`) so an existing
1284
+ // selection shows the label, not the raw uuid — without waiting
1285
+ // for the popover to open and fetch a page.
1286
+ seedOption={fkSeedOption(field, value, record)}
1287
+ />
1288
+ )
1206
1289
  }
1207
1290
 
1208
1291
  if (field.type === 'search' && field.searchEndpoint) {