@asteby/metacore-runtime-react 18.12.1 → 18.13.1
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 +16 -0
- package/dist/activity-diff.d.ts.map +1 -1
- package/dist/activity-diff.js +41 -11
- package/dist/activity-value-renderer.d.ts.map +1 -1
- package/dist/activity-value-renderer.js +28 -7
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +17 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +7 -1
- package/dist/dynamic-icon.d.ts +1 -0
- package/dist/dynamic-icon.d.ts.map +1 -1
- package/dist/dynamic-icon.js +13 -0
- package/dist/record-history.d.ts +6 -0
- package/dist/record-history.d.ts.map +1 -1
- package/dist/record-history.js +12 -3
- package/package.json +1 -1
- package/src/activity-diff.tsx +38 -10
- package/src/activity-value-renderer.tsx +36 -18
- package/src/dialogs/dynamic-record.tsx +28 -0
- package/src/dynamic-columns.tsx +11 -1
- package/src/dynamic-icon.tsx +12 -0
- package/src/record-history.tsx +32 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.13.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 18.13.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- bd619da: Activity/history polish + lucide icon cells:
|
|
14
|
+
- `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.
|
|
15
|
+
- `ActivityDiff`: a diff key now matches dotted display columns by base segment (`created_by` → `created_by.avatar`), inheriting the served label and rich renderer.
|
|
16
|
+
- `RecordHistory`: new optional `onOpenEvent(event)` prop — shows an "open in activity log" button per event so hosts can deep-link to `/activity/:id`.
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
3
19
|
## 18.12.1
|
|
4
20
|
|
|
5
21
|
### Patch 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;
|
|
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;AAuGD;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAmJpD,CAAA"}
|
package/dist/activity-diff.js
CHANGED
|
@@ -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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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) {
|
|
@@ -54,7 +76,15 @@ function changedKeys(event) {
|
|
|
54
76
|
return changed;
|
|
55
77
|
}
|
|
56
78
|
function resolveColumn(key, columns) {
|
|
57
|
-
|
|
79
|
+
if (!columns?.length)
|
|
80
|
+
return undefined;
|
|
81
|
+
const exact = columns.find((c) => c.key === key);
|
|
82
|
+
if (exact)
|
|
83
|
+
return exact;
|
|
84
|
+
// A diff key is the physical column (created_by); the served metadata may
|
|
85
|
+
// only carry the dotted display column for it (created_by.avatar). Match on
|
|
86
|
+
// the base segment so the diff cell inherits its label and rich renderer.
|
|
87
|
+
return columns.find((c) => typeof c.key === 'string' && c.key.includes('.') && c.key.split('.')[0] === key);
|
|
58
88
|
}
|
|
59
89
|
function resolveLabel(key, columns) {
|
|
60
90
|
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;
|
|
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 →
|
|
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
|
|
190
|
-
|
|
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":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;
|
|
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';
|
|
@@ -534,8 +535,18 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
534
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: "-" }));
|
|
535
536
|
}
|
|
536
537
|
if (field.type === 'image') {
|
|
538
|
+
if (isLucideIconName(value)) {
|
|
539
|
+
return _jsx(IconNameViewValue, { name: value });
|
|
540
|
+
}
|
|
537
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" }));
|
|
538
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
|
+
}
|
|
539
550
|
if (field.type === 'url' && value) {
|
|
540
551
|
return (_jsx("a", { href: value, target: "_blank", rel: "noreferrer", className: "text-sm text-primary hover:underline truncate", children: value }));
|
|
541
552
|
}
|
|
@@ -589,6 +600,12 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
589
600
|
}
|
|
590
601
|
return _jsx("p", { className: "text-sm py-1", children: display });
|
|
591
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
|
+
}
|
|
592
609
|
// StructuredViewValue renders a jsonb object/array that has no resolvable label:
|
|
593
610
|
// plain objects become a key→value list (keys humanized), primitive arrays a
|
|
594
611
|
// comma-joined line, and anything deeper a pretty-printed JSON block. Empty
|
|
@@ -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,
|
|
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"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -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';
|
package/dist/dynamic-icon.d.ts
CHANGED
|
@@ -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"}
|
package/dist/dynamic-icon.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/record-history.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/record-history.js
CHANGED
|
@@ -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", {
|
|
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
package/src/activity-diff.tsx
CHANGED
|
@@ -74,19 +74,41 @@ export interface ActivityDiffProps {
|
|
|
74
74
|
// Helpers
|
|
75
75
|
// ---------------------------------------------------------------------------
|
|
76
76
|
|
|
77
|
+
// Meta-level keys that are always present and never meaningful in a
|
|
78
|
+
// human-readable diff.
|
|
79
|
+
const META_KEYS = new Set(['id', 'created_at', 'updated_at', 'organization_id', 'org_id', 'deleted_at'])
|
|
80
|
+
|
|
81
|
+
/** True when an object is a backend-resolved sibling ({value,label} relation, {name,…} user). */
|
|
82
|
+
function isResolvedObject(v: unknown): boolean {
|
|
83
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return false
|
|
84
|
+
const o = v as Record<string, unknown>
|
|
85
|
+
return typeof o.label === 'string' || typeof o.name === 'string'
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
/** Returns all field keys that appear in the diff. */
|
|
78
89
|
function diffKeys(event: ActivityEvent): string[] {
|
|
79
|
-
if (event.changes && Object.keys(event.changes).length > 0) {
|
|
80
|
-
return Object.keys(event.changes)
|
|
81
|
-
}
|
|
82
90
|
const before = event.before ?? {}
|
|
83
91
|
const after = event.after ?? {}
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
const source =
|
|
93
|
+
event.changes && Object.keys(event.changes).length > 0
|
|
94
|
+
? new Set(Object.keys(event.changes))
|
|
95
|
+
: new Set([...Object.keys(before), ...Object.keys(after)])
|
|
96
|
+
META_KEYS.forEach((k) => source.delete(k))
|
|
97
|
+
// A resolved FK appears twice: the raw UUID key (created_by_id) and the
|
|
98
|
+
// resolved sibling (created_by: {name/label,…}). Drop the raw key — the
|
|
99
|
+
// sibling row already shows the human value. The sibling's value may live
|
|
100
|
+
// in before/after or inside changes[sibling] ({from,to}/{before,after}).
|
|
101
|
+
const siblingResolved = (sibling: string): boolean => {
|
|
102
|
+
if (isResolvedObject(before[sibling]) || isResolvedObject(after[sibling])) return true
|
|
103
|
+
const ch = (event.changes as Record<string, Record<string, unknown>> | undefined)?.[sibling]
|
|
104
|
+
if (!ch || typeof ch !== 'object') return false
|
|
105
|
+
return isResolvedObject(ch.from) || isResolvedObject(ch.to) || isResolvedObject(ch.before) || isResolvedObject(ch.after)
|
|
106
|
+
}
|
|
107
|
+
return Array.from(source).filter((k) => {
|
|
108
|
+
if (!k.endsWith('_id')) return true
|
|
109
|
+
const sibling = k.slice(0, -3)
|
|
110
|
+
return !(source.has(sibling) && siblingResolved(sibling))
|
|
111
|
+
})
|
|
90
112
|
}
|
|
91
113
|
|
|
92
114
|
/** Returns the set of keys where the value actually changed. */
|
|
@@ -105,7 +127,13 @@ function changedKeys(event: ActivityEvent): Set<string> {
|
|
|
105
127
|
}
|
|
106
128
|
|
|
107
129
|
function resolveColumn(key: string, columns?: ColumnDefinition[]): ColumnDefinition | undefined {
|
|
108
|
-
|
|
130
|
+
if (!columns?.length) return undefined
|
|
131
|
+
const exact = columns.find((c) => c.key === key)
|
|
132
|
+
if (exact) return exact
|
|
133
|
+
// A diff key is the physical column (created_by); the served metadata may
|
|
134
|
+
// only carry the dotted display column for it (created_by.avatar). Match on
|
|
135
|
+
// the base segment so the diff cell inherits its label and rich renderer.
|
|
136
|
+
return columns.find((c) => typeof c.key === 'string' && c.key.includes('.') && c.key.split('.')[0] === key)
|
|
109
137
|
}
|
|
110
138
|
|
|
111
139
|
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 →
|
|
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
|
-
|
|
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'
|
|
@@ -977,6 +978,9 @@ export function ViewValue({
|
|
|
977
978
|
}
|
|
978
979
|
|
|
979
980
|
if (field.type === 'image') {
|
|
981
|
+
if (isLucideIconName(value)) {
|
|
982
|
+
return <IconNameViewValue name={value} />
|
|
983
|
+
}
|
|
980
984
|
return value ? (
|
|
981
985
|
<img src={getImageUrl(String(value))} alt={field.label} className="h-16 w-16 rounded-lg object-cover border" />
|
|
982
986
|
) : (
|
|
@@ -984,6 +988,16 @@ export function ViewValue({
|
|
|
984
988
|
)
|
|
985
989
|
}
|
|
986
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
|
+
|
|
987
1001
|
if (field.type === 'url' && value) {
|
|
988
1002
|
return (
|
|
989
1003
|
<a
|
|
@@ -1071,6 +1085,20 @@ export function ViewValue({
|
|
|
1071
1085
|
return <p className="text-sm py-1">{display}</p>
|
|
1072
1086
|
}
|
|
1073
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
|
+
|
|
1074
1102
|
// StructuredViewValue renders a jsonb object/array that has no resolvable label:
|
|
1075
1103
|
// plain objects become a key→value list (keys humanized), primitive arrays a
|
|
1076
1104
|
// comma-joined line, and anything deeper a pretty-printed JSON block. Empty
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -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
|
package/src/dynamic-icon.tsx
CHANGED
|
@@ -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
|
+
}
|
package/src/record-history.tsx
CHANGED
|
@@ -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,
|
|
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 ? (
|