@asteby/metacore-runtime-react 23.1.0 → 23.3.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/action-modal-dispatcher.js +4 -4
  3. package/dist/dialogs/dynamic-record.d.ts +5 -0
  4. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  5. package/dist/dialogs/dynamic-record.js +146 -31
  6. package/dist/display-value.d.ts +56 -0
  7. package/dist/display-value.d.ts.map +1 -0
  8. package/dist/display-value.js +74 -0
  9. package/dist/dynamic-columns.d.ts.map +1 -1
  10. package/dist/dynamic-columns.js +2 -53
  11. package/dist/dynamic-kanban.d.ts +19 -3
  12. package/dist/dynamic-kanban.d.ts.map +1 -1
  13. package/dist/dynamic-kanban.js +114 -8
  14. package/dist/dynamic-row-actions.d.ts.map +1 -1
  15. package/dist/dynamic-row-actions.js +5 -3
  16. package/dist/dynamic-table.d.ts +9 -1
  17. package/dist/dynamic-table.d.ts.map +1 -1
  18. package/dist/dynamic-table.js +132 -32
  19. package/dist/use-infinite-scroll.d.ts +27 -0
  20. package/dist/use-infinite-scroll.d.ts.map +1 -0
  21. package/dist/use-infinite-scroll.js +60 -0
  22. package/package.json +3 -3
  23. package/src/__tests__/dynamic-kanban-infinite-scroll.test.tsx +210 -0
  24. package/src/__tests__/dynamic-table-infinite-scroll.test.tsx +196 -0
  25. package/src/__tests__/readonly-fields.test.tsx +97 -0
  26. package/src/__tests__/record-detail-display.test.tsx +113 -0
  27. package/src/__tests__/use-infinite-scroll.test.tsx +147 -0
  28. package/src/action-modal-dispatcher.tsx +4 -4
  29. package/src/dialogs/dynamic-record.tsx +217 -40
  30. package/src/display-value.tsx +134 -0
  31. package/src/dynamic-columns.tsx +6 -102
  32. package/src/dynamic-kanban.tsx +168 -8
  33. package/src/dynamic-row-actions.tsx +5 -3
  34. package/src/dynamic-table.tsx +149 -10
  35. package/src/use-infinite-scroll.ts +94 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 23.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 40a1e8e: Soporte de campos `readonly` (kernel v0.64.0) en el diálogo de registro dinámico. Un campo generado por el servidor/sistema (p. ej. `number`/`github_url` que el addon de GitHub rellena tras el create outbound) ahora se OCULTA en el formulario de creación y se muestra DESHABILITADO (input muted, valor visible) en edición. Las vistas de lectura (tabla/kanban/detalle) no cambian.
8
+ - d0eb423: El modal de detalle de registro (`ViewRecordDialog` / `DynamicRecordDialog`) ahora renderiza los valores con los MISMOS display types "pro" que la tabla, en vez de texto plano genérico. Las primitivas de render de la tabla (`OptionBadge`, `RelationThumbnail`, `statusColorFor`, `useIsDarkTheme`) se extrajeron a un módulo compartido `display-value.tsx` que tabla y dialog consumen (cero copy-paste).
9
+
10
+ El dialog ahora resuelve cada valor por el display type declarado (`cellStyle ?? type`), igual que la tabla:
11
+ - opciones/select → Badge con color resuelto y label localizado
12
+ - `cellStyle:'status'`/`'badge'` (ej. stage de kanban) → pill con color semántico y label traducido vía i18n del manifest, en vez de "backlog" crudo
13
+ - `cellStyle:'url'`/`'link'` en columna de texto (ej. `github_url`) → enlace clickeable en pestaña nueva, truncado
14
+ - `cellStyle:'datetime'` en columna numérica/epoch (ej. `synced_at`) → fecha formateada con timezone de la org, no dígitos crudos
15
+ - arrays de labels/tags → fila de badges (con color cuando el label trae `color`)
16
+ - creator/avatar → nombre + avatar
17
+
18
+ ## 23.2.1
19
+
20
+ ### Patch Changes
21
+
22
+ - d578fe0: Localiza los toasts de CRUD estándar (eliminar, crear, actualizar, borrado masivo, subida de imagen) al español vía i18n con `defaultValue`, en lugar de mostrar el `message` en inglés que devuelve el backend. Los mensajes de acciones declarativas de addons ahora pasan por `t()` por si son claves i18n.
23
+
24
+ ## 23.2.0
25
+
26
+ ### Minor Changes
27
+
28
+ - 68a6844: Scroll infinito con carga incremental en DynamicKanban y DynamicTable,
29
+ respetando la búsqueda y los filtros activos. Cero cambios de backend: se apoya
30
+ en el `page`/`per_page` que ya expone `/data/:model`.
31
+ - **Primitivos compartidos (`use-infinite-scroll`):** `dedupeById` (append puro
32
+ que descarta ids ya presentes, estable en identidad cuando no hay altas) y
33
+ `useInfiniteScrollSentinel` (IntersectionObserver sobre un sentinel; lee
34
+ `onLoadMore`/`disabled` por ref para no recrear el observer en cada render;
35
+ degrada a no-op donde no hay IntersectionObserver). Los usan ambas vistas.
36
+ - **Kanban incremental por lane:** una página global inicial (`pageSize`, 50 por
37
+ defecto) pinta el tablero agrupado —y captura naturalmente la lane
38
+ "sin asignar"— y luego cada lane rellena SU propia etapa al acercarse el
39
+ scroll al fondo (`f_<group_by>=<stage>&page=n&per_page=lanePageSize`, 25 por
40
+ defecto, sobre los filtros activos), con dedup por id y un skeleton chico al
41
+ fondo. El contador del header muestra el total real de la etapa cuando la
42
+ respuesta trae `meta.total` (`count/total`), si no el cargado. Cambiar
43
+ filtros/búsqueda resetea la paginación de todas las lanes. El drag&drop
44
+ optimista ajusta los totales de origen y destino (`applyLaneTotalsOnMove`)
45
+ para que las lanes parciales sigan mostrando un `count/total` veraz, y los
46
+ revierte si el PUT falla.
47
+ - **Tabla opt-in (`infiniteScroll?: boolean`, default `false`):** la paginación
48
+ clásica queda intacta salvo que se active. Con el flag, un sentinel al fondo
49
+ del contenedor de scroll pide la siguiente página y APPENDEA filas (dedup por
50
+ id); el pager clásico se reemplaza por un indicador "N de total". Cambiar
51
+ filtros/orden/búsqueda resetea a la página 1 y limpia el acumulado. El footer
52
+ de totales (`/aggregate`) no cambia.
53
+
54
+ ### Patch Changes
55
+
56
+ - Updated dependencies [68a6844]
57
+ - @asteby/metacore-ui@2.9.1
58
+
3
59
  ## 23.1.0
4
60
 
5
61
  ### Minor Changes
@@ -116,12 +116,12 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
116
116
  const url = buildActionUrl(endpoint, model, record.id, action.key);
117
117
  const res = await api.post(url, {});
118
118
  if (res.data.success) {
119
- toast.success(res.data.message || t('common.success'));
119
+ toast.success(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.success'));
120
120
  onOpenChange(false);
121
121
  onSuccess();
122
122
  }
123
123
  else {
124
- toast.error(res.data.message || t('common.error'));
124
+ toast.error(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.error'));
125
125
  }
126
126
  }
127
127
  catch (err) {
@@ -209,12 +209,12 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
209
209
  const url = buildActionUrl(endpoint, model, record.id, action.key);
210
210
  const res = await api.post(url, formData);
211
211
  if (res.data.success) {
212
- toast.success(res.data.message || t('common.success'));
212
+ toast.success(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.success'));
213
213
  onOpenChange(false);
214
214
  onSuccess();
215
215
  }
216
216
  else {
217
- toast.error(res.data.message || t('common.error'));
217
+ toast.error(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.error'));
218
218
  }
219
219
  }
220
220
  catch (err) {
@@ -162,7 +162,12 @@ export interface DynamicRecordDialogProps {
162
162
  export declare function isLineItemsField(field: FieldDef, value: any): boolean;
163
163
  export declare function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null;
164
164
  export declare function isMoneyField(field: FieldDef, value: any): boolean;
165
+ export declare function filterVisibleFields(fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create'): FieldDef[];
165
166
  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;
167
+ export declare function ReadonlyEditField({ field, value }: {
168
+ field: FieldDef;
169
+ value: any;
170
+ }): import("react").JSX.Element;
166
171
  export declare function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, currency: currencyProp, }: {
167
172
  field: FieldDef;
168
173
  value: any;
@@ -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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"}
@@ -27,6 +27,8 @@ import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
27
27
  import { DynamicIcon, isLucideIconName } from '../dynamic-icon';
28
28
  import { humanizeToken } from '../dynamic-columns-helpers';
29
29
  import { formatDateCell } from '../dynamic-columns';
30
+ import { OptionBadge, statusColorFor, useIsDarkTheme, } from '../display-value';
31
+ import { generateBadgeStyles } from '@asteby/metacore-ui/lib';
30
32
  import { CollectionCell } from '../collection-cell';
31
33
  import { ImageUrlContext, identityImageUrl } from '../image-url-context';
32
34
  import { TimeZoneContext, CurrencyContext } from '../org-runtime-context';
@@ -228,6 +230,19 @@ export function isMoneyField(field, value) {
228
230
  return false;
229
231
  return MONEY_KEY_HEURISTIC.some(m => key === m || key.endsWith(`_${m}`) || key.startsWith(`${m}_`));
230
232
  }
233
+ // filterVisibleFields decides which declared fields render in the form for a
234
+ // given mode. `hidden` fields never render. A `readonly` (server/system-
235
+ // generated) field is EXCLUDED on create — the user can't set a value the
236
+ // server will overwrite — but stays visible on edit/view (rendered disabled).
237
+ export function filterVisibleFields(fields, mode) {
238
+ return (fields ?? []).filter(f => {
239
+ if (f.hidden)
240
+ return false;
241
+ if (mode === 'create' && f.readonly)
242
+ return false;
243
+ return true;
244
+ });
245
+ }
231
246
  export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
232
247
  const api = useApi();
233
248
  const { t } = useTranslation();
@@ -319,7 +334,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
319
334
  catch (err) {
320
335
  console.error('[DynamicRecordDialog] load error:', err);
321
336
  if (!seed)
322
- toast.error('Error al cargar los datos');
337
+ toast.error(t('dynamic.load_error', { defaultValue: 'No se pudieron cargar los datos' }));
323
338
  }
324
339
  finally {
325
340
  if (!cancelled)
@@ -421,14 +436,14 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
421
436
  try {
422
437
  if (isCreate && onCreate) {
423
438
  const created = await onCreate(formValues);
424
- toast.success(modalMeta?.messages?.created || 'Registro creado correctamente');
439
+ toast.success(modalMeta?.messages?.created || t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' }));
425
440
  onSaved?.(created ?? undefined);
426
441
  onOpenChange(false);
427
442
  return;
428
443
  }
429
444
  if (!isCreate && recordId && onUpdate) {
430
445
  const updated = await onUpdate(String(recordId), formValues);
431
- toast.success(modalMeta?.messages?.updated || 'Guardado correctamente');
446
+ toast.success(modalMeta?.messages?.updated || t('dynamic.update_success', { defaultValue: 'Guardado correctamente' }));
432
447
  onSaved?.(updated ?? undefined);
433
448
  onOpenChange(false);
434
449
  return;
@@ -449,17 +464,19 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
449
464
  // localized fallback. NOT res.data.message — the dynamic CRUD
450
465
  // endpoint returns a raw English string that would leak into the toast.
451
466
  toast.success(modalMeta?.messages?.[isCreate ? 'created' : 'updated']
452
- || (isCreate ? 'Registro creado correctamente' : 'Guardado correctamente'));
467
+ || (isCreate
468
+ ? t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' })
469
+ : t('dynamic.update_success', { defaultValue: 'Guardado correctamente' })));
453
470
  // Hand the persisted record back so callers can auto-select it.
454
471
  onSaved?.(res.data?.data ?? res.data ?? undefined);
455
472
  onOpenChange(false);
456
473
  }
457
474
  else {
458
- toast.error(res.data?.message || 'Error al guardar');
475
+ toast.error(res.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }));
459
476
  }
460
477
  }
461
478
  catch (err) {
462
- toast.error(err?.response?.data?.message || 'Error al guardar');
479
+ toast.error(err?.response?.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }));
463
480
  }
464
481
  finally {
465
482
  setSaving(false);
@@ -475,20 +492,14 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
475
492
  }
476
493
  catch (err) {
477
494
  console.error('[DynamicRecordDialog] delete error:', err);
478
- toast.error(err?.response?.data?.message || err?.message || 'Error al eliminar');
495
+ toast.error(err?.response?.data?.message || err?.message || t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }));
479
496
  }
480
497
  finally {
481
498
  setDeleting(false);
482
499
  }
483
500
  };
484
501
  const title = modalMeta ? config.getTitle(modalMeta, t) : '';
485
- const visibleFields = modalMeta?.fields?.filter(f => {
486
- if (f.hidden)
487
- return false;
488
- if (isCreate && f.readonly)
489
- return false;
490
- return true;
491
- }) ?? [];
502
+ const visibleFields = filterVisibleFields(modalMeta?.fields, mode);
492
503
  return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
493
504
  const isFullWidth = field.type === 'textarea';
494
505
  return (_jsx("div", { className: isFullWidth ? 'sm:col-span-2' : '', children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
@@ -498,8 +509,26 @@ function LoadingSkeleton() {
498
509
  return (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-9 w-full" })] }, i))) }));
499
510
  }
500
511
  function FieldRow({ field, record, value, mode, onChange }) {
501
- const isReadonly = field.readonly || mode === 'view';
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 }))] }));
512
+ // A `readonly` field is server/system-generated (e.g. the GitHub addon's
513
+ // `number`/`github_url`, filled by the API after the outbound create). On
514
+ // CREATE it is excluded from the form entirely (see `visibleFields`); on EDIT
515
+ // it stays visible but is NOT editable — rendered as a disabled, muted input
516
+ // so the user sees its value without being able to change it. View mode keeps
517
+ // the rich read-only renderer.
518
+ const isEditReadonly = mode === 'edit' && !!field.readonly;
519
+ 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' && !isEditReadonly && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), mode === 'view' ? (_jsx(ViewValue, { field: field, value: value, record: record })) : isEditReadonly ? (_jsx(ReadonlyEditField, { field: field, value: value })) : (_jsx(EditField, { field: field, value: value, onChange: onChange, record: record }))] }));
520
+ }
521
+ // ReadonlyEditField — the edit-mode rendering of a `readonly` (system-generated)
522
+ // field: a disabled, muted input that shows the current value without allowing
523
+ // edits. Booleans render as a disabled switch to match their editable
524
+ // counterpart; everything else renders the formatted display value in a disabled
525
+ // text input.
526
+ export function ReadonlyEditField({ field, value }) {
527
+ if (field.type === 'boolean' || typeof value === 'boolean') {
528
+ return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, disabled: true }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
529
+ }
530
+ const display = formatDisplayValue(value, field);
531
+ return _jsx(Input, { value: display === '—' ? '' : display, disabled: true, readOnly: true, className: "text-muted-foreground" });
503
532
  }
504
533
  // RelationViewValue — read-only FK lead. Resolves the relation's label + image
505
534
  // from (1) the sibling object the table served, then (2) the canonical options
@@ -549,8 +578,19 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
549
578
  const getImageUrl = getImageUrlProp ?? ctxImageUrl;
550
579
  const timeZone = timeZoneProp ?? ctxTimeZone;
551
580
  const currency = currencyProp ?? ctxCurrency;
581
+ // Declarative display hint the backend stamps (mirrors the table column's
582
+ // `cellStyle`). The table renders each cell off `cellStyle ?? type`; the
583
+ // detail view keys off the SAME resolved renderer so both stay in lock-step
584
+ // (a `datetime` display on a numeric column, a `url` display on a text
585
+ // column, a `status`/`badge` pill, …).
586
+ const renderAs = field.cellStyle ?? field.type;
552
587
  // created_by / avatar resolver sibling → name (+ avatar) instead of "—".
553
- if (field.type === 'avatar' || field.key === 'created_by' || field.key === 'created_by_id') {
588
+ if (field.type === 'avatar' ||
589
+ renderAs === 'avatar' ||
590
+ renderAs === 'creator' ||
591
+ renderAs === 'user' ||
592
+ field.key === 'created_by' ||
593
+ field.key === 'created_by_id') {
554
594
  const user = createdBySibling(rawValue, record);
555
595
  if (user) {
556
596
  return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [user.avatar ? (_jsx("img", { src: getImageUrl(String(user.avatar)), alt: user.name ?? '', className: "h-6 w-6 rounded-full object-cover" })) : null, _jsx("span", { className: "text-sm", children: user.name ?? user.email ?? '—' })] }));
@@ -593,8 +633,14 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
593
633
  (field.key === 'icon' || field.key.endsWith('_icon'))) {
594
634
  return _jsx(IconNameViewValue, { name: value });
595
635
  }
596
- if (field.type === 'url' && value) {
597
- return (_jsx("a", { href: value, target: "_blank", rel: "noreferrer", className: "text-sm text-primary hover:underline truncate", children: value }));
636
+ // URL/link display (matches the table's `url`/`link` cell). Triggers on the
637
+ // stamped display type not just the storage `type` so a text column
638
+ // carrying `cellStyle:'url'` (e.g. `github_url`) renders as a clickable
639
+ // external link, opening in a new tab, truncated.
640
+ if ((renderAs === 'url' || renderAs === 'link') && value) {
641
+ const urlStr = String(value);
642
+ const href = /^https?:\/\//i.test(urlStr) ? urlStr : `https://${urlStr}`;
643
+ return (_jsxs("a", { href: href, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "truncate max-w-[360px]", children: urlStr })] }));
598
644
  }
599
645
  // Money → org-currency string. Detected by the backend `cellStyle:'currency'`
600
646
  // stamp or a numeric value whose key matches the money heuristic (fallback
@@ -615,24 +661,54 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
615
661
  }
616
662
  // Date/datetime/timestamp → tz-aware format. `date` pins to UTC (calendar
617
663
  // day); instants render in the org timezone with a full-precision tooltip.
618
- if (field.type === 'date' || field.type === 'datetime' || field.type === 'timestamp') {
619
- const renderAs = field.type === 'date' ? 'date' : field.type;
620
- const formatted = formatDateCell(value, renderAs, es, timeZone);
664
+ // Keys off the display type (`cellStyle ?? type`) so a numeric/epoch column
665
+ // stamped `datetime` (e.g. `synced_at`) formats as a date, never raw digits.
666
+ if (renderAs === 'date' ||
667
+ renderAs === 'datetime' ||
668
+ renderAs === 'timestamp' ||
669
+ renderAs === 'timestamptz') {
670
+ const dateRenderAs = renderAs === 'date' ? 'date' : renderAs;
671
+ const formatted = formatDateCell(value, dateRenderAs, es, timeZone);
621
672
  if (formatted) {
622
673
  return (_jsx("p", { className: "text-sm py-1", title: formatted.title, children: formatted.display }));
623
674
  }
624
675
  return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
625
676
  }
626
- // Enum/option field with served options → colored/iconed badge using the
627
- // served label (e.g. "Almacenable" instead of "storable").
677
+ // Enum/option field with served options → the SAME colored/iconed pill the
678
+ // table renders (shared `OptionBadge`): resolved color, thumbnail/icon and
679
+ // the localized option label (e.g. "Almacenable" instead of "storable").
628
680
  const opt = servedOption(field, value);
629
681
  if (opt) {
630
- const lead = {
631
- image: opt.image ? getImageUrl(opt.image) : null,
632
- color: opt.color ?? null,
633
- icon: opt.icon ?? null,
634
- };
635
- return (_jsxs(Badge, { variant: "secondary", className: "w-fit flex items-center gap-1", style: opt.color && !opt.icon ? { backgroundColor: opt.color, color: '#fff', borderColor: 'transparent' } : undefined, children: [_jsx(OptionLead, { option: lead, size: 16 }), opt.label] }));
682
+ return (_jsx("div", { className: "py-1", children: _jsx(OptionBadge, { option: {
683
+ value: String(opt.value ?? value ?? ''),
684
+ label: opt.label,
685
+ color: opt.color ?? undefined,
686
+ icon: opt.icon ?? undefined,
687
+ image: opt.image ?? undefined,
688
+ }, getImageUrl: getImageUrl }) }));
689
+ }
690
+ // Array of scalars / label objects (e.g. github `labels`, tags, a
691
+ // group-badge list) → a row of pills, mirroring the table's `tags` /
692
+ // `relation-badge-list` cells. Checked before the structured-object branch
693
+ // so a flat label array never renders as a mini-table.
694
+ if (Array.isArray(value) &&
695
+ (renderAs === 'tags' ||
696
+ renderAs === 'relation-badge-list' ||
697
+ value.every((v) => v === null || typeof v !== 'object' || 'label' in v || 'name' in v))) {
698
+ return _jsx(BadgeListViewValue, { items: value, getImageUrl: getImageUrl });
699
+ }
700
+ // Status / badge / select display with no served option list — a bare enum
701
+ // token (e.g. a kanban `stage` like "backlog"). Render a colored pill with a
702
+ // semantic/value-derived color and a localized-or-humanized label, matching
703
+ // the table's `status`/`badge` cells.
704
+ if ((renderAs === 'status' ||
705
+ renderAs === 'badge' ||
706
+ renderAs === 'select' ||
707
+ renderAs === 'option') &&
708
+ value !== null &&
709
+ value !== undefined &&
710
+ typeof value !== 'object') {
711
+ return _jsx(StatusBadgeViewValue, { field: field, value: value, t: t });
636
712
  }
637
713
  // Structured value (jsonb column, e.g. fiscal_data) with no label/name/title
638
714
  // to surface — render readable key/value pairs instead of falling through to
@@ -652,6 +728,44 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
652
728
  function IconNameViewValue({ name }) {
653
729
  return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx("div", { className: "h-8 w-8 flex items-center justify-center rounded bg-muted", children: _jsx(DynamicIcon, { name: name, className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm text-muted-foreground", children: name })] }));
654
730
  }
731
+ // StatusBadgeViewValue — a bare enum/status token (no served option list) as a
732
+ // colored pill: a semantic/value-derived color (same `statusColorFor` the table
733
+ // uses) plus a localized-or-humanized label. Mirrors the table's `status`/
734
+ // `badge` cell so a kanban `stage` ("backlog") reads as a colored, translated
735
+ // badge instead of the raw token.
736
+ function StatusBadgeViewValue({ field, value, t, }) {
737
+ const isDark = useIsDarkTheme();
738
+ const token = String(value);
739
+ // Prefer an explicit per-option color served on the field (metadata.stages /
740
+ // options), else derive a semantic color from the token.
741
+ const declared = field.options?.find((o) => String(o.value) === token);
742
+ const color = declared?.color || statusColorFor(token);
743
+ // Localized label: the option's already-localized label wins, then a manifest
744
+ // i18n key matching the raw token, then a humanized fallback.
745
+ const label = declared?.label ?? t(token, { defaultValue: humanizeToken(token) });
746
+ return (_jsx("div", { className: "py-1", children: _jsx(Badge, { variant: "outline", className: "border-0 flex w-fit items-center gap-1", style: generateBadgeStyles(color, { isDark }), children: label }) }));
747
+ }
748
+ // BadgeListViewValue — an array of scalars / label objects as a row of pills
749
+ // (github `labels`, tags, a group-badge list). Objects carrying a `color` render
750
+ // as colored `OptionBadge`s; plain strings render as neutral secondary pills.
751
+ function BadgeListViewValue({ items, getImageUrl, }) {
752
+ if (!items || items.length === 0) {
753
+ return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
754
+ }
755
+ return (_jsx("div", { className: "flex flex-wrap gap-1 py-1", children: items.map((item, i) => {
756
+ if (item !== null && typeof item === 'object') {
757
+ const opt = {
758
+ value: String(item.value ?? item.id ?? item.name ?? item.label ?? i),
759
+ label: String(item.label ?? item.name ?? item.value ?? ''),
760
+ color: item.color ?? undefined,
761
+ icon: item.icon ?? undefined,
762
+ image: item.image ?? item.avatar ?? undefined,
763
+ };
764
+ return _jsx(OptionBadge, { option: opt, getImageUrl: getImageUrl }, i);
765
+ }
766
+ return (_jsx(Badge, { variant: "secondary", className: "w-fit", children: String(item) }, i));
767
+ }) }));
768
+ }
655
769
  // StructuredViewValue renders a jsonb object/array that has no resolvable label.
656
770
  // It delegates to the shared `CollectionCell` in `'inline'` mode so the detail
657
771
  // view gets the SAME pro rendering as the table: a declared `item_fields` schema
@@ -738,6 +852,7 @@ export function EditField({ field, value, onChange, record }) {
738
852
  return (_jsx(Input, { type: inputType, value: value ?? '', onChange: (e) => onChange(field.type === 'number' ? (e.target.value === '' ? '' : Number(e.target.value)) : e.target.value), placeholder: field.placeholder }));
739
853
  }
740
854
  function ImageUploadField({ field: _field, value, onChange }) {
855
+ const { t } = useTranslation();
741
856
  const api = useApi();
742
857
  const model = useContext(ModelContext);
743
858
  const getImageUrl = useContext(ImageUrlContext);
@@ -758,7 +873,7 @@ function ImageUploadField({ field: _field, value, onChange }) {
758
873
  onChange(url);
759
874
  }
760
875
  catch {
761
- toast.error('Error al subir imagen');
876
+ toast.error(t('dynamic.image_upload_error', { defaultValue: 'No se pudo subir la imagen' }));
762
877
  }
763
878
  finally {
764
879
  setUploading(false);
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared display-value primitives.
3
+ *
4
+ * These are the building blocks the dynamic TABLE (`dynamic-columns.tsx`) uses
5
+ * to render "pro" cells — colored option badges, relation thumbnails, semantic
6
+ * status colors, dark-mode detection. They live here (instead of inline in the
7
+ * table) so the read-only DETAIL DIALOG (`dialogs/dynamic-record.tsx`) can reuse
8
+ * the EXACT same rendering. Ecosystem rule: shared primitives, zero copy-paste —
9
+ * table and dialog must not drift.
10
+ */
11
+ import React from 'react';
12
+ /**
13
+ * `true` when the host document is in dark mode. Observes the `<html>` class so
14
+ * badge colors re-derive on theme toggle. SSR/no-DOM safe (starts light).
15
+ */
16
+ export declare function useIsDarkTheme(): boolean;
17
+ /**
18
+ * Semantic status → badge color. Used by the `status` cell/value when no
19
+ * explicit `options` color is declared. Generic, value-driven mapping.
20
+ */
21
+ export declare function statusColorFor(value: string): string;
22
+ /**
23
+ * Tiny square thumbnail for a resolved relation/option that carries an `image`
24
+ * (brand logo, product photo, customer/user avatar). Uses the same Avatar
25
+ * primitive as the `avatar`/`creator` cells so a broken/loading image
26
+ * gracefully falls back to the record's initials. Sized small (inline style so
27
+ * an addon-arbitrary Tailwind class never gets dropped by a consuming app's
28
+ * class scan). Rendered inline alongside a label — never alone.
29
+ */
30
+ export declare const RelationThumbnail: React.FC<{
31
+ src: string;
32
+ alt: string;
33
+ getImageUrl?: (path: string) => string;
34
+ size?: number;
35
+ }>;
36
+ export interface DisplayOption {
37
+ value: string;
38
+ label: string;
39
+ icon?: string;
40
+ color?: string;
41
+ image?: string;
42
+ }
43
+ /**
44
+ * Colored option badge — the canonical "pro" pill for a select/status/badge
45
+ * value. Explicit backend `color` wins; otherwise a stable, cohesive color is
46
+ * derived from the option value so "dead" gray badges come alive. Inline style
47
+ * (hex-derived) so it works regardless of the host's tailwind safelist —
48
+ * addon-arbitrary classes aren't in the host scan.
49
+ */
50
+ export declare const OptionBadge: React.FC<{
51
+ option: DisplayOption;
52
+ getImageUrl?: (path: string) => string;
53
+ /** Accepted for call-site compatibility with the table; unused (option.label wins). */
54
+ fallback?: string;
55
+ }>;
56
+ //# sourceMappingURL=display-value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"display-value.d.ts","sourceRoot":"","sources":["../src/display-value.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAmBxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAepD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,CAcA,CAAA;AAED,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC;IAC/B,MAAM,EAAE,aAAa,CAAA;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,CAmBA,CAAA"}
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Shared display-value primitives.
4
+ *
5
+ * These are the building blocks the dynamic TABLE (`dynamic-columns.tsx`) uses
6
+ * to render "pro" cells — colored option badges, relation thumbnails, semantic
7
+ * status colors, dark-mode detection. They live here (instead of inline in the
8
+ * table) so the read-only DETAIL DIALOG (`dialogs/dynamic-record.tsx`) can reuse
9
+ * the EXACT same rendering. Ecosystem rule: shared primitives, zero copy-paste —
10
+ * table and dialog must not drift.
11
+ */
12
+ import React from 'react';
13
+ import { Avatar, AvatarImage, AvatarFallback, Badge } from '@asteby/metacore-ui';
14
+ import { generateBadgeStyles, getInitials, optionColor, } from '@asteby/metacore-ui/lib';
15
+ import { DynamicIcon } from './dynamic-icon';
16
+ /**
17
+ * `true` when the host document is in dark mode. Observes the `<html>` class so
18
+ * badge colors re-derive on theme toggle. SSR/no-DOM safe (starts light).
19
+ */
20
+ export function useIsDarkTheme() {
21
+ const [isDark, setIsDark] = React.useState(() => typeof document !== 'undefined' &&
22
+ document.documentElement.classList.contains('dark'));
23
+ React.useEffect(() => {
24
+ if (typeof document === 'undefined')
25
+ return;
26
+ const sync = () => setIsDark(document.documentElement.classList.contains('dark'));
27
+ sync();
28
+ const observer = new MutationObserver(sync);
29
+ observer.observe(document.documentElement, {
30
+ attributes: true,
31
+ attributeFilter: ['class'],
32
+ });
33
+ return () => observer.disconnect();
34
+ }, []);
35
+ return isDark;
36
+ }
37
+ /**
38
+ * Semantic status → badge color. Used by the `status` cell/value when no
39
+ * explicit `options` color is declared. Generic, value-driven mapping.
40
+ */
41
+ export function statusColorFor(value) {
42
+ const v = value.toLowerCase();
43
+ if (['active', 'enabled', 'paid', 'completed', 'done', 'success', 'approved', 'open']
44
+ .includes(v))
45
+ return '#22c55e';
46
+ if (['pending', 'draft', 'processing', 'in_progress', 'review', 'waiting'].includes(v))
47
+ return '#eab308';
48
+ if (['inactive', 'disabled', 'cancelled', 'canceled', 'failed', 'rejected', 'error', 'closed']
49
+ .includes(v))
50
+ return '#ef4444';
51
+ return '#6b7280';
52
+ }
53
+ /**
54
+ * Tiny square thumbnail for a resolved relation/option that carries an `image`
55
+ * (brand logo, product photo, customer/user avatar). Uses the same Avatar
56
+ * primitive as the `avatar`/`creator` cells so a broken/loading image
57
+ * gracefully falls back to the record's initials. Sized small (inline style so
58
+ * an addon-arbitrary Tailwind class never gets dropped by a consuming app's
59
+ * class scan). Rendered inline alongside a label — never alone.
60
+ */
61
+ export const RelationThumbnail = ({ src, alt, getImageUrl, size = 18 }) => (_jsxs(Avatar, { className: "shrink-0 rounded-sm ring-1 ring-border/40", style: { width: size, height: size }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(src) : src, alt: alt, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-sm bg-primary/10 text-[8px] font-bold text-primary", children: getInitials(alt) })] }));
62
+ /**
63
+ * Colored option badge — the canonical "pro" pill for a select/status/badge
64
+ * value. Explicit backend `color` wins; otherwise a stable, cohesive color is
65
+ * derived from the option value so "dead" gray badges come alive. Inline style
66
+ * (hex-derived) so it works regardless of the host's tailwind safelist —
67
+ * addon-arbitrary classes aren't in the host scan.
68
+ */
69
+ export const OptionBadge = ({ option, getImageUrl }) => {
70
+ const isDark = useIsDarkTheme();
71
+ const colorSource = option.color || optionColor(option.value || option.label);
72
+ const colorStyles = generateBadgeStyles(colorSource, { isDark });
73
+ return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.image ? (_jsx(RelationThumbnail, { src: option.image, alt: option.label, getImageUrl: getImageUrl, size: 16 })) : (option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" })), _jsx("span", { children: option.label })] }));
74
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAiC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAqKhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA8mBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAsC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAqFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA8mBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}