@asteby/metacore-runtime-react 18.10.2 → 18.11.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,27 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2cdb047: Add Activity / Time Machine components: `ActivityDiff`, `RecordHistory`, and
8
+ `ActivityTimeline`.
9
+ - `ActivityDiff` — renders the field-level diff of a single `ActivityEvent`
10
+ (created/updated/deleted states, before→after per field, toggle all/changed).
11
+ - `RecordHistory` — chronological timeline of all events for a single record,
12
+ collapsible cards, embeddable in a record dialog "Historial" tab.
13
+ - `ActivityTimeline` — global feed grouped by `correlation_id`, with client-side
14
+ filters (model, actor, action, date range) and injectable `resolveColumns(model)`
15
+ resolver so hosts supply metadata without any internal fetch.
16
+
17
+ All three components are transport-agnostic (no fetch, no API calls) and reuse
18
+ the existing display-type renderers (currency, status, date, boolean, relation
19
+ chips, tags, color, url) via the new `ActivityValueRenderer` helper, keeping
20
+ table cells and diff cells visually consistent.
21
+
22
+ Also exports `ActivityValueRenderer` as a standalone pure renderer for use
23
+ outside the activity components.
24
+
3
25
  ## 18.10.2
4
26
 
5
27
  ### Patch Changes
@@ -0,0 +1,70 @@
1
+ /**
2
+ * activity-diff.tsx
3
+ *
4
+ * <ActivityDiff> — renders the field-level diff of a single ActivityEvent.
5
+ *
6
+ * Three visual states driven by `event.action`:
7
+ * - created → green "after" column only (no before)
8
+ * - deleted → red "before" column only (no after)
9
+ * - updated → yellow "before → after" side-by-side per field
10
+ *
11
+ * Consumers pass the declarative `columns` metadata array (same shape as
12
+ * `TableMetadata.columns`) so labels and display types are resolved without
13
+ * any internal fetch. Degrades gracefully when `columns` is empty/absent.
14
+ *
15
+ * Toggle: "Todos los campos / Solo cambios" (with changed-field counter).
16
+ */
17
+ import * as React from 'react';
18
+ import type { ColumnDefinition } from './types';
19
+ /**
20
+ * The canonical activity event shape as produced by the kernel / host backend.
21
+ * Transport-agnostic — the component only reads the fields it needs.
22
+ */
23
+ export interface ActivityEvent {
24
+ id: string;
25
+ correlation_id?: string | null;
26
+ actor_id?: string | null;
27
+ actor_label?: string | null;
28
+ addon_key: string;
29
+ model: string;
30
+ record_id: string;
31
+ action: string;
32
+ kind?: string | null;
33
+ before?: Record<string, unknown> | null;
34
+ after?: Record<string, unknown> | null;
35
+ /**
36
+ * Explicit diff map produced by the backend. When present, only these keys
37
+ * are "changed" fields. When absent, the diff is derived from before/after.
38
+ */
39
+ changes?: Record<string, {
40
+ from: unknown;
41
+ to: unknown;
42
+ }> | null;
43
+ summary?: string | null;
44
+ occurred_at: string;
45
+ }
46
+ export interface ActivityDiffProps {
47
+ /** The activity event to render. */
48
+ event: ActivityEvent;
49
+ /**
50
+ * Column metadata for the model. Used to resolve `col.label` and display
51
+ * type. Pass `TableMetadata.columns` from the host's metadata cache.
52
+ * Optional — field keys are shown raw when absent.
53
+ */
54
+ columns?: ColumnDefinition[];
55
+ /** IANA timezone for datetime cells (org config). */
56
+ timeZone?: string;
57
+ /** ISO 4217 currency for money cells (org config). */
58
+ currency?: string;
59
+ /** BCP-47 locale. Defaults to 'es'. */
60
+ locale?: string;
61
+ /** Class applied to the root element. */
62
+ className?: string;
63
+ }
64
+ /**
65
+ * Renders the field-level diff of a single ActivityEvent. Shows each field
66
+ * with its label (from `columns`) and value formatted using the column's
67
+ * display type. Supports a toggle to show only changed fields vs. all fields.
68
+ */
69
+ export declare const ActivityDiff: React.FC<ActivityDiffProps>;
70
+ //# sourceMappingURL=activity-diff.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * activity-diff.tsx
4
+ *
5
+ * <ActivityDiff> — renders the field-level diff of a single ActivityEvent.
6
+ *
7
+ * Three visual states driven by `event.action`:
8
+ * - created → green "after" column only (no before)
9
+ * - deleted → red "before" column only (no after)
10
+ * - updated → yellow "before → after" side-by-side per field
11
+ *
12
+ * Consumers pass the declarative `columns` metadata array (same shape as
13
+ * `TableMetadata.columns`) so labels and display types are resolved without
14
+ * any internal fetch. Degrades gracefully when `columns` is empty/absent.
15
+ *
16
+ * Toggle: "Todos los campos / Solo cambios" (with changed-field counter).
17
+ */
18
+ import * as React from 'react';
19
+ import { ArrowRight } from 'lucide-react';
20
+ import { cn } from '@asteby/metacore-ui/lib';
21
+ import { Badge } from '@asteby/metacore-ui/primitives';
22
+ import { ActivityValueRenderer } from './activity-value-renderer';
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+ /** Returns all field keys that appear in the diff. */
27
+ function diffKeys(event) {
28
+ if (event.changes && Object.keys(event.changes).length > 0) {
29
+ return Object.keys(event.changes);
30
+ }
31
+ const before = event.before ?? {};
32
+ 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
+ }
41
+ /** Returns the set of keys where the value actually changed. */
42
+ function changedKeys(event) {
43
+ if (event.changes && Object.keys(event.changes).length > 0) {
44
+ return new Set(Object.keys(event.changes));
45
+ }
46
+ const before = event.before ?? {};
47
+ const after = event.after ?? {};
48
+ const changed = new Set();
49
+ const all = new Set([...Object.keys(before), ...Object.keys(after)]);
50
+ all.forEach((k) => {
51
+ if (JSON.stringify(before[k]) !== JSON.stringify(after[k]))
52
+ changed.add(k);
53
+ });
54
+ return changed;
55
+ }
56
+ function resolveColumn(key, columns) {
57
+ return columns?.find((c) => c.key === key);
58
+ }
59
+ function resolveLabel(key, columns) {
60
+ const col = resolveColumn(key, columns);
61
+ if (col?.label)
62
+ return col.label;
63
+ // Humanize snake_case as last resort
64
+ return key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
65
+ }
66
+ function actionVariant(action) {
67
+ const a = action.toLowerCase();
68
+ if (a === 'created' || a === 'create')
69
+ return 'created';
70
+ if (a === 'deleted' || a === 'delete')
71
+ return 'deleted';
72
+ if (a === 'updated' || a === 'update')
73
+ return 'updated';
74
+ return 'other';
75
+ }
76
+ const VARIANT_BADGE = {
77
+ created: { label: 'Creado', className: 'bg-green-50 text-green-700 border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-900' },
78
+ updated: { label: 'Actualizado', className: 'bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-400 dark:border-yellow-900' },
79
+ deleted: { label: 'Eliminado', className: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950/30 dark:text-red-400 dark:border-red-900' },
80
+ other: { label: '', className: 'bg-muted text-muted-foreground border-border' },
81
+ };
82
+ // Subtle row highlight colors — using inline style so arbitrary values are
83
+ // never dropped by the host's Tailwind class scan.
84
+ const ROW_STYLE = {
85
+ created: { background: 'color-mix(in srgb, #22c55e 6%, transparent)' },
86
+ deleted: { background: 'color-mix(in srgb, #ef4444 6%, transparent)' },
87
+ updated: {},
88
+ other: {},
89
+ };
90
+ // ---------------------------------------------------------------------------
91
+ // Component
92
+ // ---------------------------------------------------------------------------
93
+ /**
94
+ * Renders the field-level diff of a single ActivityEvent. Shows each field
95
+ * with its label (from `columns`) and value formatted using the column's
96
+ * display type. Supports a toggle to show only changed fields vs. all fields.
97
+ */
98
+ export const ActivityDiff = ({ event, columns, timeZone, currency, locale = 'es', className, }) => {
99
+ const variant = actionVariant(event.action);
100
+ const allKeys = diffKeys(event);
101
+ const changed = changedKeys(event);
102
+ const [showOnlyChanged, setShowOnlyChanged] = React.useState(true);
103
+ const displayedKeys = showOnlyChanged ? allKeys.filter((k) => changed.has(k)) : allKeys;
104
+ const isCreated = variant === 'created';
105
+ const isDeleted = variant === 'deleted';
106
+ const variantBadge = VARIANT_BADGE[variant] ?? VARIANT_BADGE.other;
107
+ if (allKeys.length === 0 && !event.summary) {
108
+ return (_jsx("div", { className: cn('text-sm text-muted-foreground italic py-1', className), children: "Sin campos registrados." }));
109
+ }
110
+ return (_jsxs("div", { className: cn('space-y-2', className), children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx(Badge, { variant: "outline", className: cn('text-xs font-medium px-2 py-0.5', variantBadge.className), children: variantBadge.label || event.action }), changed.size > 0 && variant === 'updated' && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [changed.size, " campo", changed.size !== 1 ? 's' : '', " modificado", changed.size !== 1 ? 's' : ''] })), allKeys.length > 0 && variant === 'updated' && (_jsx("button", { type: "button", onClick: () => setShowOnlyChanged((v) => !v), className: "ml-auto text-xs text-primary hover:underline", children: showOnlyChanged ? `Ver todos (${allKeys.length})` : 'Solo cambios' }))] }), event.summary && (_jsx("p", { className: "text-sm text-muted-foreground italic", children: event.summary })), displayedKeys.length > 0 && (_jsxs("div", { className: "rounded-lg border border-border/60 overflow-hidden text-sm", children: [_jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr] border-b border-border/40 bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground", children: [_jsx("span", { children: "Campo" }), !isCreated && _jsx("span", { children: isDeleted ? 'Valor' : 'Antes' }), isCreated && _jsx("span", {}), !isDeleted && _jsx("span", { children: isCreated ? 'Valor' : 'Después' }), isDeleted && _jsx("span", {})] }), displayedKeys.map((key, idx) => {
111
+ const col = resolveColumn(key, columns);
112
+ const label = resolveLabel(key, columns);
113
+ const isChanged = changed.has(key);
114
+ let fromVal;
115
+ let toVal;
116
+ if (event.changes?.[key]) {
117
+ fromVal = event.changes[key].from;
118
+ toVal = event.changes[key].to;
119
+ }
120
+ else {
121
+ fromVal = event.before?.[key];
122
+ toVal = event.after?.[key];
123
+ }
124
+ const rowStyle = isCreated
125
+ ? ROW_STYLE.created
126
+ : isDeleted
127
+ ? ROW_STYLE.deleted
128
+ : isChanged
129
+ ? {}
130
+ : {};
131
+ return (_jsxs("div", { style: rowStyle, className: cn('grid grid-cols-[1fr_1fr_1fr] items-start px-3 py-2 gap-x-2', idx !== displayedKeys.length - 1 && 'border-b border-border/30', isChanged && variant === 'updated' && 'bg-yellow-50/40 dark:bg-yellow-950/10'), children: [_jsx("span", { className: "text-xs font-medium text-foreground/70 pt-0.5 truncate", title: label, children: label }), !isCreated ? (_jsx("span", { className: cn(isDeleted ? 'col-span-2' : ''), children: _jsx(ActivityValueRenderer, { value: isDeleted ? fromVal : fromVal, col: col, timeZone: timeZone, currency: currency, locale: locale }) })) : (_jsx("span", {})), !isDeleted ? (_jsxs("span", { className: cn(isCreated ? 'col-span-2' : ''), children: [isChanged && variant === 'updated' && (_jsx("span", { className: "inline-flex items-center gap-1 align-middle mr-1", children: _jsx(ArrowRight, { className: "h-3 w-3 text-muted-foreground/50 shrink-0" }) })), _jsx(ActivityValueRenderer, { value: isCreated ? toVal : toVal, col: col, timeZone: timeZone, currency: currency, locale: locale })] })) : (_jsx("span", {}))] }, key));
132
+ })] }))] }));
133
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * activity-timeline.tsx
3
+ *
4
+ * <ActivityTimeline> — global activity feed grouped by correlation_id.
5
+ *
6
+ * Events that share a correlation_id are folded into a single "operation"
7
+ * group (e.g. "Juan · Pedido creado · 4 cambios"). Events without a
8
+ * correlation_id appear as standalone entries.
9
+ *
10
+ * Filters: by model, actor, action, and date range. All client-side.
11
+ *
12
+ * Transport-agnostic: events arrive via props; column metadata is resolved
13
+ * per-model via the `resolveColumns(model)` injected function. No fetch.
14
+ */
15
+ import * as React from 'react';
16
+ import type { ColumnDefinition } from './types';
17
+ import type { ActivityEvent } from './activity-diff';
18
+ export interface ActivityTimelineProps {
19
+ /**
20
+ * All activity events to display. The component groups, sorts, and filters
21
+ * them client-side — order does not matter.
22
+ */
23
+ events: ActivityEvent[];
24
+ /**
25
+ * Injectable column metadata resolver. Called once per unique model name
26
+ * encountered in the event list. Returns the column definitions for that
27
+ * model, or undefined/empty when the host has no metadata for it.
28
+ *
29
+ * The host typically implements this as a cache lookup against its
30
+ * MetadataService / metadata-cache store.
31
+ */
32
+ resolveColumns?: (model: string) => ColumnDefinition[] | undefined;
33
+ /** IANA timezone for datetime cells. */
34
+ timeZone?: string;
35
+ /** ISO 4217 currency for money cells. */
36
+ currency?: string;
37
+ /** BCP-47 locale. Defaults to 'es'. */
38
+ locale?: string;
39
+ /** Class applied to the root element. */
40
+ className?: string;
41
+ /**
42
+ * When true, the filter bar is hidden. Useful when the host already
43
+ * provides external filter controls and wants to feed pre-filtered events.
44
+ */
45
+ hideFilters?: boolean;
46
+ }
47
+ /**
48
+ * Global activity feed. Groups correlated events, renders a vertical timeline
49
+ * with filter controls (model, actor, action, date range).
50
+ *
51
+ * The `resolveColumns` function is injected by the host. It maps a model name
52
+ * to its `ColumnDefinition[]` (e.g. from the host's metadata-cache store).
53
+ * When omitted or when it returns nothing, field keys are shown raw.
54
+ */
55
+ export declare const ActivityTimeline: React.FC<ActivityTimelineProps>;
56
+ //# sourceMappingURL=activity-timeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-timeline.d.ts","sourceRoot":"","sources":["../src/activity-timeline.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA+B9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAOpD,MAAM,WAAW,qBAAqB;IAClC;;;OAGG;IACH,MAAM,EAAE,aAAa,EAAE,CAAA;IACvB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,gBAAgB,EAAE,GAAG,SAAS,CAAA;IAClE,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;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACxB;AAqQD;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CA8N5D,CAAA"}
@@ -0,0 +1,215 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * activity-timeline.tsx
4
+ *
5
+ * <ActivityTimeline> — global activity feed grouped by correlation_id.
6
+ *
7
+ * Events that share a correlation_id are folded into a single "operation"
8
+ * group (e.g. "Juan · Pedido creado · 4 cambios"). Events without a
9
+ * correlation_id appear as standalone entries.
10
+ *
11
+ * Filters: by model, actor, action, and date range. All client-side.
12
+ *
13
+ * Transport-agnostic: events arrive via props; column metadata is resolved
14
+ * per-model via the `resolveColumns(model)` injected function. No fetch.
15
+ */
16
+ import * as React from 'react';
17
+ import { formatDistanceToNow } from 'date-fns';
18
+ import { es, enUS } from 'date-fns/locale';
19
+ import { ChevronDown, ChevronRight, Clock, Filter, X, Layers, Activity, } from 'lucide-react';
20
+ import { cn } from '@asteby/metacore-ui/lib';
21
+ import { Avatar, AvatarFallback, Badge, Collapsible, CollapsibleContent, CollapsibleTrigger, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button, } from '@asteby/metacore-ui/primitives';
22
+ import { getInitials } from '@asteby/metacore-ui/lib';
23
+ import { ActivityDiff } from './activity-diff';
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+ const ACTION_DOT_COLOR = {
28
+ created: '#22c55e',
29
+ create: '#22c55e',
30
+ updated: '#eab308',
31
+ update: '#eab308',
32
+ deleted: '#ef4444',
33
+ delete: '#ef4444',
34
+ };
35
+ function actionDotColor(action) {
36
+ return ACTION_DOT_COLOR[action.toLowerCase()] ?? '#6b7280';
37
+ }
38
+ const ACTION_LABELS_ES = {
39
+ created: 'creó',
40
+ create: 'creó',
41
+ updated: 'actualizó',
42
+ update: 'actualizó',
43
+ deleted: 'eliminó',
44
+ delete: 'eliminó',
45
+ };
46
+ function groupEvents(events) {
47
+ const grouped = new Map();
48
+ for (const ev of events) {
49
+ const key = ev.correlation_id || ev.id;
50
+ const arr = grouped.get(key) ?? [];
51
+ arr.push(ev);
52
+ grouped.set(key, arr);
53
+ }
54
+ const result = [];
55
+ grouped.forEach((evs, key) => {
56
+ // Sort ascending (oldest first within a group)
57
+ const sorted = [...evs].sort((a, b) => new Date(a.occurred_at).getTime() - new Date(b.occurred_at).getTime());
58
+ const root = sorted[0];
59
+ // Count distinct changed fields across all events in this group
60
+ let changedFieldCount = 0;
61
+ for (const ev of sorted) {
62
+ if (ev.changes)
63
+ changedFieldCount += Object.keys(ev.changes).length;
64
+ else if (ev.before || ev.after) {
65
+ const keys = new Set([...Object.keys(ev.before ?? {}), ...Object.keys(ev.after ?? {})]);
66
+ changedFieldCount += keys.size;
67
+ }
68
+ }
69
+ result.push({ key, root, events: sorted, changedFieldCount });
70
+ });
71
+ // Sort groups: most recent root event first
72
+ result.sort((a, b) => new Date(b.root.occurred_at).getTime() - new Date(a.root.occurred_at).getTime());
73
+ return result;
74
+ }
75
+ function uniqueValues(events, key) {
76
+ const set = new Set();
77
+ for (const ev of events) {
78
+ const v = ev[key];
79
+ if (v !== undefined && v !== null && v !== '')
80
+ set.add(String(v));
81
+ }
82
+ return Array.from(set).sort();
83
+ }
84
+ const GroupCard = ({ group, resolveColumns, timeZone, currency, locale, dateLocale, isOpen, onToggle, }) => {
85
+ const { root, events, changedFieldCount } = group;
86
+ const isMulti = events.length > 1;
87
+ const actor = root.actor_label || root.actor_id || 'Sistema';
88
+ const dotColor = actionDotColor(root.action);
89
+ const timeAgo = (() => {
90
+ try {
91
+ return formatDistanceToNow(new Date(root.occurred_at), { addSuffix: true, locale: dateLocale });
92
+ }
93
+ catch {
94
+ return root.occurred_at;
95
+ }
96
+ })();
97
+ const fullDate = (() => {
98
+ try {
99
+ return new Date(root.occurred_at).toLocaleString(locale === 'en' ? 'en-US' : 'es-MX', {
100
+ ...(timeZone ? { timeZone } : {}),
101
+ dateStyle: 'medium',
102
+ timeStyle: 'short',
103
+ });
104
+ }
105
+ catch {
106
+ return root.occurred_at;
107
+ }
108
+ })();
109
+ const summaryLine = root.summary
110
+ ? root.summary
111
+ : `${actor} ${ACTION_LABELS_ES[root.action.toLowerCase()] ?? root.action} ${root.model}`;
112
+ return (_jsx(Collapsible, { open: isOpen, onOpenChange: onToggle, children: _jsxs("div", { className: "relative", children: [_jsx("span", { className: "absolute -left-5 top-4 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-start 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 mt-0.5", 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: [_jsx("p", { className: "text-sm font-medium text-foreground truncate", title: summaryLine, children: summaryLine }), _jsxs("div", { className: "flex items-center gap-2 mt-1 flex-wrap", children: [_jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Clock, { className: "h-3 w-3 opacity-60 shrink-0" }), _jsx("span", { title: fullDate, children: timeAgo })] }), _jsx(Badge, { variant: "outline", className: "text-[10px] px-1.5 py-0 h-4", children: root.model }), isMulti && (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Layers, { className: "h-3 w-3 opacity-60 shrink-0" }), events.length, " eventos"] })), changedFieldCount > 0 && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [changedFieldCount, " campo", changedFieldCount !== 1 ? 's' : ''] }))] })] }), _jsx("span", { className: "shrink-0 text-muted-foreground mt-1", 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 divide-y divide-border/30", children: events.map((ev, idx) => {
113
+ const cols = resolveColumns(ev.model);
114
+ const evActor = ev.actor_label || ev.actor_id || 'Sistema';
115
+ return (_jsxs("div", { className: "px-4 py-3 space-y-2", children: [isMulti && (_jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground/70", children: ev.model }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: evActor }), idx > 0 && (_jsxs(_Fragment, { children: [_jsx("span", { children: "\u00B7" }), _jsx("span", { title: ev.occurred_at, children: (() => {
116
+ try {
117
+ return formatDistanceToNow(new Date(ev.occurred_at), { addSuffix: true, locale: dateLocale });
118
+ }
119
+ catch {
120
+ return ev.occurred_at;
121
+ }
122
+ })() })] }))] })), _jsx(ActivityDiff, { event: ev, columns: cols, timeZone: timeZone, currency: currency, locale: locale })] }, ev.id));
123
+ }) }) })] })] }) }));
124
+ };
125
+ // ---------------------------------------------------------------------------
126
+ // Main component
127
+ // ---------------------------------------------------------------------------
128
+ /**
129
+ * Global activity feed. Groups correlated events, renders a vertical timeline
130
+ * with filter controls (model, actor, action, date range).
131
+ *
132
+ * The `resolveColumns` function is injected by the host. It maps a model name
133
+ * to its `ColumnDefinition[]` (e.g. from the host's metadata-cache store).
134
+ * When omitted or when it returns nothing, field keys are shown raw.
135
+ */
136
+ export const ActivityTimeline = ({ events, resolveColumns = () => undefined, timeZone, currency, locale = 'es', className, hideFilters = false, }) => {
137
+ const dateLocale = locale === 'en' ? enUS : es;
138
+ // -----------------------------------------------------------------------
139
+ // Filter state
140
+ // -----------------------------------------------------------------------
141
+ const [filterModel, setFilterModel] = React.useState('__all__');
142
+ const [filterActor, setFilterActor] = React.useState('__all__');
143
+ const [filterAction, setFilterAction] = React.useState('__all__');
144
+ const [filterFrom, setFilterFrom] = React.useState('');
145
+ const [filterTo, setFilterTo] = React.useState('');
146
+ const models = React.useMemo(() => uniqueValues(events, 'model'), [events]);
147
+ const actors = React.useMemo(() => uniqueValues(events, 'actor_label').filter(Boolean), [events]);
148
+ const actions = React.useMemo(() => uniqueValues(events, 'action'), [events]);
149
+ const hasFilters = filterModel !== '__all__' ||
150
+ filterActor !== '__all__' ||
151
+ filterAction !== '__all__' ||
152
+ filterFrom !== '' ||
153
+ filterTo !== '';
154
+ const clearFilters = () => {
155
+ setFilterModel('__all__');
156
+ setFilterActor('__all__');
157
+ setFilterAction('__all__');
158
+ setFilterFrom('');
159
+ setFilterTo('');
160
+ };
161
+ // -----------------------------------------------------------------------
162
+ // Filtered + grouped events
163
+ // -----------------------------------------------------------------------
164
+ const filtered = React.useMemo(() => {
165
+ return events.filter((ev) => {
166
+ if (filterModel !== '__all__' && ev.model !== filterModel)
167
+ return false;
168
+ if (filterActor !== '__all__' && ev.actor_label !== filterActor)
169
+ return false;
170
+ if (filterAction !== '__all__' && ev.action !== filterAction)
171
+ return false;
172
+ if (filterFrom) {
173
+ const from = new Date(filterFrom);
174
+ if (new Date(ev.occurred_at) < from)
175
+ return false;
176
+ }
177
+ if (filterTo) {
178
+ const to = new Date(filterTo);
179
+ // inclusive end-of-day
180
+ to.setHours(23, 59, 59, 999);
181
+ if (new Date(ev.occurred_at) > to)
182
+ return false;
183
+ }
184
+ return true;
185
+ });
186
+ }, [events, filterModel, filterActor, filterAction, filterFrom, filterTo]);
187
+ const groups = React.useMemo(() => groupEvents(filtered), [filtered]);
188
+ // -----------------------------------------------------------------------
189
+ // Open/close state (first group open by default)
190
+ // -----------------------------------------------------------------------
191
+ const [openKeys, setOpenKeys] = React.useState(() => groups.length > 0 ? new Set([groups[0].key]) : new Set());
192
+ // Reset open state when filtered groups change substantially
193
+ const prevGroupKeysRef = React.useRef('');
194
+ React.useEffect(() => {
195
+ const current = groups.map((g) => g.key).join(',');
196
+ if (current !== prevGroupKeysRef.current) {
197
+ prevGroupKeysRef.current = current;
198
+ setOpenKeys(groups.length > 0 ? new Set([groups[0].key]) : new Set());
199
+ }
200
+ }, [groups]);
201
+ const toggleGroup = (key) => {
202
+ setOpenKeys((prev) => {
203
+ const next = new Set(prev);
204
+ if (next.has(key))
205
+ next.delete(key);
206
+ else
207
+ next.add(key);
208
+ return next;
209
+ });
210
+ };
211
+ // -----------------------------------------------------------------------
212
+ // Render
213
+ // -----------------------------------------------------------------------
214
+ return (_jsxs("div", { className: cn('space-y-4', className), children: [!hideFilters && (_jsxs("div", { className: "rounded-lg border border-border/60 bg-muted/20 p-3 space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Filter, { className: "h-4 w-4 text-muted-foreground shrink-0" }), _jsx("span", { className: "text-sm font-medium text-muted-foreground", children: "Filtros" }), hasFilters && (_jsxs(Button, { variant: "ghost", size: "sm", className: "ml-auto h-7 px-2 text-xs", onClick: clearFilters, children: [_jsx(X, { className: "h-3 w-3 mr-1" }), "Limpiar"] }))] }), _jsxs("div", { className: "grid grid-cols-2 gap-2 sm:grid-cols-4", children: [models.length > 1 && (_jsxs(Select, { value: filterModel, onValueChange: setFilterModel, children: [_jsx(SelectTrigger, { className: "h-8 text-xs", children: _jsx(SelectValue, { placeholder: "M\u00F3dulo" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__all__", children: "Todos los m\u00F3dulos" }), models.map((m) => (_jsx(SelectItem, { value: m, children: m }, m)))] })] })), actors.length > 1 && (_jsxs(Select, { value: filterActor, onValueChange: setFilterActor, children: [_jsx(SelectTrigger, { className: "h-8 text-xs", children: _jsx(SelectValue, { placeholder: "Usuario" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__all__", children: "Todos los usuarios" }), actors.map((a) => (_jsx(SelectItem, { value: a, children: a }, a)))] })] })), actions.length > 1 && (_jsxs(Select, { value: filterAction, onValueChange: setFilterAction, children: [_jsx(SelectTrigger, { className: "h-8 text-xs", children: _jsx(SelectValue, { placeholder: "Acci\u00F3n" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__all__", children: "Todas las acciones" }), actions.map((a) => (_jsx(SelectItem, { value: a, children: a }, a)))] })] })), _jsxs("div", { className: "col-span-2 sm:col-span-1 flex gap-1", children: [_jsx(Input, { type: "date", value: filterFrom, onChange: (e) => setFilterFrom(e.target.value), className: "h-8 text-xs", placeholder: "Desde", title: "Desde" }), _jsx(Input, { type: "date", value: filterTo, onChange: (e) => setFilterTo(e.target.value), className: "h-8 text-xs", placeholder: "Hasta", title: "Hasta" })] })] })] })), groups.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center py-16 gap-3 text-muted-foreground", children: [_jsx(Activity, { className: "h-10 w-10 opacity-30" }), _jsx("p", { className: "text-sm", children: hasFilters ? 'Sin resultados con los filtros actuales.' : 'Sin actividad registrada.' })] })) : (_jsxs("div", { className: "relative pl-5 space-y-3", children: [_jsx("span", { className: "absolute left-2 top-2 bottom-2 w-px bg-border", "aria-hidden": "true" }), groups.map((group) => (_jsx(GroupCard, { group: group, resolveColumns: resolveColumns, timeZone: timeZone, currency: currency, locale: locale, dateLocale: dateLocale, isOpen: openKeys.has(group.key), onToggle: () => toggleGroup(group.key) }, group.key)))] }))] }));
215
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * activity-value-renderer.tsx
3
+ *
4
+ * Pure, transport-agnostic value renderer for Activity / Time Machine diffs.
5
+ * Reuses the same display-type logic as dynamic-columns.tsx (currency, status,
6
+ * date, boolean, badge, relation, url, tags, color, number, percent) so the
7
+ * diff cells and the table cells are always consistent.
8
+ *
9
+ * Kept in its own module so it has no dependency on tanstack-table or the
10
+ * column-factory machinery — only React + metacore-ui primitives.
11
+ */
12
+ import * as React from 'react';
13
+ import type { ColumnDefinition } from './types';
14
+ export interface ActivityValueRendererProps {
15
+ /** The raw value to display (from before/after/changes). */
16
+ value: unknown;
17
+ /** Column metadata for display formatting. Optional — falls back to string. */
18
+ col?: ColumnDefinition;
19
+ /** IANA timezone (org config) for datetime cells. */
20
+ timeZone?: string;
21
+ /** ISO 4217 currency for money cells. */
22
+ currency?: string;
23
+ /** BCP-47 locale tag (e.g. 'es', 'en'). Defaults to 'es'. */
24
+ locale?: string;
25
+ }
26
+ /**
27
+ * Renders a single field value from an activity event using the same display
28
+ * type logic as `defaultGetDynamicColumns`. Pass a `ColumnDefinition` to get
29
+ * rich formatting (currency, status badge, date, boolean, etc.); without it
30
+ * the component falls back to a plain string representation.
31
+ */
32
+ export declare const ActivityValueRenderer: React.FC<ActivityValueRendererProps>;
33
+ //# sourceMappingURL=activity-value-renderer.d.ts.map
@@ -0,0 +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"}