@asteby/metacore-runtime-react 24.0.0 → 24.0.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,21 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 24.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 4a62c3c: Preview automático del registro en el modal de confirmación de acciones de fila.
8
+
9
+ Al confirmar una row-action con `confirm` (p. ej. aceptar/rechazar un traspaso), el
10
+ `ConfirmActionDialog` ahora muestra un resumen compacto y de solo lectura del registro
11
+ que va a afectar, para no confirmar a ciegas. Es 100% del SDK y genérico: se apoya en la
12
+ metadata de tabla del modelo (labels + display hints, leída del cache o con un único fetch
13
+ a `/metadata/table/<model>`) y en los siblings de relación que la tabla ya resolvió sobre
14
+ la fila. La heurística surface relaciones resueltas a su label, campos line-items (jsonb,
15
+ como `Transfer.items`, renderizados producto × cantidad) y un puñado de escalares de
16
+ identidad; omite `id`, `organization_id`, timestamps y los `*_id` crudos sin label. Se
17
+ degrada solo: si no hay nada útil que mostrar, no renderiza la sección.
18
+
3
19
  ## 24.0.0
4
20
 
5
21
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AAgDA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AA0FhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCA+DlB"}
1
+ {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AAwDA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AA0FhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCA+DlB"}
@@ -19,8 +19,10 @@ import { DynamicRelations } from './dynamic-relations';
19
19
  import { DynamicSelectField } from './dynamic-select-field';
20
20
  import { DynamicDateField } from './dynamic-date-field';
21
21
  import { UploadField } from './upload-field';
22
- import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn } from './dynamic-form-schema';
22
+ import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn, getFieldRef } from './dynamic-form-schema';
23
23
  import { FieldGrid, FieldCell, FieldLabel } from './field-grid';
24
+ import { useMetadataCache } from './metadata-cache';
25
+ import { ViewValue, objectLabel, relationSiblingValue, isLineItemsField as isRecordLineItemsField, } from './dialogs/dynamic-record';
24
26
  // Canonical registry lives in @asteby/metacore-sdk
25
27
  import { getActionComponent, } from '@asteby/metacore-sdk';
26
28
  function isPrefillSpec(v) {
@@ -110,6 +112,143 @@ function buildActionUrl(endpoint, model, recordId, actionKey) {
110
112
  }
111
113
  return hasRecord ? `/data/${model}/me/${recordId}/action/${actionKey}` : `/data/${model}/me/action/${actionKey}`;
112
114
  }
115
+ // ── Preview automático del registro en el modal de confirmación ─────────────
116
+ //
117
+ // Antes de confirmar una row-action (aceptar/rechazar un traspaso, etc.) el
118
+ // usuario ve un resumen compacto y read-only de QUÉ registro va a afectar, para
119
+ // no confirmar a ciegas. Es 100% del SDK y genérico: se apoya en la metadata de
120
+ // tabla del modelo (labels + display hints) y en los siblings de relación que la
121
+ // tabla ya resolvió sobre el `record`. Se degrada solo — si no hay ni un campo
122
+ // ni líneas útiles que mostrar, no renderiza nada (ni una caja vacía).
123
+ // Claves de sistema/infra que nunca aportan contexto al preview.
124
+ const PREVIEW_SKIP_KEYS = new Set([
125
+ 'id',
126
+ 'organization_id',
127
+ 'org_id',
128
+ 'created_at',
129
+ 'updated_at',
130
+ 'deleted_at',
131
+ 'created_by',
132
+ 'created_by_id',
133
+ 'updated_by',
134
+ 'updated_by_id',
135
+ ]);
136
+ // Campos escalares "de identidad" que sí vale la pena anclar en el preview
137
+ // aunque no sean relación ni line-items (nombre, folio, estado, etc.).
138
+ const PREVIEW_INTEREST_KEYS = new Set([
139
+ 'name',
140
+ 'title',
141
+ 'code',
142
+ 'reference',
143
+ 'folio',
144
+ 'number',
145
+ 'status',
146
+ 'stage',
147
+ 'state',
148
+ ]);
149
+ const PREVIEW_INTEREST_STYLES = new Set(['status', 'badge', 'currency']);
150
+ const MAX_PREVIEW_FIELD_ROWS = 6;
151
+ function isEmptyPreviewValue(value) {
152
+ if (value === null || value === undefined || value === '')
153
+ return true;
154
+ if (Array.isArray(value))
155
+ return value.length === 0;
156
+ if (typeof value === 'object' && !(value instanceof Date))
157
+ return Object.keys(value).length === 0;
158
+ return false;
159
+ }
160
+ // selectPreviewColumns aplica la heurística compacta sobre las columnas del
161
+ // modelo: relaciones resueltas a su label, campos line-items (jsonb) y un puñado
162
+ // de escalares de identidad. Omite id/org/timestamps y los *_id crudos cuya
163
+ // relación no resolvió a un label legible.
164
+ function selectPreviewColumns(columns, record) {
165
+ if (!columns || !record)
166
+ return [];
167
+ const lineItemRows = [];
168
+ const fieldRows = [];
169
+ for (const col of columns) {
170
+ if (!col || !col.key)
171
+ continue;
172
+ if (col.hidden)
173
+ continue;
174
+ if (PREVIEW_SKIP_KEYS.has(col.key))
175
+ continue;
176
+ const value = record[col.key];
177
+ // Line-items (jsonb array como Transfer.items) → mini-tabla producto×cantidad.
178
+ if (isRecordLineItemsField(col, value)) {
179
+ if (isEmptyPreviewValue(value))
180
+ continue;
181
+ lineItemRows.push({ col, value, lineItems: true });
182
+ continue;
183
+ }
184
+ // Relación (ref / search / dynamic_select / *_id): solo si el sibling
185
+ // resolvió a un label legible; si es un *_id crudo sin resolver, se omite.
186
+ const isRelation = !!getFieldRef(col) ||
187
+ col.type === 'search' ||
188
+ col.type === 'relation' ||
189
+ col.widget === 'dynamic_select' ||
190
+ (typeof col.key === 'string' && col.key.endsWith('_id'));
191
+ if (isRelation) {
192
+ const sib = relationSiblingValue(col, record);
193
+ const label = typeof sib === 'string' ? sib : objectLabel(sib);
194
+ if (!label)
195
+ continue;
196
+ fieldRows.push({ col, value, lineItems: false });
197
+ continue;
198
+ }
199
+ // Escalar de identidad (nombre/folio/estado/moneda…) con valor.
200
+ const styleKey = col.cellStyle ?? col.type;
201
+ const interesting = PREVIEW_INTEREST_KEYS.has(col.key) || PREVIEW_INTEREST_STYLES.has(String(styleKey));
202
+ if (interesting && !isEmptyPreviewValue(value)) {
203
+ fieldRows.push({ col, value, lineItems: false });
204
+ }
205
+ }
206
+ return [...fieldRows.slice(0, MAX_PREVIEW_FIELD_ROWS), ...lineItemRows];
207
+ }
208
+ function RecordPreview({ model, record }) {
209
+ const { t } = useTranslation();
210
+ const api = useApi();
211
+ const cached = useMetadataCache((s) => s.getMetadata(model));
212
+ const setMetadata = useMetadataCache((s) => s.setMetadata);
213
+ const [fetched, setFetched] = useState(null);
214
+ // Sin metadata cacheada → UN fetch a /metadata/table/<model> (mismo patrón
215
+ // que model-action-toolbar). Se guarda en el store para próximos usos.
216
+ useEffect(() => {
217
+ if (cached || !model)
218
+ return;
219
+ let cancelled = false;
220
+ api
221
+ .get(`/metadata/table/${model}`)
222
+ .then((res) => {
223
+ if (cancelled)
224
+ return;
225
+ const meta = (res.data?.data ?? res.data);
226
+ if (meta && Array.isArray(meta.columns)) {
227
+ setFetched(meta);
228
+ setMetadata(model, meta);
229
+ }
230
+ })
231
+ .catch(() => {
232
+ if (!cancelled)
233
+ setFetched(null);
234
+ });
235
+ return () => {
236
+ cancelled = true;
237
+ };
238
+ }, [cached, model, api, setMetadata]);
239
+ const meta = cached ?? fetched;
240
+ const rows = useMemo(() => selectPreviewColumns(meta?.columns, record), [meta, record]);
241
+ // Degradación: nada útil que mostrar → no renderiza la sección.
242
+ if (rows.length === 0)
243
+ return null;
244
+ return (_jsx("div", { className: "mt-1 overflow-hidden rounded-md border bg-muted/30 text-sm", children: rows.map(({ col, value, lineItems }) => {
245
+ const label = t(col.label, { defaultValue: col.label });
246
+ if (lineItems) {
247
+ return (_jsxs("div", { className: "border-t px-3 py-2 first:border-t-0", children: [_jsx("div", { className: "mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: label }), _jsx(ViewValue, { field: col, value: value, record: record })] }, col.key));
248
+ }
249
+ return (_jsxs("div", { className: "flex items-baseline gap-3 border-t px-3 py-1.5 first:border-t-0", children: [_jsx("span", { className: "w-1/3 shrink-0 truncate text-muted-foreground", children: label }), _jsx("div", { className: "min-w-0 flex-1 [&_p]:py-0", children: _jsx(ViewValue, { field: col, value: value, record: record }) })] }, col.key));
250
+ }) }));
251
+ }
113
252
  function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
114
253
  const { t } = useTranslation();
115
254
  const api = useApi();
@@ -139,7 +278,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
139
278
  setExecuting(false);
140
279
  }
141
280
  };
142
- return (_jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) }));
281
+ return (_jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), record ? _jsx(RecordPreview, { model: model, record: record }) : null, _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) }));
143
282
  }
144
283
  function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
145
284
  const { t } = useTranslation();
@@ -159,6 +159,9 @@ export interface DynamicRecordDialogProps {
159
159
  */
160
160
  onChange?: () => void;
161
161
  }
162
+ export declare function objectLabel(value: any): string | undefined;
163
+ export declare function relationSiblingValue(field: FieldDef, record: any): any;
164
+ export declare function fieldItemFields(field: FieldDef): ItemField[] | undefined;
162
165
  export declare function isLineItemsField(field: FieldDef, value: any): boolean;
163
166
  export declare function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null;
164
167
  export declare function isMoneyField(field: FieldDef, value: any): boolean;
@@ -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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"}
@@ -47,7 +47,7 @@ function resolvePath(obj, path) {
47
47
  // objectLabel pulls a human label off a resolved relation/user object the
48
48
  // backend serves: `{value,label}` (FK sibling), `{name,...}` (user object such
49
49
  // as created_by), or `{title}`. Returns undefined for plain/empty objects.
50
- function objectLabel(value) {
50
+ export function objectLabel(value) {
51
51
  if (!value || typeof value !== 'object' || Array.isArray(value))
52
52
  return undefined;
53
53
  const label = value.label ?? value.name ?? value.title;
@@ -66,7 +66,7 @@ function pickImage(value) {
66
66
  // FK column. A field `category_id` (search/dynamic_select/ref) ships a sibling
67
67
  // `record.category = {value,label,image?}` (or a bare string/{name}); returns
68
68
  // the raw sibling (object or string) so the caller can extract label + image.
69
- function relationSiblingValue(field, record) {
69
+ export function relationSiblingValue(field, record) {
70
70
  if (!record)
71
71
  return undefined;
72
72
  const candidates = [];
@@ -91,7 +91,7 @@ function relationSiblingValue(field, record) {
91
91
  }
92
92
  // fieldItemFields reads the declared jsonb line-items schema off a field,
93
93
  // tolerating the snake_case `item_fields` alias the kernel serves.
94
- function fieldItemFields(field) {
94
+ export function fieldItemFields(field) {
95
95
  return field.itemFields ?? field.item_fields;
96
96
  }
97
97
  // isLineItemsField — a jsonb line-items column (e.g. Transfer.items): it either
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "24.0.0",
3
+ "version": "24.0.1",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,9 +42,17 @@ import { DynamicRelations } from './dynamic-relations'
42
42
  import { DynamicSelectField } from './dynamic-select-field'
43
43
  import { DynamicDateField } from './dynamic-date-field'
44
44
  import { UploadField } from './upload-field'
45
- import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn } from './dynamic-form-schema'
45
+ import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn, getFieldRef } from './dynamic-form-schema'
46
46
  import { FieldGrid, FieldCell, FieldLabel } from './field-grid'
47
- import type { ActionFieldDef } from './types'
47
+ import { useMetadataCache } from './metadata-cache'
48
+ import {
49
+ ViewValue,
50
+ objectLabel,
51
+ relationSiblingValue,
52
+ fieldItemFields,
53
+ isLineItemsField as isRecordLineItemsField,
54
+ } from './dialogs/dynamic-record'
55
+ import type { ActionFieldDef, TableMetadata, ColumnDefinition } from './types'
48
56
  // Canonical registry lives in @asteby/metacore-sdk
49
57
  import {
50
58
  type ActionMetadata,
@@ -228,6 +236,172 @@ function buildActionUrl(endpoint: string | undefined, model: string, recordId: s
228
236
  return hasRecord ? `/data/${model}/me/${recordId}/action/${actionKey}` : `/data/${model}/me/action/${actionKey}`
229
237
  }
230
238
 
239
+ // ── Preview automático del registro en el modal de confirmación ─────────────
240
+ //
241
+ // Antes de confirmar una row-action (aceptar/rechazar un traspaso, etc.) el
242
+ // usuario ve un resumen compacto y read-only de QUÉ registro va a afectar, para
243
+ // no confirmar a ciegas. Es 100% del SDK y genérico: se apoya en la metadata de
244
+ // tabla del modelo (labels + display hints) y en los siblings de relación que la
245
+ // tabla ya resolvió sobre el `record`. Se degrada solo — si no hay ni un campo
246
+ // ni líneas útiles que mostrar, no renderiza nada (ni una caja vacía).
247
+
248
+ // Claves de sistema/infra que nunca aportan contexto al preview.
249
+ const PREVIEW_SKIP_KEYS = new Set([
250
+ 'id',
251
+ 'organization_id',
252
+ 'org_id',
253
+ 'created_at',
254
+ 'updated_at',
255
+ 'deleted_at',
256
+ 'created_by',
257
+ 'created_by_id',
258
+ 'updated_by',
259
+ 'updated_by_id',
260
+ ])
261
+
262
+ // Campos escalares "de identidad" que sí vale la pena anclar en el preview
263
+ // aunque no sean relación ni line-items (nombre, folio, estado, etc.).
264
+ const PREVIEW_INTEREST_KEYS = new Set([
265
+ 'name',
266
+ 'title',
267
+ 'code',
268
+ 'reference',
269
+ 'folio',
270
+ 'number',
271
+ 'status',
272
+ 'stage',
273
+ 'state',
274
+ ])
275
+ const PREVIEW_INTEREST_STYLES = new Set(['status', 'badge', 'currency'])
276
+
277
+ const MAX_PREVIEW_FIELD_ROWS = 6
278
+
279
+ function isEmptyPreviewValue(value: any): boolean {
280
+ if (value === null || value === undefined || value === '') return true
281
+ if (Array.isArray(value)) return value.length === 0
282
+ if (typeof value === 'object' && !(value instanceof Date)) return Object.keys(value).length === 0
283
+ return false
284
+ }
285
+
286
+ interface PreviewRow {
287
+ col: ColumnDefinition
288
+ value: any
289
+ lineItems: boolean
290
+ }
291
+
292
+ // selectPreviewColumns aplica la heurística compacta sobre las columnas del
293
+ // modelo: relaciones resueltas a su label, campos line-items (jsonb) y un puñado
294
+ // de escalares de identidad. Omite id/org/timestamps y los *_id crudos cuya
295
+ // relación no resolvió a un label legible.
296
+ function selectPreviewColumns(columns: ColumnDefinition[] | undefined, record: any): PreviewRow[] {
297
+ if (!columns || !record) return []
298
+ const lineItemRows: PreviewRow[] = []
299
+ const fieldRows: PreviewRow[] = []
300
+
301
+ for (const col of columns) {
302
+ if (!col || !col.key) continue
303
+ if (col.hidden) continue
304
+ if (PREVIEW_SKIP_KEYS.has(col.key)) continue
305
+ const value = record[col.key]
306
+
307
+ // Line-items (jsonb array como Transfer.items) → mini-tabla producto×cantidad.
308
+ if (isRecordLineItemsField(col as any, value)) {
309
+ if (isEmptyPreviewValue(value)) continue
310
+ lineItemRows.push({ col, value, lineItems: true })
311
+ continue
312
+ }
313
+
314
+ // Relación (ref / search / dynamic_select / *_id): solo si el sibling
315
+ // resolvió a un label legible; si es un *_id crudo sin resolver, se omite.
316
+ const isRelation =
317
+ !!getFieldRef(col as ActionFieldDef) ||
318
+ col.type === 'search' ||
319
+ col.type === 'relation' ||
320
+ (col as { widget?: string }).widget === 'dynamic_select' ||
321
+ (typeof col.key === 'string' && col.key.endsWith('_id'))
322
+ if (isRelation) {
323
+ const sib = relationSiblingValue(col as any, record)
324
+ const label = typeof sib === 'string' ? sib : objectLabel(sib)
325
+ if (!label) continue
326
+ fieldRows.push({ col, value, lineItems: false })
327
+ continue
328
+ }
329
+
330
+ // Escalar de identidad (nombre/folio/estado/moneda…) con valor.
331
+ const styleKey = col.cellStyle ?? col.type
332
+ const interesting =
333
+ PREVIEW_INTEREST_KEYS.has(col.key) || PREVIEW_INTEREST_STYLES.has(String(styleKey))
334
+ if (interesting && !isEmptyPreviewValue(value)) {
335
+ fieldRows.push({ col, value, lineItems: false })
336
+ }
337
+ }
338
+
339
+ return [...fieldRows.slice(0, MAX_PREVIEW_FIELD_ROWS), ...lineItemRows]
340
+ }
341
+
342
+ function RecordPreview({ model, record }: { model: string; record: any }) {
343
+ const { t } = useTranslation()
344
+ const api = useApi()
345
+ const cached = useMetadataCache((s) => s.getMetadata(model))
346
+ const setMetadata = useMetadataCache((s) => s.setMetadata)
347
+ const [fetched, setFetched] = useState<TableMetadata | null>(null)
348
+
349
+ // Sin metadata cacheada → UN fetch a /metadata/table/<model> (mismo patrón
350
+ // que model-action-toolbar). Se guarda en el store para próximos usos.
351
+ useEffect(() => {
352
+ if (cached || !model) return
353
+ let cancelled = false
354
+ api
355
+ .get(`/metadata/table/${model}`)
356
+ .then((res) => {
357
+ if (cancelled) return
358
+ const meta = (res.data?.data ?? res.data) as TableMetadata
359
+ if (meta && Array.isArray(meta.columns)) {
360
+ setFetched(meta)
361
+ setMetadata(model, meta)
362
+ }
363
+ })
364
+ .catch(() => {
365
+ if (!cancelled) setFetched(null)
366
+ })
367
+ return () => {
368
+ cancelled = true
369
+ }
370
+ }, [cached, model, api, setMetadata])
371
+
372
+ const meta = cached ?? fetched
373
+ const rows = useMemo(() => selectPreviewColumns(meta?.columns, record), [meta, record])
374
+
375
+ // Degradación: nada útil que mostrar → no renderiza la sección.
376
+ if (rows.length === 0) return null
377
+
378
+ return (
379
+ <div className="mt-1 overflow-hidden rounded-md border bg-muted/30 text-sm">
380
+ {rows.map(({ col, value, lineItems }) => {
381
+ const label = t(col.label, { defaultValue: col.label })
382
+ if (lineItems) {
383
+ return (
384
+ <div key={col.key} className="border-t px-3 py-2 first:border-t-0">
385
+ <div className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
386
+ {label}
387
+ </div>
388
+ <ViewValue field={col as any} value={value} record={record} />
389
+ </div>
390
+ )
391
+ }
392
+ return (
393
+ <div key={col.key} className="flex items-baseline gap-3 border-t px-3 py-1.5 first:border-t-0">
394
+ <span className="w-1/3 shrink-0 truncate text-muted-foreground">{label}</span>
395
+ <div className="min-w-0 flex-1 [&_p]:py-0">
396
+ <ViewValue field={col as any} value={value} record={record} />
397
+ </div>
398
+ </div>
399
+ )
400
+ })}
401
+ </div>
402
+ )
403
+ }
404
+
231
405
  function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }: ActionModalProps) {
232
406
  const { t } = useTranslation()
233
407
  const api = useApi()
@@ -268,6 +442,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
268
442
  {action.confirmMessage || `${label}?`}
269
443
  </AlertDialogDescription>
270
444
  </AlertDialogHeader>
445
+ {record ? <RecordPreview model={model} record={record} /> : null}
271
446
  <AlertDialogFooter>
272
447
  <AlertDialogCancel disabled={executing}>{t('common.cancel')}</AlertDialogCancel>
273
448
  <AlertDialogAction
@@ -267,7 +267,7 @@ function resolvePath(obj: any, path: string): any {
267
267
  // objectLabel pulls a human label off a resolved relation/user object the
268
268
  // backend serves: `{value,label}` (FK sibling), `{name,...}` (user object such
269
269
  // as created_by), or `{title}`. Returns undefined for plain/empty objects.
270
- function objectLabel(value: any): string | undefined {
270
+ export function objectLabel(value: any): string | undefined {
271
271
  if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
272
272
  const label = value.label ?? value.name ?? value.title
273
273
  if (label != null && label !== '') return String(label)
@@ -285,7 +285,7 @@ function pickImage(value: any): string | undefined {
285
285
  // FK column. A field `category_id` (search/dynamic_select/ref) ships a sibling
286
286
  // `record.category = {value,label,image?}` (or a bare string/{name}); returns
287
287
  // the raw sibling (object or string) so the caller can extract label + image.
288
- function relationSiblingValue(field: FieldDef, record: any): any {
288
+ export function relationSiblingValue(field: FieldDef, record: any): any {
289
289
  if (!record) return undefined
290
290
  const candidates: string[] = []
291
291
  const ref = getFieldRef(field as ActionFieldDef)
@@ -305,7 +305,7 @@ function relationSiblingValue(field: FieldDef, record: any): any {
305
305
 
306
306
  // fieldItemFields reads the declared jsonb line-items schema off a field,
307
307
  // tolerating the snake_case `item_fields` alias the kernel serves.
308
- function fieldItemFields(field: FieldDef): ItemField[] | undefined {
308
+ export function fieldItemFields(field: FieldDef): ItemField[] | undefined {
309
309
  return field.itemFields ?? field.item_fields
310
310
  }
311
311