@asteby/metacore-runtime-react 18.12.0 → 18.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/activity-diff.d.ts.map +1 -1
- package/dist/activity-diff.js +9 -1
- 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 +46 -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 +7 -1
- package/src/activity-value-renderer.tsx +36 -18
- package/src/dialogs/dynamic-record.tsx +76 -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.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bd619da: Activity/history polish + lucide icon cells:
|
|
8
|
+
- `ActivityValueRenderer`: backend-resolved entity objects ({name,avatar,email} users, {value,label} relations) render as an avatar/name chip instead of raw JSON — covers the "Created By" row in a record's history diff. Relation chips also unwrap resolved objects.
|
|
9
|
+
- `ActivityDiff`: a diff key now matches dotted display columns by base segment (`created_by` → `created_by.avatar`), inheriting the served label and rich renderer.
|
|
10
|
+
- `RecordHistory`: new optional `onOpenEvent(event)` prop — shows an "open in activity log" button per event so hosts can deep-link to `/activity/:id`.
|
|
11
|
+
- Image cells (`dynamic-columns` + record detail `ViewValue`): a value that is a lucide icon name (an addon's `icon` column, e.g. "Banknote") renders the glyph via `DynamicIcon` instead of a broken `<img>` (empty grey box). New `isLucideIconName` export.
|
|
12
|
+
|
|
13
|
+
## 18.12.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- e568344: ViewValue: render structured jsonb values (objects/arrays without a label/name/title) as readable key→value pairs instead of "[object Object]" — e.g. a `fiscal_data` jsonb column on the record detail page. Plain objects become a humanized key/value list, primitive arrays a comma-joined line, nested structures a pretty-printed JSON block; empty structures render the "—" empty marker.
|
|
18
|
+
|
|
3
19
|
## 18.12.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-diff.d.ts","sourceRoot":"","sources":["../src/activity-diff.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oCAAoC;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;
|
|
1
|
+
{"version":3,"file":"activity-diff.d.ts","sourceRoot":"","sources":["../src/activity-diff.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oCAAoC;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAiFD;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAmJpD,CAAA"}
|
package/dist/activity-diff.js
CHANGED
|
@@ -54,7 +54,15 @@ function changedKeys(event) {
|
|
|
54
54
|
return changed;
|
|
55
55
|
}
|
|
56
56
|
function resolveColumn(key, columns) {
|
|
57
|
-
|
|
57
|
+
if (!columns?.length)
|
|
58
|
+
return undefined;
|
|
59
|
+
const exact = columns.find((c) => c.key === key);
|
|
60
|
+
if (exact)
|
|
61
|
+
return exact;
|
|
62
|
+
// A diff key is the physical column (created_by); the served metadata may
|
|
63
|
+
// only carry the dotted display column for it (created_by.avatar). Match on
|
|
64
|
+
// the base segment so the diff cell inherits its label and rich renderer.
|
|
65
|
+
return columns.find((c) => typeof c.key === 'string' && c.key.includes('.') && c.key.split('.')[0] === key);
|
|
58
66
|
}
|
|
59
67
|
function resolveLabel(key, columns) {
|
|
60
68
|
const col = resolveColumn(key, columns);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-value-renderer.d.ts","sourceRoot":"","sources":["../src/activity-value-renderer.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAU9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;
|
|
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';
|
|
@@ -123,6 +124,9 @@ function formatDisplayValue(rawValue, field) {
|
|
|
123
124
|
// when no declared option matches the value.
|
|
124
125
|
return match?.label ?? humanizeToken(value);
|
|
125
126
|
}
|
|
127
|
+
// Structured value with no label — JSON beats "[object Object]".
|
|
128
|
+
if (typeof value === 'object')
|
|
129
|
+
return JSON.stringify(value);
|
|
126
130
|
return String(value);
|
|
127
131
|
}
|
|
128
132
|
const MODE_CONFIG = {
|
|
@@ -531,8 +535,18 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
531
535
|
return value ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-5 w-5 rounded-full border shadow-sm", style: { backgroundColor: value } }), _jsx("span", { className: "text-sm", children: value })] })) : (_jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "-" }));
|
|
532
536
|
}
|
|
533
537
|
if (field.type === 'image') {
|
|
538
|
+
if (isLucideIconName(value)) {
|
|
539
|
+
return _jsx(IconNameViewValue, { name: value });
|
|
540
|
+
}
|
|
534
541
|
return value ? (_jsx("img", { src: getImageUrl(String(value)), alt: field.label, className: "h-16 w-16 rounded-lg object-cover border" })) : (_jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "Sin imagen" }));
|
|
535
542
|
}
|
|
543
|
+
// Icon-name column served as plain text (the table infers cellStyle image,
|
|
544
|
+
// but the detail/modal field keeps the storage type): render the glyph.
|
|
545
|
+
if (isLucideIconName(value) &&
|
|
546
|
+
typeof field.key === 'string' &&
|
|
547
|
+
(field.key === 'icon' || field.key.endsWith('_icon'))) {
|
|
548
|
+
return _jsx(IconNameViewValue, { name: value });
|
|
549
|
+
}
|
|
536
550
|
if (field.type === 'url' && value) {
|
|
537
551
|
return (_jsx("a", { href: value, target: "_blank", rel: "noreferrer", className: "text-sm text-primary hover:underline truncate", children: value }));
|
|
538
552
|
}
|
|
@@ -574,12 +588,44 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
574
588
|
};
|
|
575
589
|
return (_jsxs(Badge, { variant: "secondary", className: "w-fit flex items-center gap-1", style: opt.color && !opt.icon ? { backgroundColor: opt.color, color: '#fff', borderColor: 'transparent' } : undefined, children: [_jsx(OptionLead, { option: lead, size: 16 }), opt.label] }));
|
|
576
590
|
}
|
|
591
|
+
// Structured value (jsonb column, e.g. fiscal_data) with no label/name/title
|
|
592
|
+
// to surface — render readable key/value pairs instead of falling through to
|
|
593
|
+
// String(value) ("[object Object]").
|
|
594
|
+
if (value !== null && typeof value === 'object') {
|
|
595
|
+
return _jsx(StructuredViewValue, { value: value });
|
|
596
|
+
}
|
|
577
597
|
const display = formatDisplayValue(value, field);
|
|
578
598
|
if (field.type === 'textarea') {
|
|
579
599
|
return (_jsx("p", { className: "text-sm whitespace-pre-wrap rounded-md bg-muted/40 p-3 min-h-[60px]", children: display }));
|
|
580
600
|
}
|
|
581
601
|
return _jsx("p", { className: "text-sm py-1", children: display });
|
|
582
602
|
}
|
|
603
|
+
// IconNameViewValue — read view for a column whose value is a lucide icon name
|
|
604
|
+
// (an addon's `icon` column): the glyph plus the name, so the value stays
|
|
605
|
+
// copyable/recognizable next to its rendering.
|
|
606
|
+
function IconNameViewValue({ name }) {
|
|
607
|
+
return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx("div", { className: "h-8 w-8 flex items-center justify-center rounded bg-muted", children: _jsx(DynamicIcon, { name: name, className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm text-muted-foreground", children: name })] }));
|
|
608
|
+
}
|
|
609
|
+
// StructuredViewValue renders a jsonb object/array that has no resolvable label:
|
|
610
|
+
// plain objects become a key→value list (keys humanized), primitive arrays a
|
|
611
|
+
// comma-joined line, and anything deeper a pretty-printed JSON block. Empty
|
|
612
|
+
// structures render the same "—" marker as null scalars.
|
|
613
|
+
function StructuredViewValue({ value }) {
|
|
614
|
+
if (Array.isArray(value)) {
|
|
615
|
+
if (value.length === 0) {
|
|
616
|
+
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
617
|
+
}
|
|
618
|
+
if (value.every(v => v === null || typeof v !== 'object')) {
|
|
619
|
+
return _jsx("p", { className: "text-sm py-1", children: value.map(v => String(v ?? '—')).join(', ') });
|
|
620
|
+
}
|
|
621
|
+
return (_jsx("pre", { className: "text-xs whitespace-pre-wrap rounded-md bg-muted/40 p-3 overflow-x-auto", children: JSON.stringify(value, null, 2) }));
|
|
622
|
+
}
|
|
623
|
+
const entries = Object.entries(value).filter(([, v]) => v !== null && v !== undefined && v !== '');
|
|
624
|
+
if (entries.length === 0) {
|
|
625
|
+
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
626
|
+
}
|
|
627
|
+
return (_jsx("dl", { className: "text-sm py-1 space-y-0.5", children: entries.map(([k, v]) => (_jsxs("div", { className: "flex gap-2", children: [_jsxs("dt", { className: "text-muted-foreground shrink-0", children: [humanizeToken(k), ":"] }), _jsx("dd", { className: "break-words", children: typeof v === 'object' ? JSON.stringify(v) : String(v) })] }, k))) }));
|
|
628
|
+
}
|
|
583
629
|
function EditField({ field, value, onChange }) {
|
|
584
630
|
if (field.type === 'boolean') {
|
|
585
631
|
return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, onCheckedChange: onChange }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAgC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,
|
|
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
|
@@ -105,7 +105,13 @@ function changedKeys(event: ActivityEvent): Set<string> {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function resolveColumn(key: string, columns?: ColumnDefinition[]): ColumnDefinition | undefined {
|
|
108
|
-
|
|
108
|
+
if (!columns?.length) return undefined
|
|
109
|
+
const exact = columns.find((c) => c.key === key)
|
|
110
|
+
if (exact) return exact
|
|
111
|
+
// A diff key is the physical column (created_by); the served metadata may
|
|
112
|
+
// only carry the dotted display column for it (created_by.avatar). Match on
|
|
113
|
+
// the base segment so the diff cell inherits its label and rich renderer.
|
|
114
|
+
return columns.find((c) => typeof c.key === 'string' && c.key.includes('.') && c.key.split('.')[0] === key)
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
function resolveLabel(key: string, columns?: ColumnDefinition[]): string {
|
|
@@ -51,6 +51,31 @@ const statusColorFor = (value: string): string => {
|
|
|
51
51
|
return '#6b7280'
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// resolvedEntity — a diff snapshot value may be the backend-resolved sibling
|
|
55
|
+
// object ({value,label} relation, {name,avatar,email} user). Surface its human
|
|
56
|
+
// identity instead of raw JSON. Returns undefined for anything else.
|
|
57
|
+
function resolvedEntity(value: unknown): { name: string; avatar?: string; email?: string } | undefined {
|
|
58
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
59
|
+
const v = value as Record<string, unknown>
|
|
60
|
+
const name = v.name ?? v.label ?? v.title
|
|
61
|
+
if (typeof name !== 'string' || name === '') return undefined
|
|
62
|
+
const avatar = typeof v.avatar === 'string' && v.avatar !== '' ? v.avatar : undefined
|
|
63
|
+
const email = typeof v.email === 'string' && v.email !== '' ? v.email : undefined
|
|
64
|
+
return { name, avatar, email }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const EntityChip: React.FC<{ entity: { name: string; avatar?: string; email?: string } }> = ({ entity }) => (
|
|
68
|
+
<span className="inline-flex items-center gap-1.5" title={entity.email}>
|
|
69
|
+
<Avatar className="h-5 w-5 rounded-full">
|
|
70
|
+
<AvatarImage src={entity.avatar ?? ''} alt={entity.name} />
|
|
71
|
+
<AvatarFallback className="text-[8px] font-bold bg-primary/10 text-primary">
|
|
72
|
+
{getInitials(entity.name)}
|
|
73
|
+
</AvatarFallback>
|
|
74
|
+
</Avatar>
|
|
75
|
+
<span className="text-sm font-medium truncate" style={{ maxWidth: 180 }}>{entity.name}</span>
|
|
76
|
+
</span>
|
|
77
|
+
)
|
|
78
|
+
|
|
54
79
|
const useIsDarkTheme = () => {
|
|
55
80
|
const [isDark, setIsDark] = React.useState(() =>
|
|
56
81
|
typeof document !== 'undefined' && document.documentElement.classList.contains('dark'),
|
|
@@ -103,9 +128,12 @@ export const ActivityValueRenderer: React.FC<ActivityValueRendererProps> = ({
|
|
|
103
128
|
return <span className="text-muted-foreground">—</span>
|
|
104
129
|
}
|
|
105
130
|
|
|
106
|
-
// No column metadata →
|
|
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'
|
|
@@ -323,6 +324,9 @@ function formatDisplayValue(rawValue: any, field: FieldDef): string {
|
|
|
323
324
|
return match?.label ?? humanizeToken(value)
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
// Structured value with no label — JSON beats "[object Object]".
|
|
328
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
329
|
+
|
|
326
330
|
return String(value)
|
|
327
331
|
}
|
|
328
332
|
|
|
@@ -974,6 +978,9 @@ export function ViewValue({
|
|
|
974
978
|
}
|
|
975
979
|
|
|
976
980
|
if (field.type === 'image') {
|
|
981
|
+
if (isLucideIconName(value)) {
|
|
982
|
+
return <IconNameViewValue name={value} />
|
|
983
|
+
}
|
|
977
984
|
return value ? (
|
|
978
985
|
<img src={getImageUrl(String(value))} alt={field.label} className="h-16 w-16 rounded-lg object-cover border" />
|
|
979
986
|
) : (
|
|
@@ -981,6 +988,16 @@ export function ViewValue({
|
|
|
981
988
|
)
|
|
982
989
|
}
|
|
983
990
|
|
|
991
|
+
// Icon-name column served as plain text (the table infers cellStyle image,
|
|
992
|
+
// but the detail/modal field keeps the storage type): render the glyph.
|
|
993
|
+
if (
|
|
994
|
+
isLucideIconName(value) &&
|
|
995
|
+
typeof field.key === 'string' &&
|
|
996
|
+
(field.key === 'icon' || field.key.endsWith('_icon'))
|
|
997
|
+
) {
|
|
998
|
+
return <IconNameViewValue name={value} />
|
|
999
|
+
}
|
|
1000
|
+
|
|
984
1001
|
if (field.type === 'url' && value) {
|
|
985
1002
|
return (
|
|
986
1003
|
<a
|
|
@@ -1048,6 +1065,13 @@ export function ViewValue({
|
|
|
1048
1065
|
)
|
|
1049
1066
|
}
|
|
1050
1067
|
|
|
1068
|
+
// Structured value (jsonb column, e.g. fiscal_data) with no label/name/title
|
|
1069
|
+
// to surface — render readable key/value pairs instead of falling through to
|
|
1070
|
+
// String(value) ("[object Object]").
|
|
1071
|
+
if (value !== null && typeof value === 'object') {
|
|
1072
|
+
return <StructuredViewValue value={value} />
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1051
1075
|
const display = formatDisplayValue(value, field)
|
|
1052
1076
|
|
|
1053
1077
|
if (field.type === 'textarea') {
|
|
@@ -1061,6 +1085,58 @@ export function ViewValue({
|
|
|
1061
1085
|
return <p className="text-sm py-1">{display}</p>
|
|
1062
1086
|
}
|
|
1063
1087
|
|
|
1088
|
+
// IconNameViewValue — read view for a column whose value is a lucide icon name
|
|
1089
|
+
// (an addon's `icon` column): the glyph plus the name, so the value stays
|
|
1090
|
+
// copyable/recognizable next to its rendering.
|
|
1091
|
+
function IconNameViewValue({ name }: { name: string }) {
|
|
1092
|
+
return (
|
|
1093
|
+
<div className="flex items-center gap-2 py-1">
|
|
1094
|
+
<div className="h-8 w-8 flex items-center justify-center rounded bg-muted">
|
|
1095
|
+
<DynamicIcon name={name} className="h-4 w-4" />
|
|
1096
|
+
</div>
|
|
1097
|
+
<span className="text-sm text-muted-foreground">{name}</span>
|
|
1098
|
+
</div>
|
|
1099
|
+
)
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// StructuredViewValue renders a jsonb object/array that has no resolvable label:
|
|
1103
|
+
// plain objects become a key→value list (keys humanized), primitive arrays a
|
|
1104
|
+
// comma-joined line, and anything deeper a pretty-printed JSON block. Empty
|
|
1105
|
+
// structures render the same "—" marker as null scalars.
|
|
1106
|
+
function StructuredViewValue({ value }: { value: any }) {
|
|
1107
|
+
if (Array.isArray(value)) {
|
|
1108
|
+
if (value.length === 0) {
|
|
1109
|
+
return <p className="text-sm py-1 text-muted-foreground">—</p>
|
|
1110
|
+
}
|
|
1111
|
+
if (value.every(v => v === null || typeof v !== 'object')) {
|
|
1112
|
+
return <p className="text-sm py-1">{value.map(v => String(v ?? '—')).join(', ')}</p>
|
|
1113
|
+
}
|
|
1114
|
+
return (
|
|
1115
|
+
<pre className="text-xs whitespace-pre-wrap rounded-md bg-muted/40 p-3 overflow-x-auto">
|
|
1116
|
+
{JSON.stringify(value, null, 2)}
|
|
1117
|
+
</pre>
|
|
1118
|
+
)
|
|
1119
|
+
}
|
|
1120
|
+
const entries = Object.entries(value).filter(
|
|
1121
|
+
([, v]) => v !== null && v !== undefined && v !== '',
|
|
1122
|
+
)
|
|
1123
|
+
if (entries.length === 0) {
|
|
1124
|
+
return <p className="text-sm py-1 text-muted-foreground">—</p>
|
|
1125
|
+
}
|
|
1126
|
+
return (
|
|
1127
|
+
<dl className="text-sm py-1 space-y-0.5">
|
|
1128
|
+
{entries.map(([k, v]) => (
|
|
1129
|
+
<div key={k} className="flex gap-2">
|
|
1130
|
+
<dt className="text-muted-foreground shrink-0">{humanizeToken(k)}:</dt>
|
|
1131
|
+
<dd className="break-words">
|
|
1132
|
+
{typeof v === 'object' ? JSON.stringify(v) : String(v)}
|
|
1133
|
+
</dd>
|
|
1134
|
+
</div>
|
|
1135
|
+
))}
|
|
1136
|
+
</dl>
|
|
1137
|
+
)
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1064
1140
|
function EditField({ field, value, onChange }: {
|
|
1065
1141
|
field: FieldDef
|
|
1066
1142
|
value: any
|
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 ? (
|