@asteby/metacore-runtime-react 18.12.0 → 18.13.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,21 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bd619da: Activity/history polish + lucide icon cells:
8
+ - `ActivityValueRenderer`: backend-resolved entity objects ({name,avatar,email} users, {value,label} relations) render as an avatar/name chip instead of raw JSON — covers the "Created By" row in a record's history diff. Relation chips also unwrap resolved objects.
9
+ - `ActivityDiff`: a diff key now matches dotted display columns by base segment (`created_by` → `created_by.avatar`), inheriting the served label and rich renderer.
10
+ - `RecordHistory`: new optional `onOpenEvent(event)` prop — shows an "open in activity log" button per event so hosts can deep-link to `/activity/:id`.
11
+ - Image cells (`dynamic-columns` + record detail `ViewValue`): a value that is a lucide icon name (an addon's `icon` column, e.g. "Banknote") renders the glyph via `DynamicIcon` instead of a broken `<img>` (empty grey box). New `isLucideIconName` export.
12
+
13
+ ## 18.12.1
14
+
15
+ ### Patch Changes
16
+
17
+ - e568344: ViewValue: render structured jsonb values (objects/arrays without a label/name/title) as readable key→value pairs instead of "[object Object]" — e.g. a `fiscal_data` jsonb column on the record detail page. Plain objects become a humanized key/value list, primitive arrays a comma-joined line, nested structures a pretty-printed JSON block; empty structures render the "—" empty marker.
18
+
3
19
  ## 18.12.0
4
20
 
5
21
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"activity-diff.d.ts","sourceRoot":"","sources":["../src/activity-diff.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oCAAoC;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AA2ED;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAmJpD,CAAA"}
1
+ {"version":3,"file":"activity-diff.d.ts","sourceRoot":"","sources":["../src/activity-diff.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oCAAoC;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAiFD;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAmJpD,CAAA"}
@@ -54,7 +54,15 @@ function changedKeys(event) {
54
54
  return changed;
55
55
  }
56
56
  function resolveColumn(key, columns) {
57
- return columns?.find((c) => c.key === key);
57
+ if (!columns?.length)
58
+ return undefined;
59
+ const exact = columns.find((c) => c.key === key);
60
+ if (exact)
61
+ return exact;
62
+ // A diff key is the physical column (created_by); the served metadata may
63
+ // only carry the dotted display column for it (created_by.avatar). Match on
64
+ // the base segment so the diff cell inherits its label and rich renderer.
65
+ return columns.find((c) => typeof c.key === 'string' && c.key.includes('.') && c.key.split('.')[0] === key);
58
66
  }
59
67
  function resolveLabel(key, columns) {
60
68
  const col = resolveColumn(key, columns);
@@ -1 +1 @@
1
- {"version":3,"file":"activity-value-renderer.d.ts","sourceRoot":"","sources":["../src/activity-value-renderer.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAU9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAiD/C,MAAM,WAAW,0BAA0B;IACvC,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAA;IACd,+EAA+E;IAC/E,GAAG,CAAC,EAAE,gBAAgB,CAAA;IACtB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAwRtE,CAAA"}
1
+ {"version":3,"file":"activity-value-renderer.d.ts","sourceRoot":"","sources":["../src/activity-value-renderer.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAU9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AA0E/C,MAAM,WAAW,0BAA0B;IACvC,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAA;IACd,+EAA+E;IAC/E,GAAG,CAAC,EAAE,gBAAgB,CAAA;IACtB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAiRtE,CAAA"}
@@ -41,6 +41,21 @@ const statusColorFor = (value) => {
41
41
  return '#ef4444';
42
42
  return '#6b7280';
43
43
  };
44
+ // resolvedEntity — a diff snapshot value may be the backend-resolved sibling
45
+ // object ({value,label} relation, {name,avatar,email} user). Surface its human
46
+ // identity instead of raw JSON. Returns undefined for anything else.
47
+ function resolvedEntity(value) {
48
+ if (!value || typeof value !== 'object' || Array.isArray(value))
49
+ return undefined;
50
+ const v = value;
51
+ const name = v.name ?? v.label ?? v.title;
52
+ if (typeof name !== 'string' || name === '')
53
+ return undefined;
54
+ const avatar = typeof v.avatar === 'string' && v.avatar !== '' ? v.avatar : undefined;
55
+ const email = typeof v.email === 'string' && v.email !== '' ? v.email : undefined;
56
+ return { name, avatar, email };
57
+ }
58
+ const EntityChip = ({ entity }) => (_jsxs("span", { className: "inline-flex items-center gap-1.5", title: entity.email, children: [_jsxs(Avatar, { className: "h-5 w-5 rounded-full", children: [_jsx(AvatarImage, { src: entity.avatar ?? '', alt: entity.name }), _jsx(AvatarFallback, { className: "text-[8px] font-bold bg-primary/10 text-primary", children: getInitials(entity.name) })] }), _jsx("span", { className: "text-sm font-medium truncate", style: { maxWidth: 180 }, children: entity.name })] }));
44
59
  const useIsDarkTheme = () => {
45
60
  const [isDark, setIsDark] = React.useState(() => typeof document !== 'undefined' && document.documentElement.classList.contains('dark'));
46
61
  React.useEffect(() => {
@@ -66,9 +81,13 @@ export const ActivityValueRenderer = ({ value, col, timeZone, currency, locale =
66
81
  if (value === null || value === undefined || value === '') {
67
82
  return _jsx("span", { className: "text-muted-foreground", children: "\u2014" });
68
83
  }
69
- // No column metadata → plain string
84
+ // No column metadata → entity chip when the value is a resolved object,
85
+ // plain string otherwise.
70
86
  if (!col) {
71
87
  if (typeof value === 'object') {
88
+ const entity = resolvedEntity(value);
89
+ if (entity)
90
+ return _jsx(EntityChip, { entity: entity });
72
91
  return (_jsx("span", { className: "text-muted-foreground text-xs font-mono", children: JSON.stringify(value) }));
73
92
  }
74
93
  return _jsx("span", { className: "font-medium text-sm", children: String(value) });
@@ -177,7 +196,7 @@ export const ActivityValueRenderer = ({ value, col, timeZone, currency, locale =
177
196
  // Relation chip (FK / reference)
178
197
  // -----------------------------------------------------------------------
179
198
  if (renderAs === 'relation' || renderAs === 'reference' || col.ref) {
180
- const sv = String(value);
199
+ const sv = resolvedEntity(value)?.name ?? (typeof value === 'object' ? JSON.stringify(value) : String(value));
181
200
  const chipStyles = relationChipStyles(sv, { isDark });
182
201
  return (_jsx("span", { className: "inline-flex items-center rounded-md px-2 py-0.5 text-sm font-medium", style: { ...chipStyles, maxWidth: 180 }, title: sv, children: _jsx("span", { className: "truncate", children: sv }) }));
183
202
  }
@@ -186,10 +205,8 @@ export const ActivityValueRenderer = ({ value, col, timeZone, currency, locale =
186
205
  // in a diff snapshot the value is likely a string (name/email) or the object.
187
206
  // -----------------------------------------------------------------------
188
207
  if (renderAs === 'creator' || renderAs === 'user' || renderAs === 'avatar' || renderAs === 'search') {
189
- const name = typeof value === 'object' && value !== null
190
- ? String(value.name ?? value.label ?? JSON.stringify(value))
191
- : String(value);
192
- return (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsxs(Avatar, { className: "h-5 w-5 rounded-full", children: [_jsx(AvatarImage, { src: "", alt: name }), _jsx(AvatarFallback, { className: "text-[8px] font-bold bg-primary/10 text-primary", children: getInitials(name) })] }), _jsx("span", { className: "text-sm font-medium truncate", style: { maxWidth: 180 }, children: name })] }));
208
+ const entity = resolvedEntity(value) ?? { name: typeof value === 'object' ? JSON.stringify(value) : String(value) };
209
+ return _jsx(EntityChip, { entity: entity });
193
210
  }
194
211
  // -----------------------------------------------------------------------
195
212
  // Code / truncate-text / phone
@@ -201,9 +218,13 @@ export const ActivityValueRenderer = ({ value, col, timeZone, currency, locale =
201
218
  return _jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: display });
202
219
  }
203
220
  // -----------------------------------------------------------------------
204
- // Generic object fallback
221
+ // Generic object fallback — resolved entities render as a chip, the rest
222
+ // as raw JSON.
205
223
  // -----------------------------------------------------------------------
206
224
  if (typeof value === 'object') {
225
+ const entity = resolvedEntity(value);
226
+ if (entity)
227
+ return _jsx(EntityChip, { entity: entity });
207
228
  return (_jsx("span", { className: "text-muted-foreground text-xs font-mono", children: JSON.stringify(value) }));
208
229
  }
209
230
  // -----------------------------------------------------------------------
@@ -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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"}
@@ -24,6 +24,7 @@ import { DynamicRelations } from '../dynamic-relations';
24
24
  import { useOptionsResolver } from '../use-options-resolver';
25
25
  import { getFieldRef } from '../dynamic-form-schema';
26
26
  import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
27
+ import { DynamicIcon, isLucideIconName } from '../dynamic-icon';
27
28
  import { humanizeToken } from '../dynamic-columns-helpers';
28
29
  import { formatDateCell } from '../dynamic-columns';
29
30
  import { ImageUrlContext, identityImageUrl } from '../image-url-context';
@@ -123,6 +124,9 @@ function formatDisplayValue(rawValue, field) {
123
124
  // when no declared option matches the value.
124
125
  return match?.label ?? humanizeToken(value);
125
126
  }
127
+ // Structured value with no label — JSON beats "[object Object]".
128
+ if (typeof value === 'object')
129
+ return JSON.stringify(value);
126
130
  return String(value);
127
131
  }
128
132
  const MODE_CONFIG = {
@@ -531,8 +535,18 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
531
535
  return value ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-5 w-5 rounded-full border shadow-sm", style: { backgroundColor: value } }), _jsx("span", { className: "text-sm", children: value })] })) : (_jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "-" }));
532
536
  }
533
537
  if (field.type === 'image') {
538
+ if (isLucideIconName(value)) {
539
+ return _jsx(IconNameViewValue, { name: value });
540
+ }
534
541
  return value ? (_jsx("img", { src: getImageUrl(String(value)), alt: field.label, className: "h-16 w-16 rounded-lg object-cover border" })) : (_jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "Sin imagen" }));
535
542
  }
543
+ // Icon-name column served as plain text (the table infers cellStyle image,
544
+ // but the detail/modal field keeps the storage type): render the glyph.
545
+ if (isLucideIconName(value) &&
546
+ typeof field.key === 'string' &&
547
+ (field.key === 'icon' || field.key.endsWith('_icon'))) {
548
+ return _jsx(IconNameViewValue, { name: value });
549
+ }
536
550
  if (field.type === 'url' && value) {
537
551
  return (_jsx("a", { href: value, target: "_blank", rel: "noreferrer", className: "text-sm text-primary hover:underline truncate", children: value }));
538
552
  }
@@ -574,12 +588,44 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
574
588
  };
575
589
  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] }));
576
590
  }
591
+ // Structured value (jsonb column, e.g. fiscal_data) with no label/name/title
592
+ // to surface — render readable key/value pairs instead of falling through to
593
+ // String(value) ("[object Object]").
594
+ if (value !== null && typeof value === 'object') {
595
+ return _jsx(StructuredViewValue, { value: value });
596
+ }
577
597
  const display = formatDisplayValue(value, field);
578
598
  if (field.type === 'textarea') {
579
599
  return (_jsx("p", { className: "text-sm whitespace-pre-wrap rounded-md bg-muted/40 p-3 min-h-[60px]", children: display }));
580
600
  }
581
601
  return _jsx("p", { className: "text-sm py-1", children: display });
582
602
  }
603
+ // IconNameViewValue — read view for a column whose value is a lucide icon name
604
+ // (an addon's `icon` column): the glyph plus the name, so the value stays
605
+ // copyable/recognizable next to its rendering.
606
+ function IconNameViewValue({ name }) {
607
+ 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 })] }));
608
+ }
609
+ // StructuredViewValue renders a jsonb object/array that has no resolvable label:
610
+ // plain objects become a key→value list (keys humanized), primitive arrays a
611
+ // comma-joined line, and anything deeper a pretty-printed JSON block. Empty
612
+ // structures render the same "—" marker as null scalars.
613
+ function StructuredViewValue({ value }) {
614
+ if (Array.isArray(value)) {
615
+ if (value.length === 0) {
616
+ return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
617
+ }
618
+ if (value.every(v => v === null || typeof v !== 'object')) {
619
+ return _jsx("p", { className: "text-sm py-1", children: value.map(v => String(v ?? '—')).join(', ') });
620
+ }
621
+ return (_jsx("pre", { className: "text-xs whitespace-pre-wrap rounded-md bg-muted/40 p-3 overflow-x-auto", children: JSON.stringify(value, null, 2) }));
622
+ }
623
+ const entries = Object.entries(value).filter(([, v]) => v !== null && v !== undefined && v !== '');
624
+ if (entries.length === 0) {
625
+ return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
626
+ }
627
+ return (_jsx("dl", { className: "text-sm py-1 space-y-0.5", children: entries.map(([k, v]) => (_jsxs("div", { className: "flex gap-2", children: [_jsxs("dt", { className: "text-muted-foreground shrink-0", children: [humanizeToken(k), ":"] }), _jsx("dd", { className: "break-words", children: typeof v === 'object' ? JSON.stringify(v) : String(v) })] }, k))) }));
628
+ }
583
629
  function EditField({ field, value, onChange }) {
584
630
  if (field.type === 'boolean') {
585
631
  return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, onCheckedChange: onChange }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
@@ -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;AAgC9C,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;AAqKD;;;;;;;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,CAinBnB;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;AAgC9C,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;AAqKD;;;;;;;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,CA2nBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -23,7 +23,7 @@ import { generateBadgeStyles, getInitials, optionColor, relationChipStyles, } fr
23
23
  import { Progress } from './dialogs/_primitives';
24
24
  import { humanizeToken } from './dynamic-columns-helpers';
25
25
  import { OptionsContext } from './options-context';
26
- import { DynamicIcon } from './dynamic-icon';
26
+ import { DynamicIcon, isLucideIconName } from './dynamic-icon';
27
27
  import { isNilUuid, normalizeNilUuid } from './nil-uuid';
28
28
  import { isColumnVisibleInTable } from './column-visibility';
29
29
  const defaultGetImageUrl = (path) => path;
@@ -722,6 +722,12 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
722
722
  : null);
723
723
  if (!imageValue)
724
724
  return _jsx("span", { className: "text-muted-foreground", children: "-" });
725
+ // Lucide icon name, not an image path (e.g. an addon's
726
+ // `icon` column seeded as "Banknote") — render the glyph;
727
+ // an <img> here would 404 into an empty grey box.
728
+ if (isLucideIconName(imageValue)) {
729
+ return (_jsx("div", { className: "h-10 w-10 flex items-center justify-center rounded bg-muted", children: _jsx(DynamicIcon, { name: imageValue, className: "h-5 w-5" }) }));
730
+ }
725
731
  return (_jsx("div", { className: "h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center", children: _jsx("img", { src: getImageUrl(String(imageValue)), alt: "Thumbnail", className: "h-full w-full object-contain", onError: (e) => {
726
732
  ;
727
733
  e.currentTarget.style.display = 'none';
@@ -3,4 +3,5 @@ export interface DynamicIconProps {
3
3
  className?: string;
4
4
  }
5
5
  export declare function DynamicIcon({ name, className }: DynamicIconProps): import("react").JSX.Element | null;
6
+ export declare function isLucideIconName(value: unknown): value is string;
6
7
  //# sourceMappingURL=dynamic-icon.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-icon.d.ts","sourceRoot":"","sources":["../src/dynamic-icon.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,gBAAgB,sCAIhE"}
1
+ {"version":3,"file":"dynamic-icon.d.ts","sourceRoot":"","sources":["../src/dynamic-icon.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,gBAAgB,sCAIhE;AAQD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAIhE"}
@@ -9,3 +9,16 @@ export function DynamicIcon({ name, className }) {
9
9
  return null;
10
10
  return _jsx(Icon, { className: className });
11
11
  }
12
+ // isLucideIconName — true when a string is a lucide-react icon name
13
+ // ("Banknote", "CreditCard"). Lets image-ish renderers tell an icon name apart
14
+ // from an image path/URL: addons declare icons by lucide slug (same convention
15
+ // as OptionDef.icon), so a column inferred as `image` may carry one. Path-like
16
+ // strings (slash, dot, scheme) are rejected before the registry lookup; "Icon"
17
+ // itself is the generic base component, not a real glyph.
18
+ export function isLucideIconName(value) {
19
+ if (typeof value !== 'string' || value === '' || value === 'Icon')
20
+ return false;
21
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(value))
22
+ return false;
23
+ return Boolean(icons[value]);
24
+ }
@@ -31,6 +31,12 @@ export interface RecordHistoryProps {
31
31
  locale?: string;
32
32
  /** Class applied to the root element. */
33
33
  className?: string;
34
+ /**
35
+ * When provided, each event header shows an "open in activity log" button
36
+ * that invokes this with the event — the host navigates to its activity
37
+ * detail page (e.g. `/activity/:id`). Omitted → no button.
38
+ */
39
+ onOpenEvent?: (event: ActivityEvent) => void;
34
40
  }
35
41
  /**
36
42
  * Shows the full activity history of a single record as a vertical timeline.
@@ -1 +1 @@
1
- {"version":3,"file":"record-history.d.ts","sourceRoot":"","sources":["../src/record-history.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAc9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAOpD,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,MAAM,EAAE,aAAa,EAAE,CAAA;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAoCD;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAoJtD,CAAA"}
1
+ {"version":3,"file":"record-history.d.ts","sourceRoot":"","sources":["../src/record-history.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAc9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAOpD,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,MAAM,EAAE,aAAa,EAAE,CAAA;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;CAC/C;AAoCD;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA6KtD,CAAA"}
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import * as React from 'react';
14
14
  import { formatDistanceToNow } from 'date-fns';
15
15
  import { es, enUS } from 'date-fns/locale';
16
- import { ChevronDown, ChevronRight, Clock } from 'lucide-react';
16
+ import { ChevronDown, ChevronRight, Clock, ExternalLink } from 'lucide-react';
17
17
  import { cn } from '@asteby/metacore-ui/lib';
18
18
  import { Avatar, AvatarFallback, Badge, Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@asteby/metacore-ui/primitives';
19
19
  import { getInitials } from '@asteby/metacore-ui/lib';
@@ -51,7 +51,7 @@ function actionDotColor(action) {
51
51
  * Each event is collapsible — the header shows actor + time; expanding reveals
52
52
  * the <ActivityDiff> with field-level changes.
53
53
  */
54
- export const RecordHistory = ({ events, columns, timeZone, currency, locale = 'es', className, }) => {
54
+ export const RecordHistory = ({ events, columns, timeZone, currency, locale = 'es', className, onOpenEvent, }) => {
55
55
  const dateLocale = locale === 'en' ? enUS : es;
56
56
  // Sort: most recent first
57
57
  const sorted = React.useMemo(() => [...events].sort((a, b) => new Date(b.occurred_at).getTime() - new Date(a.occurred_at).getTime()), [events]);
@@ -94,6 +94,15 @@ export const RecordHistory = ({ events, columns, timeZone, currency, locale = 'e
94
94
  return event.occurred_at;
95
95
  }
96
96
  })();
97
- return (_jsx(Collapsible, { open: isOpen, onOpenChange: () => toggle(event.id), children: _jsxs("div", { className: "relative", children: [_jsx("span", { className: "absolute -left-5 top-3.5 h-2.5 w-2.5 rounded-full border-2 border-background -translate-x-[4px]", style: { background: dotColor }, "aria-hidden": "true" }), _jsxs("div", { className: "rounded-lg border border-border/60 bg-card overflow-hidden", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-muted/30 transition-colors", children: [_jsx(Avatar, { className: "h-7 w-7 rounded-full shrink-0", children: _jsx(AvatarFallback, { className: "text-[9px] font-bold bg-primary/10 text-primary", children: getInitials(actor) }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "text-sm font-semibold text-foreground truncate", children: actor }), _jsx("span", { className: "text-sm text-muted-foreground", children: actionLabel(event.action) })] }), _jsxs("div", { className: "flex items-center gap-1.5 mt-0.5", children: [_jsx(Clock, { className: "h-3 w-3 text-muted-foreground/60 shrink-0" }), _jsx("span", { className: "text-xs text-muted-foreground", title: fullDate, children: timeAgo }), event.addon_key && (_jsx(Badge, { variant: "outline", className: "text-[10px] px-1.5 py-0 h-4 ml-1", children: event.addon_key }))] })] }), _jsx("span", { className: "shrink-0 text-muted-foreground", children: isOpen ? (_jsx(ChevronDown, { className: "h-4 w-4" })) : (_jsx(ChevronRight, { className: "h-4 w-4" })) })] }) }), _jsx(CollapsibleContent, { children: _jsx("div", { className: "border-t border-border/40 px-4 py-3", children: _jsx(ActivityDiff, { event: event, columns: columns, timeZone: timeZone, currency: currency, locale: locale }) }) })] })] }) }, event.id));
97
+ return (_jsx(Collapsible, { open: isOpen, onOpenChange: () => toggle(event.id), children: _jsxs("div", { className: "relative", children: [_jsx("span", { className: "absolute -left-5 top-3.5 h-2.5 w-2.5 rounded-full border-2 border-background -translate-x-[4px]", style: { background: dotColor }, "aria-hidden": "true" }), _jsxs("div", { className: "rounded-lg border border-border/60 bg-card overflow-hidden", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-muted/30 transition-colors", children: [_jsx(Avatar, { className: "h-7 w-7 rounded-full shrink-0", children: _jsx(AvatarFallback, { className: "text-[9px] font-bold bg-primary/10 text-primary", children: getInitials(actor) }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "text-sm font-semibold text-foreground truncate", children: actor }), _jsx("span", { className: "text-sm text-muted-foreground", children: actionLabel(event.action) })] }), _jsxs("div", { className: "flex items-center gap-1.5 mt-0.5", children: [_jsx(Clock, { className: "h-3 w-3 text-muted-foreground/60 shrink-0" }), _jsx("span", { className: "text-xs text-muted-foreground", title: fullDate, children: timeAgo }), event.addon_key && (_jsx(Badge, { variant: "outline", className: "text-[10px] px-1.5 py-0 h-4 ml-1", children: event.addon_key }))] })] }), onOpenEvent && (_jsx("span", { role: "button", tabIndex: 0, "aria-label": "Ver en registro de actividad", title: "Ver en registro de actividad", className: "shrink-0 rounded-md p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted/60 transition-colors cursor-pointer", onClick: (e) => {
98
+ e.stopPropagation();
99
+ onOpenEvent(event);
100
+ }, onKeyDown: (e) => {
101
+ if (e.key === 'Enter' || e.key === ' ') {
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+ onOpenEvent(event);
105
+ }
106
+ }, children: _jsx(ExternalLink, { className: "h-3.5 w-3.5" }) })), _jsx("span", { className: "shrink-0 text-muted-foreground", children: isOpen ? (_jsx(ChevronDown, { className: "h-4 w-4" })) : (_jsx(ChevronRight, { className: "h-4 w-4" })) })] }) }), _jsx(CollapsibleContent, { children: _jsx("div", { className: "border-t border-border/40 px-4 py-3", children: _jsx(ActivityDiff, { event: event, columns: columns, timeZone: timeZone, currency: currency, locale: locale }) }) })] })] }) }, event.id));
98
107
  }) })] }));
99
108
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.12.0",
3
+ "version": "18.13.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -105,7 +105,13 @@ function changedKeys(event: ActivityEvent): Set<string> {
105
105
  }
106
106
 
107
107
  function resolveColumn(key: string, columns?: ColumnDefinition[]): ColumnDefinition | undefined {
108
- return columns?.find((c) => c.key === key)
108
+ if (!columns?.length) return undefined
109
+ const exact = columns.find((c) => c.key === key)
110
+ if (exact) return exact
111
+ // A diff key is the physical column (created_by); the served metadata may
112
+ // only carry the dotted display column for it (created_by.avatar). Match on
113
+ // the base segment so the diff cell inherits its label and rich renderer.
114
+ return columns.find((c) => typeof c.key === 'string' && c.key.includes('.') && c.key.split('.')[0] === key)
109
115
  }
110
116
 
111
117
  function resolveLabel(key: string, columns?: ColumnDefinition[]): string {
@@ -51,6 +51,31 @@ const statusColorFor = (value: string): string => {
51
51
  return '#6b7280'
52
52
  }
53
53
 
54
+ // resolvedEntity — a diff snapshot value may be the backend-resolved sibling
55
+ // object ({value,label} relation, {name,avatar,email} user). Surface its human
56
+ // identity instead of raw JSON. Returns undefined for anything else.
57
+ function resolvedEntity(value: unknown): { name: string; avatar?: string; email?: string } | undefined {
58
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
59
+ const v = value as Record<string, unknown>
60
+ const name = v.name ?? v.label ?? v.title
61
+ if (typeof name !== 'string' || name === '') return undefined
62
+ const avatar = typeof v.avatar === 'string' && v.avatar !== '' ? v.avatar : undefined
63
+ const email = typeof v.email === 'string' && v.email !== '' ? v.email : undefined
64
+ return { name, avatar, email }
65
+ }
66
+
67
+ const EntityChip: React.FC<{ entity: { name: string; avatar?: string; email?: string } }> = ({ entity }) => (
68
+ <span className="inline-flex items-center gap-1.5" title={entity.email}>
69
+ <Avatar className="h-5 w-5 rounded-full">
70
+ <AvatarImage src={entity.avatar ?? ''} alt={entity.name} />
71
+ <AvatarFallback className="text-[8px] font-bold bg-primary/10 text-primary">
72
+ {getInitials(entity.name)}
73
+ </AvatarFallback>
74
+ </Avatar>
75
+ <span className="text-sm font-medium truncate" style={{ maxWidth: 180 }}>{entity.name}</span>
76
+ </span>
77
+ )
78
+
54
79
  const useIsDarkTheme = () => {
55
80
  const [isDark, setIsDark] = React.useState(() =>
56
81
  typeof document !== 'undefined' && document.documentElement.classList.contains('dark'),
@@ -103,9 +128,12 @@ export const ActivityValueRenderer: React.FC<ActivityValueRendererProps> = ({
103
128
  return <span className="text-muted-foreground">—</span>
104
129
  }
105
130
 
106
- // No column metadata → plain string
131
+ // No column metadata → entity chip when the value is a resolved object,
132
+ // plain string otherwise.
107
133
  if (!col) {
108
134
  if (typeof value === 'object') {
135
+ const entity = resolvedEntity(value)
136
+ if (entity) return <EntityChip entity={entity} />
109
137
  return (
110
138
  <span className="text-muted-foreground text-xs font-mono">
111
139
  {JSON.stringify(value)}
@@ -304,7 +332,7 @@ export const ActivityValueRenderer: React.FC<ActivityValueRendererProps> = ({
304
332
  // -----------------------------------------------------------------------
305
333
 
306
334
  if (renderAs === 'relation' || renderAs === 'reference' || col.ref) {
307
- const sv = String(value)
335
+ const sv = resolvedEntity(value)?.name ?? (typeof value === 'object' ? JSON.stringify(value) : String(value))
308
336
  const chipStyles = relationChipStyles(sv, { isDark })
309
337
  return (
310
338
  <span
@@ -323,21 +351,8 @@ export const ActivityValueRenderer: React.FC<ActivityValueRendererProps> = ({
323
351
  // -----------------------------------------------------------------------
324
352
 
325
353
  if (renderAs === 'creator' || renderAs === 'user' || renderAs === 'avatar' || renderAs === 'search') {
326
- const name =
327
- typeof value === 'object' && value !== null
328
- ? String((value as any).name ?? (value as any).label ?? JSON.stringify(value))
329
- : String(value)
330
- return (
331
- <span className="inline-flex items-center gap-1.5">
332
- <Avatar className="h-5 w-5 rounded-full">
333
- <AvatarImage src="" alt={name} />
334
- <AvatarFallback className="text-[8px] font-bold bg-primary/10 text-primary">
335
- {getInitials(name)}
336
- </AvatarFallback>
337
- </Avatar>
338
- <span className="text-sm font-medium truncate" style={{ maxWidth: 180 }}>{name}</span>
339
- </span>
340
- )
354
+ const entity = resolvedEntity(value) ?? { name: typeof value === 'object' ? JSON.stringify(value) : String(value) }
355
+ return <EntityChip entity={entity} />
341
356
  }
342
357
 
343
358
  // -----------------------------------------------------------------------
@@ -352,10 +367,13 @@ export const ActivityValueRenderer: React.FC<ActivityValueRendererProps> = ({
352
367
  }
353
368
 
354
369
  // -----------------------------------------------------------------------
355
- // Generic object fallback
370
+ // Generic object fallback — resolved entities render as a chip, the rest
371
+ // as raw JSON.
356
372
  // -----------------------------------------------------------------------
357
373
 
358
374
  if (typeof value === 'object') {
375
+ const entity = resolvedEntity(value)
376
+ if (entity) return <EntityChip entity={entity} />
359
377
  return (
360
378
  <span className="text-muted-foreground text-xs font-mono">
361
379
  {JSON.stringify(value)}
@@ -53,6 +53,7 @@ import { DynamicRelations } from '../dynamic-relations'
53
53
  import { useOptionsResolver, type ResolvedOption } from '../use-options-resolver'
54
54
  import { getFieldRef } from '../dynamic-form-schema'
55
55
  import { isNilUuid, normalizeNilUuid } from '../nil-uuid'
56
+ import { DynamicIcon, isLucideIconName } from '../dynamic-icon'
56
57
  import { humanizeToken } from '../dynamic-columns-helpers'
57
58
  import { formatDateCell } from '../dynamic-columns'
58
59
  import type { ActionFieldDef, RelationMeta } from '../types'
@@ -323,6 +324,9 @@ function formatDisplayValue(rawValue: any, field: FieldDef): string {
323
324
  return match?.label ?? humanizeToken(value)
324
325
  }
325
326
 
327
+ // Structured value with no label — JSON beats "[object Object]".
328
+ if (typeof value === 'object') return JSON.stringify(value)
329
+
326
330
  return String(value)
327
331
  }
328
332
 
@@ -974,6 +978,9 @@ export function ViewValue({
974
978
  }
975
979
 
976
980
  if (field.type === 'image') {
981
+ if (isLucideIconName(value)) {
982
+ return <IconNameViewValue name={value} />
983
+ }
977
984
  return value ? (
978
985
  <img src={getImageUrl(String(value))} alt={field.label} className="h-16 w-16 rounded-lg object-cover border" />
979
986
  ) : (
@@ -981,6 +988,16 @@ export function ViewValue({
981
988
  )
982
989
  }
983
990
 
991
+ // Icon-name column served as plain text (the table infers cellStyle image,
992
+ // but the detail/modal field keeps the storage type): render the glyph.
993
+ if (
994
+ isLucideIconName(value) &&
995
+ typeof field.key === 'string' &&
996
+ (field.key === 'icon' || field.key.endsWith('_icon'))
997
+ ) {
998
+ return <IconNameViewValue name={value} />
999
+ }
1000
+
984
1001
  if (field.type === 'url' && value) {
985
1002
  return (
986
1003
  <a
@@ -1048,6 +1065,13 @@ export function ViewValue({
1048
1065
  )
1049
1066
  }
1050
1067
 
1068
+ // Structured value (jsonb column, e.g. fiscal_data) with no label/name/title
1069
+ // to surface — render readable key/value pairs instead of falling through to
1070
+ // String(value) ("[object Object]").
1071
+ if (value !== null && typeof value === 'object') {
1072
+ return <StructuredViewValue value={value} />
1073
+ }
1074
+
1051
1075
  const display = formatDisplayValue(value, field)
1052
1076
 
1053
1077
  if (field.type === 'textarea') {
@@ -1061,6 +1085,58 @@ export function ViewValue({
1061
1085
  return <p className="text-sm py-1">{display}</p>
1062
1086
  }
1063
1087
 
1088
+ // IconNameViewValue — read view for a column whose value is a lucide icon name
1089
+ // (an addon's `icon` column): the glyph plus the name, so the value stays
1090
+ // copyable/recognizable next to its rendering.
1091
+ function IconNameViewValue({ name }: { name: string }) {
1092
+ return (
1093
+ <div className="flex items-center gap-2 py-1">
1094
+ <div className="h-8 w-8 flex items-center justify-center rounded bg-muted">
1095
+ <DynamicIcon name={name} className="h-4 w-4" />
1096
+ </div>
1097
+ <span className="text-sm text-muted-foreground">{name}</span>
1098
+ </div>
1099
+ )
1100
+ }
1101
+
1102
+ // StructuredViewValue renders a jsonb object/array that has no resolvable label:
1103
+ // plain objects become a key→value list (keys humanized), primitive arrays a
1104
+ // comma-joined line, and anything deeper a pretty-printed JSON block. Empty
1105
+ // structures render the same "—" marker as null scalars.
1106
+ function StructuredViewValue({ value }: { value: any }) {
1107
+ if (Array.isArray(value)) {
1108
+ if (value.length === 0) {
1109
+ return <p className="text-sm py-1 text-muted-foreground">—</p>
1110
+ }
1111
+ if (value.every(v => v === null || typeof v !== 'object')) {
1112
+ return <p className="text-sm py-1">{value.map(v => String(v ?? '—')).join(', ')}</p>
1113
+ }
1114
+ return (
1115
+ <pre className="text-xs whitespace-pre-wrap rounded-md bg-muted/40 p-3 overflow-x-auto">
1116
+ {JSON.stringify(value, null, 2)}
1117
+ </pre>
1118
+ )
1119
+ }
1120
+ const entries = Object.entries(value).filter(
1121
+ ([, v]) => v !== null && v !== undefined && v !== '',
1122
+ )
1123
+ if (entries.length === 0) {
1124
+ return <p className="text-sm py-1 text-muted-foreground">—</p>
1125
+ }
1126
+ return (
1127
+ <dl className="text-sm py-1 space-y-0.5">
1128
+ {entries.map(([k, v]) => (
1129
+ <div key={k} className="flex gap-2">
1130
+ <dt className="text-muted-foreground shrink-0">{humanizeToken(k)}:</dt>
1131
+ <dd className="break-words">
1132
+ {typeof v === 'object' ? JSON.stringify(v) : String(v)}
1133
+ </dd>
1134
+ </div>
1135
+ ))}
1136
+ </dl>
1137
+ )
1138
+ }
1139
+
1064
1140
  function EditField({ field, value, onChange }: {
1065
1141
  field: FieldDef
1066
1142
  value: any
@@ -44,7 +44,7 @@ import {
44
44
  import { Progress } from './dialogs/_primitives'
45
45
  import { humanizeToken } from './dynamic-columns-helpers'
46
46
  import { OptionsContext } from './options-context'
47
- import { DynamicIcon } from './dynamic-icon'
47
+ import { DynamicIcon, isLucideIconName } from './dynamic-icon'
48
48
  import { isNilUuid, normalizeNilUuid } from './nil-uuid'
49
49
  import type { TableMetadata, ColumnDefinition } from './types'
50
50
  import { isColumnVisibleInTable } from './column-visibility'
@@ -1147,6 +1147,16 @@ export function makeDefaultGetDynamicColumns(
1147
1147
  ? row.original.media.find((m: any) => m.type === 'image')?.url
1148
1148
  : null)
1149
1149
  if (!imageValue) return <span className="text-muted-foreground">-</span>
1150
+ // Lucide icon name, not an image path (e.g. an addon's
1151
+ // `icon` column seeded as "Banknote") — render the glyph;
1152
+ // an <img> here would 404 into an empty grey box.
1153
+ if (isLucideIconName(imageValue)) {
1154
+ return (
1155
+ <div className="h-10 w-10 flex items-center justify-center rounded bg-muted">
1156
+ <DynamicIcon name={imageValue} className="h-5 w-5" />
1157
+ </div>
1158
+ )
1159
+ }
1150
1160
  return (
1151
1161
  <div className="h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center">
1152
1162
  <img
@@ -13,3 +13,15 @@ export function DynamicIcon({ name, className }: DynamicIconProps) {
13
13
  if (!Icon) return null
14
14
  return <Icon className={className} />
15
15
  }
16
+
17
+ // isLucideIconName — true when a string is a lucide-react icon name
18
+ // ("Banknote", "CreditCard"). Lets image-ish renderers tell an icon name apart
19
+ // from an image path/URL: addons declare icons by lucide slug (same convention
20
+ // as OptionDef.icon), so a column inferred as `image` may carry one. Path-like
21
+ // strings (slash, dot, scheme) are rejected before the registry lookup; "Icon"
22
+ // itself is the generic base component, not a real glyph.
23
+ export function isLucideIconName(value: unknown): value is string {
24
+ if (typeof value !== 'string' || value === '' || value === 'Icon') return false
25
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(value)) return false
26
+ return Boolean((icons as unknown as Record<string, unknown>)[value])
27
+ }
@@ -13,7 +13,7 @@
13
13
  import * as React from 'react'
14
14
  import { formatDistanceToNow } from 'date-fns'
15
15
  import { es, enUS } from 'date-fns/locale'
16
- import { ChevronDown, ChevronRight, User, Clock } from 'lucide-react'
16
+ import { ChevronDown, ChevronRight, Clock, ExternalLink } from 'lucide-react'
17
17
  import { cn } from '@asteby/metacore-ui/lib'
18
18
  import {
19
19
  Avatar,
@@ -51,6 +51,12 @@ export interface RecordHistoryProps {
51
51
  locale?: string
52
52
  /** Class applied to the root element. */
53
53
  className?: string
54
+ /**
55
+ * When provided, each event header shows an "open in activity log" button
56
+ * that invokes this with the event — the host navigates to its activity
57
+ * detail page (e.g. `/activity/:id`). Omitted → no button.
58
+ */
59
+ onOpenEvent?: (event: ActivityEvent) => void
54
60
  }
55
61
 
56
62
  // ---------------------------------------------------------------------------
@@ -99,6 +105,7 @@ export const RecordHistory: React.FC<RecordHistoryProps> = ({
99
105
  currency,
100
106
  locale = 'es',
101
107
  className,
108
+ onOpenEvent,
102
109
  }) => {
103
110
  const dateLocale = locale === 'en' ? enUS : es
104
111
 
@@ -209,6 +216,30 @@ export const RecordHistory: React.FC<RecordHistoryProps> = ({
209
216
  </div>
210
217
  </div>
211
218
 
219
+ {/* Open the event's detail page in the activity log */}
220
+ {onOpenEvent && (
221
+ <span
222
+ role="button"
223
+ tabIndex={0}
224
+ aria-label="Ver en registro de actividad"
225
+ title="Ver en registro de actividad"
226
+ className="shrink-0 rounded-md p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted/60 transition-colors cursor-pointer"
227
+ onClick={(e) => {
228
+ e.stopPropagation()
229
+ onOpenEvent(event)
230
+ }}
231
+ onKeyDown={(e) => {
232
+ if (e.key === 'Enter' || e.key === ' ') {
233
+ e.preventDefault()
234
+ e.stopPropagation()
235
+ onOpenEvent(event)
236
+ }
237
+ }}
238
+ >
239
+ <ExternalLink className="h-3.5 w-3.5" />
240
+ </span>
241
+ )}
242
+
212
243
  {/* Expand/collapse chevron */}
213
244
  <span className="shrink-0 text-muted-foreground">
214
245
  {isOpen ? (