@asteby/metacore-runtime-react 18.10.2 → 18.12.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 +28 -0
- package/dist/activity-diff.d.ts +70 -0
- package/dist/activity-diff.d.ts.map +1 -0
- package/dist/activity-diff.js +133 -0
- package/dist/activity-timeline.d.ts +56 -0
- package/dist/activity-timeline.d.ts.map +1 -0
- package/dist/activity-timeline.js +215 -0
- package/dist/activity-value-renderer.d.ts +33 -0
- package/dist/activity-value-renderer.d.ts.map +1 -0
- package/dist/activity-value-renderer.js +213 -0
- package/dist/dynamic-table.d.ts +9 -3
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +6 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/record-history.d.ts +41 -0
- package/dist/record-history.d.ts.map +1 -0
- package/dist/record-history.js +99 -0
- package/package.json +1 -1
- package/src/activity-diff.tsx +298 -0
- package/src/activity-timeline.tsx +574 -0
- package/src/activity-value-renderer.tsx +371 -0
- package/src/dynamic-table.tsx +23 -4
- package/src/index.ts +17 -0
- package/src/record-history.tsx +243 -0
|
@@ -0,0 +1,298 @@
|
|
|
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
|
+
|
|
18
|
+
import * as React from 'react'
|
|
19
|
+
import { ChevronDown, ChevronRight, ArrowRight } from 'lucide-react'
|
|
20
|
+
import { cn } from '@asteby/metacore-ui/lib'
|
|
21
|
+
import { Badge } from '@asteby/metacore-ui/primitives'
|
|
22
|
+
import type { ColumnDefinition } from './types'
|
|
23
|
+
import { ActivityValueRenderer } from './activity-value-renderer'
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Types
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The canonical activity event shape as produced by the kernel / host backend.
|
|
31
|
+
* Transport-agnostic — the component only reads the fields it needs.
|
|
32
|
+
*/
|
|
33
|
+
export interface ActivityEvent {
|
|
34
|
+
id: string
|
|
35
|
+
correlation_id?: string | null
|
|
36
|
+
actor_id?: string | null
|
|
37
|
+
actor_label?: string | null
|
|
38
|
+
addon_key: string
|
|
39
|
+
model: string
|
|
40
|
+
record_id: string
|
|
41
|
+
action: string
|
|
42
|
+
kind?: string | null
|
|
43
|
+
before?: Record<string, unknown> | null
|
|
44
|
+
after?: Record<string, unknown> | null
|
|
45
|
+
/**
|
|
46
|
+
* Explicit diff map produced by the backend. When present, only these keys
|
|
47
|
+
* are "changed" fields. When absent, the diff is derived from before/after.
|
|
48
|
+
*/
|
|
49
|
+
changes?: Record<string, { from: unknown; to: unknown }> | null
|
|
50
|
+
summary?: string | null
|
|
51
|
+
occurred_at: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ActivityDiffProps {
|
|
55
|
+
/** The activity event to render. */
|
|
56
|
+
event: ActivityEvent
|
|
57
|
+
/**
|
|
58
|
+
* Column metadata for the model. Used to resolve `col.label` and display
|
|
59
|
+
* type. Pass `TableMetadata.columns` from the host's metadata cache.
|
|
60
|
+
* Optional — field keys are shown raw when absent.
|
|
61
|
+
*/
|
|
62
|
+
columns?: ColumnDefinition[]
|
|
63
|
+
/** IANA timezone for datetime cells (org config). */
|
|
64
|
+
timeZone?: string
|
|
65
|
+
/** ISO 4217 currency for money cells (org config). */
|
|
66
|
+
currency?: string
|
|
67
|
+
/** BCP-47 locale. Defaults to 'es'. */
|
|
68
|
+
locale?: string
|
|
69
|
+
/** Class applied to the root element. */
|
|
70
|
+
className?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Helpers
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/** Returns all field keys that appear in the diff. */
|
|
78
|
+
function diffKeys(event: ActivityEvent): string[] {
|
|
79
|
+
if (event.changes && Object.keys(event.changes).length > 0) {
|
|
80
|
+
return Object.keys(event.changes)
|
|
81
|
+
}
|
|
82
|
+
const before = event.before ?? {}
|
|
83
|
+
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)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Returns the set of keys where the value actually changed. */
|
|
93
|
+
function changedKeys(event: ActivityEvent): Set<string> {
|
|
94
|
+
if (event.changes && Object.keys(event.changes).length > 0) {
|
|
95
|
+
return new Set(Object.keys(event.changes))
|
|
96
|
+
}
|
|
97
|
+
const before = event.before ?? {}
|
|
98
|
+
const after = event.after ?? {}
|
|
99
|
+
const changed = new Set<string>()
|
|
100
|
+
const all = new Set([...Object.keys(before), ...Object.keys(after)])
|
|
101
|
+
all.forEach((k) => {
|
|
102
|
+
if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.add(k)
|
|
103
|
+
})
|
|
104
|
+
return changed
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveColumn(key: string, columns?: ColumnDefinition[]): ColumnDefinition | undefined {
|
|
108
|
+
return columns?.find((c) => c.key === key)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveLabel(key: string, columns?: ColumnDefinition[]): string {
|
|
112
|
+
const col = resolveColumn(key, columns)
|
|
113
|
+
if (col?.label) return col.label
|
|
114
|
+
// Humanize snake_case as last resort
|
|
115
|
+
return key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function actionVariant(action: string): 'created' | 'updated' | 'deleted' | 'other' {
|
|
119
|
+
const a = action.toLowerCase()
|
|
120
|
+
if (a === 'created' || a === 'create') return 'created'
|
|
121
|
+
if (a === 'deleted' || a === 'delete') return 'deleted'
|
|
122
|
+
if (a === 'updated' || a === 'update') return 'updated'
|
|
123
|
+
return 'other'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const VARIANT_BADGE: Record<string, { label: string; className: string }> = {
|
|
127
|
+
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' },
|
|
128
|
+
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' },
|
|
129
|
+
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' },
|
|
130
|
+
other: { label: '', className: 'bg-muted text-muted-foreground border-border' },
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Subtle row highlight colors — using inline style so arbitrary values are
|
|
134
|
+
// never dropped by the host's Tailwind class scan.
|
|
135
|
+
const ROW_STYLE = {
|
|
136
|
+
created: { background: 'color-mix(in srgb, #22c55e 6%, transparent)' },
|
|
137
|
+
deleted: { background: 'color-mix(in srgb, #ef4444 6%, transparent)' },
|
|
138
|
+
updated: {},
|
|
139
|
+
other: {},
|
|
140
|
+
} as Record<string, React.CSSProperties>
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Component
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Renders the field-level diff of a single ActivityEvent. Shows each field
|
|
148
|
+
* with its label (from `columns`) and value formatted using the column's
|
|
149
|
+
* display type. Supports a toggle to show only changed fields vs. all fields.
|
|
150
|
+
*/
|
|
151
|
+
export const ActivityDiff: React.FC<ActivityDiffProps> = ({
|
|
152
|
+
event,
|
|
153
|
+
columns,
|
|
154
|
+
timeZone,
|
|
155
|
+
currency,
|
|
156
|
+
locale = 'es',
|
|
157
|
+
className,
|
|
158
|
+
}) => {
|
|
159
|
+
const variant = actionVariant(event.action)
|
|
160
|
+
const allKeys = diffKeys(event)
|
|
161
|
+
const changed = changedKeys(event)
|
|
162
|
+
const [showOnlyChanged, setShowOnlyChanged] = React.useState(true)
|
|
163
|
+
|
|
164
|
+
const displayedKeys = showOnlyChanged ? allKeys.filter((k) => changed.has(k)) : allKeys
|
|
165
|
+
const isCreated = variant === 'created'
|
|
166
|
+
const isDeleted = variant === 'deleted'
|
|
167
|
+
|
|
168
|
+
const variantBadge = VARIANT_BADGE[variant] ?? VARIANT_BADGE.other
|
|
169
|
+
|
|
170
|
+
if (allKeys.length === 0 && !event.summary) {
|
|
171
|
+
return (
|
|
172
|
+
<div className={cn('text-sm text-muted-foreground italic py-1', className)}>
|
|
173
|
+
Sin campos registrados.
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div className={cn('space-y-2', className)}>
|
|
180
|
+
{/* Header: action badge + field count + toggle */}
|
|
181
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
182
|
+
<Badge variant="outline" className={cn('text-xs font-medium px-2 py-0.5', variantBadge.className)}>
|
|
183
|
+
{variantBadge.label || event.action}
|
|
184
|
+
</Badge>
|
|
185
|
+
{changed.size > 0 && variant === 'updated' && (
|
|
186
|
+
<span className="text-xs text-muted-foreground">
|
|
187
|
+
{changed.size} campo{changed.size !== 1 ? 's' : ''} modificado{changed.size !== 1 ? 's' : ''}
|
|
188
|
+
</span>
|
|
189
|
+
)}
|
|
190
|
+
{allKeys.length > 0 && variant === 'updated' && (
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={() => setShowOnlyChanged((v) => !v)}
|
|
194
|
+
className="ml-auto text-xs text-primary hover:underline"
|
|
195
|
+
>
|
|
196
|
+
{showOnlyChanged ? `Ver todos (${allKeys.length})` : 'Solo cambios'}
|
|
197
|
+
</button>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Summary line (if backend provided one) */}
|
|
202
|
+
{event.summary && (
|
|
203
|
+
<p className="text-sm text-muted-foreground italic">{event.summary}</p>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* Diff table */}
|
|
207
|
+
{displayedKeys.length > 0 && (
|
|
208
|
+
<div className="rounded-lg border border-border/60 overflow-hidden text-sm">
|
|
209
|
+
{/* Column headers */}
|
|
210
|
+
<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">
|
|
211
|
+
<span>Campo</span>
|
|
212
|
+
{!isCreated && <span>{isDeleted ? 'Valor' : 'Antes'}</span>}
|
|
213
|
+
{isCreated && <span></span>}
|
|
214
|
+
{!isDeleted && <span>{isCreated ? 'Valor' : 'Después'}</span>}
|
|
215
|
+
{isDeleted && <span></span>}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{displayedKeys.map((key, idx) => {
|
|
219
|
+
const col = resolveColumn(key, columns)
|
|
220
|
+
const label = resolveLabel(key, columns)
|
|
221
|
+
const isChanged = changed.has(key)
|
|
222
|
+
|
|
223
|
+
let fromVal: unknown
|
|
224
|
+
let toVal: unknown
|
|
225
|
+
|
|
226
|
+
if (event.changes?.[key]) {
|
|
227
|
+
fromVal = event.changes[key].from
|
|
228
|
+
toVal = event.changes[key].to
|
|
229
|
+
} else {
|
|
230
|
+
fromVal = event.before?.[key]
|
|
231
|
+
toVal = event.after?.[key]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const rowStyle = isCreated
|
|
235
|
+
? ROW_STYLE.created
|
|
236
|
+
: isDeleted
|
|
237
|
+
? ROW_STYLE.deleted
|
|
238
|
+
: isChanged
|
|
239
|
+
? {}
|
|
240
|
+
: {}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div
|
|
244
|
+
key={key}
|
|
245
|
+
style={rowStyle}
|
|
246
|
+
className={cn(
|
|
247
|
+
'grid grid-cols-[1fr_1fr_1fr] items-start px-3 py-2 gap-x-2',
|
|
248
|
+
idx !== displayedKeys.length - 1 && 'border-b border-border/30',
|
|
249
|
+
isChanged && variant === 'updated' && 'bg-yellow-50/40 dark:bg-yellow-950/10',
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
{/* Field label */}
|
|
253
|
+
<span className="text-xs font-medium text-foreground/70 pt-0.5 truncate" title={label}>
|
|
254
|
+
{label}
|
|
255
|
+
</span>
|
|
256
|
+
|
|
257
|
+
{/* Before value (or value for deleted/created) */}
|
|
258
|
+
{!isCreated ? (
|
|
259
|
+
<span className={cn(isDeleted ? 'col-span-2' : '')}>
|
|
260
|
+
<ActivityValueRenderer
|
|
261
|
+
value={isDeleted ? fromVal : fromVal}
|
|
262
|
+
col={col}
|
|
263
|
+
timeZone={timeZone}
|
|
264
|
+
currency={currency}
|
|
265
|
+
locale={locale}
|
|
266
|
+
/>
|
|
267
|
+
</span>
|
|
268
|
+
) : (
|
|
269
|
+
<span />
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{/* After value */}
|
|
273
|
+
{!isDeleted ? (
|
|
274
|
+
<span className={cn(isCreated ? 'col-span-2' : '')}>
|
|
275
|
+
{isChanged && variant === 'updated' && (
|
|
276
|
+
<span className="inline-flex items-center gap-1 align-middle mr-1">
|
|
277
|
+
<ArrowRight className="h-3 w-3 text-muted-foreground/50 shrink-0" />
|
|
278
|
+
</span>
|
|
279
|
+
)}
|
|
280
|
+
<ActivityValueRenderer
|
|
281
|
+
value={isCreated ? toVal : toVal}
|
|
282
|
+
col={col}
|
|
283
|
+
timeZone={timeZone}
|
|
284
|
+
currency={currency}
|
|
285
|
+
locale={locale}
|
|
286
|
+
/>
|
|
287
|
+
</span>
|
|
288
|
+
) : (
|
|
289
|
+
<span />
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
)
|
|
293
|
+
})}
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
)
|
|
298
|
+
}
|