@asteby/metacore-runtime-react 23.2.0 → 23.4.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,38 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 23.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - edfa5a9: Render "pro" de URLs, imágenes y archivos: una URL nunca se muestra cruda. Nuevas primitivas compartidas en `rich-url.tsx` (`MediaValue`, `UrlChip`, `FileChip`, `ImageThumbnail`, `RichText`, `linkifyText`, `classifyUrl`, …) que consumen por igual la CELDA de la tabla/kanban (`dynamic-columns.tsx`) y el DETAIL DIALOG (`dynamic-record.tsx`) — cero copy-paste.
8
+ - URL de página (ej. `github_url` con la issue completa) → chip compacto con icono `ExternalLink` + label corto (hostname, ej. "github.com", o el nombre de archivo si aplica); URL completa en el tooltip; abre en pestaña nueva. Ya no se ve la URL de 120 caracteres.
9
+ - URL de imagen (jpg/png/gif/webp/avif/svg y rutas `…/storage/media/…` sin extensión) → THUMBNAIL inline redondeado y clickeable que abre la imagen completa; `onError` degrada a link chip (nunca un icono de imagen rota). En la celda el thumbnail es chico (~h-8) para no romper la altura de fila; en el dialog es más grande.
10
+ - URL de archivo (pdf/zip/docx/mp4/…) → chip con icono `FileText` + nombre del archivo.
11
+ - URLs embebidas en texto largo (body, textarea, long-text) → se linkifican con el mismo estilo (bare o markdown `[label](url)`), recortando puntuación final y respetando paréntesis balanceados; imágenes y archivos embebidos también se renderizan inline.
12
+
13
+ Sin llamadas de red para renderizar (nada de servicio de favicons — CSP/privacidad): el icono es un glyph lucide local.
14
+
15
+ ## 23.3.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 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.
20
+ - 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).
21
+
22
+ El dialog ahora resuelve cada valor por el display type declarado (`cellStyle ?? type`), igual que la tabla:
23
+ - opciones/select → Badge con color resuelto y label localizado
24
+ - `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
25
+ - `cellStyle:'url'`/`'link'` en columna de texto (ej. `github_url`) → enlace clickeable en pestaña nueva, truncado
26
+ - `cellStyle:'datetime'` en columna numérica/epoch (ej. `synced_at`) → fecha formateada con timezone de la org, no dígitos crudos
27
+ - arrays de labels/tags → fila de badges (con color cuando el label trae `color`)
28
+ - creator/avatar → nombre + avatar
29
+
30
+ ## 23.2.1
31
+
32
+ ### Patch Changes
33
+
34
+ - 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.
35
+
3
36
  ## 23.2.0
4
37
 
5
38
  ### 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,9 @@ 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 { MediaValue, RichText } from '../rich-url';
32
+ import { generateBadgeStyles } from '@asteby/metacore-ui/lib';
30
33
  import { CollectionCell } from '../collection-cell';
31
34
  import { ImageUrlContext, identityImageUrl } from '../image-url-context';
32
35
  import { TimeZoneContext, CurrencyContext } from '../org-runtime-context';
@@ -228,6 +231,19 @@ export function isMoneyField(field, value) {
228
231
  return false;
229
232
  return MONEY_KEY_HEURISTIC.some(m => key === m || key.endsWith(`_${m}`) || key.startsWith(`${m}_`));
230
233
  }
234
+ // filterVisibleFields decides which declared fields render in the form for a
235
+ // given mode. `hidden` fields never render. A `readonly` (server/system-
236
+ // generated) field is EXCLUDED on create — the user can't set a value the
237
+ // server will overwrite — but stays visible on edit/view (rendered disabled).
238
+ export function filterVisibleFields(fields, mode) {
239
+ return (fields ?? []).filter(f => {
240
+ if (f.hidden)
241
+ return false;
242
+ if (mode === 'create' && f.readonly)
243
+ return false;
244
+ return true;
245
+ });
246
+ }
231
247
  export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
232
248
  const api = useApi();
233
249
  const { t } = useTranslation();
@@ -319,7 +335,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
319
335
  catch (err) {
320
336
  console.error('[DynamicRecordDialog] load error:', err);
321
337
  if (!seed)
322
- toast.error('Error al cargar los datos');
338
+ toast.error(t('dynamic.load_error', { defaultValue: 'No se pudieron cargar los datos' }));
323
339
  }
324
340
  finally {
325
341
  if (!cancelled)
@@ -421,14 +437,14 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
421
437
  try {
422
438
  if (isCreate && onCreate) {
423
439
  const created = await onCreate(formValues);
424
- toast.success(modalMeta?.messages?.created || 'Registro creado correctamente');
440
+ toast.success(modalMeta?.messages?.created || t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' }));
425
441
  onSaved?.(created ?? undefined);
426
442
  onOpenChange(false);
427
443
  return;
428
444
  }
429
445
  if (!isCreate && recordId && onUpdate) {
430
446
  const updated = await onUpdate(String(recordId), formValues);
431
- toast.success(modalMeta?.messages?.updated || 'Guardado correctamente');
447
+ toast.success(modalMeta?.messages?.updated || t('dynamic.update_success', { defaultValue: 'Guardado correctamente' }));
432
448
  onSaved?.(updated ?? undefined);
433
449
  onOpenChange(false);
434
450
  return;
@@ -449,17 +465,19 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
449
465
  // localized fallback. NOT res.data.message — the dynamic CRUD
450
466
  // endpoint returns a raw English string that would leak into the toast.
451
467
  toast.success(modalMeta?.messages?.[isCreate ? 'created' : 'updated']
452
- || (isCreate ? 'Registro creado correctamente' : 'Guardado correctamente'));
468
+ || (isCreate
469
+ ? t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' })
470
+ : t('dynamic.update_success', { defaultValue: 'Guardado correctamente' })));
453
471
  // Hand the persisted record back so callers can auto-select it.
454
472
  onSaved?.(res.data?.data ?? res.data ?? undefined);
455
473
  onOpenChange(false);
456
474
  }
457
475
  else {
458
- toast.error(res.data?.message || 'Error al guardar');
476
+ toast.error(res.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }));
459
477
  }
460
478
  }
461
479
  catch (err) {
462
- toast.error(err?.response?.data?.message || 'Error al guardar');
480
+ toast.error(err?.response?.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }));
463
481
  }
464
482
  finally {
465
483
  setSaving(false);
@@ -475,20 +493,14 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
475
493
  }
476
494
  catch (err) {
477
495
  console.error('[DynamicRecordDialog] delete error:', err);
478
- toast.error(err?.response?.data?.message || err?.message || 'Error al eliminar');
496
+ toast.error(err?.response?.data?.message || err?.message || t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }));
479
497
  }
480
498
  finally {
481
499
  setDeleting(false);
482
500
  }
483
501
  };
484
502
  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
- }) ?? [];
503
+ const visibleFields = filterVisibleFields(modalMeta?.fields, mode);
492
504
  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
505
  const isFullWidth = field.type === 'textarea';
494
506
  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 +510,26 @@ function LoadingSkeleton() {
498
510
  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
511
  }
500
512
  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 }))] }));
513
+ // A `readonly` field is server/system-generated (e.g. the GitHub addon's
514
+ // `number`/`github_url`, filled by the API after the outbound create). On
515
+ // CREATE it is excluded from the form entirely (see `visibleFields`); on EDIT
516
+ // it stays visible but is NOT editable — rendered as a disabled, muted input
517
+ // so the user sees its value without being able to change it. View mode keeps
518
+ // the rich read-only renderer.
519
+ const isEditReadonly = mode === 'edit' && !!field.readonly;
520
+ 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 }))] }));
521
+ }
522
+ // ReadonlyEditField — the edit-mode rendering of a `readonly` (system-generated)
523
+ // field: a disabled, muted input that shows the current value without allowing
524
+ // edits. Booleans render as a disabled switch to match their editable
525
+ // counterpart; everything else renders the formatted display value in a disabled
526
+ // text input.
527
+ export function ReadonlyEditField({ field, value }) {
528
+ if (field.type === 'boolean' || typeof value === 'boolean') {
529
+ 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' })] }));
530
+ }
531
+ const display = formatDisplayValue(value, field);
532
+ return _jsx(Input, { value: display === '—' ? '' : display, disabled: true, readOnly: true, className: "text-muted-foreground" });
503
533
  }
504
534
  // RelationViewValue — read-only FK lead. Resolves the relation's label + image
505
535
  // from (1) the sibling object the table served, then (2) the canonical options
@@ -549,8 +579,19 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
549
579
  const getImageUrl = getImageUrlProp ?? ctxImageUrl;
550
580
  const timeZone = timeZoneProp ?? ctxTimeZone;
551
581
  const currency = currencyProp ?? ctxCurrency;
582
+ // Declarative display hint the backend stamps (mirrors the table column's
583
+ // `cellStyle`). The table renders each cell off `cellStyle ?? type`; the
584
+ // detail view keys off the SAME resolved renderer so both stay in lock-step
585
+ // (a `datetime` display on a numeric column, a `url` display on a text
586
+ // column, a `status`/`badge` pill, …).
587
+ const renderAs = field.cellStyle ?? field.type;
552
588
  // created_by / avatar resolver sibling → name (+ avatar) instead of "—".
553
- if (field.type === 'avatar' || field.key === 'created_by' || field.key === 'created_by_id') {
589
+ if (field.type === 'avatar' ||
590
+ renderAs === 'avatar' ||
591
+ renderAs === 'creator' ||
592
+ renderAs === 'user' ||
593
+ field.key === 'created_by' ||
594
+ field.key === 'created_by_id') {
554
595
  const user = createdBySibling(rawValue, record);
555
596
  if (user) {
556
597
  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 +634,15 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
593
634
  (field.key === 'icon' || field.key.endsWith('_icon'))) {
594
635
  return _jsx(IconNameViewValue, { name: value });
595
636
  }
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 }));
637
+ // URL/link display (matches the table's `url`/`link` cell). Triggers on the
638
+ // stamped display type not just the storage `type` so a text column
639
+ // carrying `cellStyle:'url'` (e.g. `github_url`) renders as a clickable
640
+ // external link, opening in a new tab, truncated.
641
+ if ((renderAs === 'url' || renderAs === 'link') && value) {
642
+ // Shared media renderer (same as the table cell): an image URL shows a
643
+ // larger inline thumbnail, a file a chip, else a compact link chip with
644
+ // a smart label — never the raw 120-char URL.
645
+ return (_jsx("div", { className: "py-1", children: _jsx(MediaValue, { url: String(value), getImageUrl: getImageUrl, label: field.styleConfig?.label, icon: field.styleConfig?.icon, thumbHeight: 200, maxLabelWidth: 360 }) }));
598
646
  }
599
647
  // Money → org-currency string. Detected by the backend `cellStyle:'currency'`
600
648
  // stamp or a numeric value whose key matches the money heuristic (fallback
@@ -615,24 +663,54 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
615
663
  }
616
664
  // Date/datetime/timestamp → tz-aware format. `date` pins to UTC (calendar
617
665
  // 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);
666
+ // Keys off the display type (`cellStyle ?? type`) so a numeric/epoch column
667
+ // stamped `datetime` (e.g. `synced_at`) formats as a date, never raw digits.
668
+ if (renderAs === 'date' ||
669
+ renderAs === 'datetime' ||
670
+ renderAs === 'timestamp' ||
671
+ renderAs === 'timestamptz') {
672
+ const dateRenderAs = renderAs === 'date' ? 'date' : renderAs;
673
+ const formatted = formatDateCell(value, dateRenderAs, es, timeZone);
621
674
  if (formatted) {
622
675
  return (_jsx("p", { className: "text-sm py-1", title: formatted.title, children: formatted.display }));
623
676
  }
624
677
  return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
625
678
  }
626
- // Enum/option field with served options → colored/iconed badge using the
627
- // served label (e.g. "Almacenable" instead of "storable").
679
+ // Enum/option field with served options → the SAME colored/iconed pill the
680
+ // table renders (shared `OptionBadge`): resolved color, thumbnail/icon and
681
+ // the localized option label (e.g. "Almacenable" instead of "storable").
628
682
  const opt = servedOption(field, value);
629
683
  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] }));
684
+ return (_jsx("div", { className: "py-1", children: _jsx(OptionBadge, { option: {
685
+ value: String(opt.value ?? value ?? ''),
686
+ label: opt.label,
687
+ color: opt.color ?? undefined,
688
+ icon: opt.icon ?? undefined,
689
+ image: opt.image ?? undefined,
690
+ }, getImageUrl: getImageUrl }) }));
691
+ }
692
+ // Array of scalars / label objects (e.g. github `labels`, tags, a
693
+ // group-badge list) → a row of pills, mirroring the table's `tags` /
694
+ // `relation-badge-list` cells. Checked before the structured-object branch
695
+ // so a flat label array never renders as a mini-table.
696
+ if (Array.isArray(value) &&
697
+ (renderAs === 'tags' ||
698
+ renderAs === 'relation-badge-list' ||
699
+ value.every((v) => v === null || typeof v !== 'object' || 'label' in v || 'name' in v))) {
700
+ return _jsx(BadgeListViewValue, { items: value, getImageUrl: getImageUrl });
701
+ }
702
+ // Status / badge / select display with no served option list — a bare enum
703
+ // token (e.g. a kanban `stage` like "backlog"). Render a colored pill with a
704
+ // semantic/value-derived color and a localized-or-humanized label, matching
705
+ // the table's `status`/`badge` cells.
706
+ if ((renderAs === 'status' ||
707
+ renderAs === 'badge' ||
708
+ renderAs === 'select' ||
709
+ renderAs === 'option') &&
710
+ value !== null &&
711
+ value !== undefined &&
712
+ typeof value !== 'object') {
713
+ return _jsx(StatusBadgeViewValue, { field: field, value: value, t: t });
636
714
  }
637
715
  // Structured value (jsonb column, e.g. fiscal_data) with no label/name/title
638
716
  // to surface — render readable key/value pairs instead of falling through to
@@ -641,10 +719,14 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
641
719
  return (_jsx(StructuredViewValue, { value: value, field: field, locale: i18n.language, t: t }));
642
720
  }
643
721
  const display = formatDisplayValue(value, field);
644
- if (field.type === 'textarea') {
645
- return (_jsx("p", { className: "text-sm whitespace-pre-wrap rounded-md bg-muted/40 p-3 min-h-[60px]", children: display }));
722
+ // Free text may embed URLs (a github body, notes, a long-text field). Turn
723
+ // them into rich chips / inline thumbnails with the shared linkifier instead
724
+ // of showing raw URLs — the rest of the text is preserved verbatim.
725
+ const hasUrl = display !== '—' && /(https?:\/\/|www\.)\S/i.test(display);
726
+ if (field.type === 'textarea' || renderAs === 'textarea' || renderAs === 'long-text') {
727
+ return (_jsx("p", { className: "text-sm whitespace-pre-wrap rounded-md bg-muted/40 p-3 min-h-[60px] [&_img]:my-1", children: hasUrl ? (_jsx(RichText, { text: display, getImageUrl: getImageUrl, imageHeight: 200 })) : (display) }));
646
728
  }
647
- return _jsx("p", { className: "text-sm py-1", children: display });
729
+ return (_jsx("p", { className: "text-sm py-1", children: hasUrl ? (_jsx(RichText, { text: display, getImageUrl: getImageUrl, imageHeight: 160 })) : (display) }));
648
730
  }
649
731
  // IconNameViewValue — read view for a column whose value is a lucide icon name
650
732
  // (an addon's `icon` column): the glyph plus the name, so the value stays
@@ -652,6 +734,44 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
652
734
  function IconNameViewValue({ name }) {
653
735
  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
736
  }
737
+ // StatusBadgeViewValue — a bare enum/status token (no served option list) as a
738
+ // colored pill: a semantic/value-derived color (same `statusColorFor` the table
739
+ // uses) plus a localized-or-humanized label. Mirrors the table's `status`/
740
+ // `badge` cell so a kanban `stage` ("backlog") reads as a colored, translated
741
+ // badge instead of the raw token.
742
+ function StatusBadgeViewValue({ field, value, t, }) {
743
+ const isDark = useIsDarkTheme();
744
+ const token = String(value);
745
+ // Prefer an explicit per-option color served on the field (metadata.stages /
746
+ // options), else derive a semantic color from the token.
747
+ const declared = field.options?.find((o) => String(o.value) === token);
748
+ const color = declared?.color || statusColorFor(token);
749
+ // Localized label: the option's already-localized label wins, then a manifest
750
+ // i18n key matching the raw token, then a humanized fallback.
751
+ const label = declared?.label ?? t(token, { defaultValue: humanizeToken(token) });
752
+ 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 }) }));
753
+ }
754
+ // BadgeListViewValue — an array of scalars / label objects as a row of pills
755
+ // (github `labels`, tags, a group-badge list). Objects carrying a `color` render
756
+ // as colored `OptionBadge`s; plain strings render as neutral secondary pills.
757
+ function BadgeListViewValue({ items, getImageUrl, }) {
758
+ if (!items || items.length === 0) {
759
+ return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
760
+ }
761
+ return (_jsx("div", { className: "flex flex-wrap gap-1 py-1", children: items.map((item, i) => {
762
+ if (item !== null && typeof item === 'object') {
763
+ const opt = {
764
+ value: String(item.value ?? item.id ?? item.name ?? item.label ?? i),
765
+ label: String(item.label ?? item.name ?? item.value ?? ''),
766
+ color: item.color ?? undefined,
767
+ icon: item.icon ?? undefined,
768
+ image: item.image ?? item.avatar ?? undefined,
769
+ };
770
+ return _jsx(OptionBadge, { option: opt, getImageUrl: getImageUrl }, i);
771
+ }
772
+ return (_jsx(Badge, { variant: "secondary", className: "w-fit", children: String(item) }, i));
773
+ }) }));
774
+ }
655
775
  // StructuredViewValue renders a jsonb object/array that has no resolvable label.
656
776
  // It delegates to the shared `CollectionCell` in `'inline'` mode so the detail
657
777
  // view gets the SAME pro rendering as the table: a declared `item_fields` schema
@@ -738,6 +858,7 @@ export function EditField({ field, value, onChange, record }) {
738
858
  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
859
  }
740
860
  function ImageUploadField({ field: _field, value, onChange }) {
861
+ const { t } = useTranslation();
741
862
  const api = useApi();
742
863
  const model = useContext(ModelContext);
743
864
  const getImageUrl = useContext(ImageUrlContext);
@@ -758,7 +879,7 @@ function ImageUploadField({ field: _field, value, onChange }) {
758
879
  onChange(url);
759
880
  }
760
881
  catch {
761
- toast.error('Error al subir imagen');
882
+ toast.error(t('dynamic.image_upload_error', { defaultValue: 'No se pudo subir la imagen' }));
762
883
  }
763
884
  finally {
764
885
  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;AAuC9C,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,CAqmBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}