@asteby/metacore-runtime-react 18.13.0 → 18.13.2

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,17 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.13.2
4
+
5
+ ### Patch Changes
6
+
7
+ - b37e1d7: RecordHistory: event headers show the actor's photo, not just initials — `ActivityEvent` gains `actor_avatar` and the component renders it via the new `resolveAvatarUrl` prop (host resolves the storage path, e.g. ops' `getStorageUrl(path, 'avatars')`; identity fallback for absolute paths).
8
+
9
+ ## 18.13.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 6b8f7b2: ActivityDiff: drop noise rows from history diffs — raw FK keys (`created_by_id`) are hidden when their resolved sibling (`created_by: {name,…}`) is present in the same snapshot (covers before/after and the changes {from,to} shape), and `deleted_at` joins the meta-key filter alongside id/created_at/updated_at/organization_id.
14
+
3
15
  ## 18.13.0
4
16
 
5
17
  ### Minor Changes
@@ -25,6 +25,8 @@ export interface ActivityEvent {
25
25
  correlation_id?: string | null;
26
26
  actor_id?: string | null;
27
27
  actor_label?: string | null;
28
+ /** Storage path of the actor's avatar image, when the backend resolves one. */
29
+ actor_avatar?: string | null;
28
30
  addon_key: string;
29
31
  model: string;
30
32
  record_id: string;
@@ -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;AAiFD;;;;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,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,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;AAuGD;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAmJpD,CAAA"}
@@ -23,20 +23,42 @@ import { ActivityValueRenderer } from './activity-value-renderer';
23
23
  // ---------------------------------------------------------------------------
24
24
  // Helpers
25
25
  // ---------------------------------------------------------------------------
26
+ // Meta-level keys that are always present and never meaningful in a
27
+ // human-readable diff.
28
+ const META_KEYS = new Set(['id', 'created_at', 'updated_at', 'organization_id', 'org_id', 'deleted_at']);
29
+ /** True when an object is a backend-resolved sibling ({value,label} relation, {name,…} user). */
30
+ function isResolvedObject(v) {
31
+ if (!v || typeof v !== 'object' || Array.isArray(v))
32
+ return false;
33
+ const o = v;
34
+ return typeof o.label === 'string' || typeof o.name === 'string';
35
+ }
26
36
  /** Returns all field keys that appear in the diff. */
27
37
  function diffKeys(event) {
28
- if (event.changes && Object.keys(event.changes).length > 0) {
29
- return Object.keys(event.changes);
30
- }
31
38
  const before = event.before ?? {};
32
39
  const after = event.after ?? {};
33
- const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
34
- // Filter out meta-level keys that are always present and rarely meaningful
35
- // in a human-readable diff (id, created_at, updated_at, organization_id).
36
- const META = new Set(['id', 'created_at', 'updated_at', 'organization_id', 'org_id']);
37
- keys.forEach((k) => { if (META.has(k))
38
- keys.delete(k); });
39
- return Array.from(keys);
40
+ const source = event.changes && Object.keys(event.changes).length > 0
41
+ ? new Set(Object.keys(event.changes))
42
+ : new Set([...Object.keys(before), ...Object.keys(after)]);
43
+ META_KEYS.forEach((k) => source.delete(k));
44
+ // A resolved FK appears twice: the raw UUID key (created_by_id) and the
45
+ // resolved sibling (created_by: {name/label,…}). Drop the raw key — the
46
+ // sibling row already shows the human value. The sibling's value may live
47
+ // in before/after or inside changes[sibling] ({from,to}/{before,after}).
48
+ const siblingResolved = (sibling) => {
49
+ if (isResolvedObject(before[sibling]) || isResolvedObject(after[sibling]))
50
+ return true;
51
+ const ch = event.changes?.[sibling];
52
+ if (!ch || typeof ch !== 'object')
53
+ return false;
54
+ return isResolvedObject(ch.from) || isResolvedObject(ch.to) || isResolvedObject(ch.before) || isResolvedObject(ch.after);
55
+ };
56
+ return Array.from(source).filter((k) => {
57
+ if (!k.endsWith('_id'))
58
+ return true;
59
+ const sibling = k.slice(0, -3);
60
+ return !(source.has(sibling) && siblingResolved(sibling));
61
+ });
40
62
  }
41
63
  /** Returns the set of keys where the value actually changed. */
42
64
  function changedKeys(event) {
@@ -37,6 +37,12 @@ export interface RecordHistoryProps {
37
37
  * detail page (e.g. `/activity/:id`). Omitted → no button.
38
38
  */
39
39
  onOpenEvent?: (event: ActivityEvent) => void;
40
+ /**
41
+ * Resolves an event's `actor_avatar` storage path to a fetchable URL
42
+ * (e.g. ops' `getStorageUrl(path, 'avatars')`). Identity when omitted —
43
+ * fine for absolute same-origin paths.
44
+ */
45
+ resolveAvatarUrl?: (path: string) => string;
40
46
  }
41
47
  /**
42
48
  * 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;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"}
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;AAe9B,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;IAC5C;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAC9C;AAoCD;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAqLtD,CAAA"}
@@ -15,7 +15,7 @@ import { formatDistanceToNow } from 'date-fns';
15
15
  import { es, enUS } from 'date-fns/locale';
16
16
  import { ChevronDown, ChevronRight, Clock, ExternalLink } from 'lucide-react';
17
17
  import { cn } from '@asteby/metacore-ui/lib';
18
- import { Avatar, AvatarFallback, Badge, Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@asteby/metacore-ui/primitives';
18
+ import { Avatar, AvatarFallback, AvatarImage, Badge, Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@asteby/metacore-ui/primitives';
19
19
  import { getInitials } from '@asteby/metacore-ui/lib';
20
20
  import { ActivityDiff } from './activity-diff';
21
21
  // ---------------------------------------------------------------------------
@@ -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, onOpenEvent, }) => {
54
+ export const RecordHistory = ({ events, columns, timeZone, currency, locale = 'es', className, onOpenEvent, resolveAvatarUrl, }) => {
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,7 +94,7 @@ 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 }))] })] }), 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) => {
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: [_jsxs(Avatar, { className: "h-7 w-7 rounded-full shrink-0", children: [event.actor_avatar ? (_jsx(AvatarImage, { src: resolveAvatarUrl ? resolveAvatarUrl(event.actor_avatar) : event.actor_avatar, alt: actor })) : null, _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
98
  e.stopPropagation();
99
99
  onOpenEvent(event);
100
100
  }, onKeyDown: (e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.13.0",
3
+ "version": "18.13.2",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,6 +35,8 @@ export interface ActivityEvent {
35
35
  correlation_id?: string | null
36
36
  actor_id?: string | null
37
37
  actor_label?: string | null
38
+ /** Storage path of the actor's avatar image, when the backend resolves one. */
39
+ actor_avatar?: string | null
38
40
  addon_key: string
39
41
  model: string
40
42
  record_id: string
@@ -74,19 +76,41 @@ export interface ActivityDiffProps {
74
76
  // Helpers
75
77
  // ---------------------------------------------------------------------------
76
78
 
79
+ // Meta-level keys that are always present and never meaningful in a
80
+ // human-readable diff.
81
+ const META_KEYS = new Set(['id', 'created_at', 'updated_at', 'organization_id', 'org_id', 'deleted_at'])
82
+
83
+ /** True when an object is a backend-resolved sibling ({value,label} relation, {name,…} user). */
84
+ function isResolvedObject(v: unknown): boolean {
85
+ if (!v || typeof v !== 'object' || Array.isArray(v)) return false
86
+ const o = v as Record<string, unknown>
87
+ return typeof o.label === 'string' || typeof o.name === 'string'
88
+ }
89
+
77
90
  /** Returns all field keys that appear in the diff. */
78
91
  function diffKeys(event: ActivityEvent): string[] {
79
- if (event.changes && Object.keys(event.changes).length > 0) {
80
- return Object.keys(event.changes)
81
- }
82
92
  const before = event.before ?? {}
83
93
  const after = event.after ?? {}
84
- const keys = new Set([...Object.keys(before), ...Object.keys(after)])
85
- // Filter out meta-level keys that are always present and rarely meaningful
86
- // in a human-readable diff (id, created_at, updated_at, organization_id).
87
- const META = new Set(['id', 'created_at', 'updated_at', 'organization_id', 'org_id'])
88
- keys.forEach((k) => { if (META.has(k)) keys.delete(k) })
89
- return Array.from(keys)
94
+ const source =
95
+ event.changes && Object.keys(event.changes).length > 0
96
+ ? new Set(Object.keys(event.changes))
97
+ : new Set([...Object.keys(before), ...Object.keys(after)])
98
+ META_KEYS.forEach((k) => source.delete(k))
99
+ // A resolved FK appears twice: the raw UUID key (created_by_id) and the
100
+ // resolved sibling (created_by: {name/label,…}). Drop the raw key — the
101
+ // sibling row already shows the human value. The sibling's value may live
102
+ // in before/after or inside changes[sibling] ({from,to}/{before,after}).
103
+ const siblingResolved = (sibling: string): boolean => {
104
+ if (isResolvedObject(before[sibling]) || isResolvedObject(after[sibling])) return true
105
+ const ch = (event.changes as Record<string, Record<string, unknown>> | undefined)?.[sibling]
106
+ if (!ch || typeof ch !== 'object') return false
107
+ return isResolvedObject(ch.from) || isResolvedObject(ch.to) || isResolvedObject(ch.before) || isResolvedObject(ch.after)
108
+ }
109
+ return Array.from(source).filter((k) => {
110
+ if (!k.endsWith('_id')) return true
111
+ const sibling = k.slice(0, -3)
112
+ return !(source.has(sibling) && siblingResolved(sibling))
113
+ })
90
114
  }
91
115
 
92
116
  /** Returns the set of keys where the value actually changed. */
@@ -18,6 +18,7 @@ import { cn } from '@asteby/metacore-ui/lib'
18
18
  import {
19
19
  Avatar,
20
20
  AvatarFallback,
21
+ AvatarImage,
21
22
  Badge,
22
23
  Collapsible,
23
24
  CollapsibleContent,
@@ -57,6 +58,12 @@ export interface RecordHistoryProps {
57
58
  * detail page (e.g. `/activity/:id`). Omitted → no button.
58
59
  */
59
60
  onOpenEvent?: (event: ActivityEvent) => void
61
+ /**
62
+ * Resolves an event's `actor_avatar` storage path to a fetchable URL
63
+ * (e.g. ops' `getStorageUrl(path, 'avatars')`). Identity when omitted —
64
+ * fine for absolute same-origin paths.
65
+ */
66
+ resolveAvatarUrl?: (path: string) => string
60
67
  }
61
68
 
62
69
  // ---------------------------------------------------------------------------
@@ -106,6 +113,7 @@ export const RecordHistory: React.FC<RecordHistoryProps> = ({
106
113
  locale = 'es',
107
114
  className,
108
115
  onOpenEvent,
116
+ resolveAvatarUrl,
109
117
  }) => {
110
118
  const dateLocale = locale === 'en' ? enUS : es
111
119
 
@@ -187,8 +195,15 @@ export const RecordHistory: React.FC<RecordHistoryProps> = ({
187
195
  type="button"
188
196
  className="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-muted/30 transition-colors"
189
197
  >
190
- {/* Actor avatar */}
198
+ {/* Actor avatar — the photo when the event carries one,
199
+ initials otherwise */}
191
200
  <Avatar className="h-7 w-7 rounded-full shrink-0">
201
+ {event.actor_avatar ? (
202
+ <AvatarImage
203
+ src={resolveAvatarUrl ? resolveAvatarUrl(event.actor_avatar) : event.actor_avatar}
204
+ alt={actor}
205
+ />
206
+ ) : null}
192
207
  <AvatarFallback className="text-[9px] font-bold bg-primary/10 text-primary">
193
208
  {getInitials(actor)}
194
209
  </AvatarFallback>