@object-ui/app-shell 4.4.0 → 4.5.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 +36 -0
- package/dist/utils/index.js +17 -7
- package/dist/views/RecordDetailView.js +69 -3
- package/package.json +24 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 4.5.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d714e85: Lookup display-name resolution now falls back through a Salesforce-style chain
|
|
8
|
+
when an `$expand`'d reference object lacks a top-level `name`/`label`/
|
|
9
|
+
`display_name`/`title` field:
|
|
10
|
+
1. Standard display fields (existing behaviour)
|
|
11
|
+
2. `salutation first_name last_name` composite — handles person records that
|
|
12
|
+
only carry first/last name parts
|
|
13
|
+
3. `email` — last-resort identifier, beats the opaque id
|
|
14
|
+
|
|
15
|
+
Applies to `LookupCellRenderer`, `PageHeader.subtitle` interpolation,
|
|
16
|
+
`DetailView` page-mode `titleFormat`, and the shared `formatRecordTitle`
|
|
17
|
+
utility. Concretely: a Contact reference with `first_name: Bob`, `last_name:
|
|
18
|
+
Lin` and no `name` field now renders as `Bob Lin` everywhere — instead of
|
|
19
|
+
the email or [object Object] fallback.
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [ab5e281]
|
|
22
|
+
- Updated dependencies [d714e85]
|
|
23
|
+
- Updated dependencies [6b6afd1]
|
|
24
|
+
- Updated dependencies [22fa558]
|
|
25
|
+
- Updated dependencies [aa7855f]
|
|
26
|
+
- Updated dependencies [170d89f]
|
|
27
|
+
- @object-ui/types@4.5.0
|
|
28
|
+
- @object-ui/fields@4.5.0
|
|
29
|
+
- @object-ui/layout@4.5.0
|
|
30
|
+
- @object-ui/components@4.5.0
|
|
31
|
+
- @object-ui/i18n@4.5.0
|
|
32
|
+
- @object-ui/auth@4.5.0
|
|
33
|
+
- @object-ui/collaboration@4.5.0
|
|
34
|
+
- @object-ui/core@4.5.0
|
|
35
|
+
- @object-ui/data-objectstack@4.5.0
|
|
36
|
+
- @object-ui/permissions@4.5.0
|
|
37
|
+
- @object-ui/react@4.5.0
|
|
38
|
+
|
|
3
39
|
## 4.4.0
|
|
4
40
|
|
|
5
41
|
### Patch Changes
|
package/dist/utils/index.js
CHANGED
|
@@ -69,14 +69,24 @@ export function formatRecordTitle(titleFormat, record) {
|
|
|
69
69
|
break;
|
|
70
70
|
value = value[p];
|
|
71
71
|
}
|
|
72
|
-
// Auto-extract display name from expanded reference objects
|
|
72
|
+
// Auto-extract display name from expanded reference objects, with a
|
|
73
|
+
// Salesforce-style fallback chain.
|
|
73
74
|
if (value && typeof value === 'object') {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
const o = value;
|
|
76
|
+
let display = o.name ?? o.full_name ?? o.display_name ?? o.label ?? o.title ?? o.subject ?? null;
|
|
77
|
+
if (display == null || (typeof display === 'string' && !display.trim())) {
|
|
78
|
+
const composite = [o.salutation, o.first_name, o.last_name]
|
|
79
|
+
.filter((p) => typeof p === 'string' && p.trim())
|
|
80
|
+
.map((p) => p.trim())
|
|
81
|
+
.join(' ');
|
|
82
|
+
if (composite)
|
|
83
|
+
display = composite;
|
|
84
|
+
else if (typeof o.email === 'string' && o.email.trim())
|
|
85
|
+
display = o.email.trim();
|
|
86
|
+
else
|
|
87
|
+
display = null;
|
|
88
|
+
}
|
|
89
|
+
value = display;
|
|
80
90
|
}
|
|
81
91
|
if (value === null || value === undefined || value === '') {
|
|
82
92
|
return EMPTY_TOKEN;
|
|
@@ -358,18 +358,84 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
358
358
|
}
|
|
359
359
|
let cancelled = false;
|
|
360
360
|
setHistoryLoading(true);
|
|
361
|
+
// Pull `old_value` and `new_value` so we can render a per-field diff
|
|
362
|
+
// ("Industry: finance → healthcare") instead of just the action verb.
|
|
363
|
+
// The backend already authorises sys_audit_log row access; column-level
|
|
364
|
+
// redaction would need to happen server-side, not by omitting columns here.
|
|
361
365
|
dataSource
|
|
362
366
|
.find('sys_audit_log', {
|
|
363
367
|
$filter: { record_id: pureRecordId, object_name: objectDef.name },
|
|
364
368
|
$orderby: { created_at: 'desc' },
|
|
365
369
|
$top: 50,
|
|
366
|
-
$select: ['id', 'created_at', 'action', 'user_id'],
|
|
370
|
+
$select: ['id', 'created_at', 'action', 'user_id', 'old_value', 'new_value'],
|
|
367
371
|
})
|
|
368
|
-
.then((res) => {
|
|
372
|
+
.then(async (res) => {
|
|
369
373
|
if (cancelled)
|
|
370
374
|
return;
|
|
371
375
|
const items = Array.isArray(res) ? res : res?.data || [];
|
|
372
|
-
|
|
376
|
+
// 1) Resolve actor display names + avatars in a single batched call
|
|
377
|
+
// so the timeline never falls back to a raw UUID.
|
|
378
|
+
const userIds = Array.from(new Set(items
|
|
379
|
+
.map((it) => it?.user_id)
|
|
380
|
+
.filter((v) => typeof v === 'string' && v.length > 0)));
|
|
381
|
+
let userMap = new Map();
|
|
382
|
+
if (userIds.length > 0) {
|
|
383
|
+
try {
|
|
384
|
+
const usersRes = await dataSource.find('sys_user', {
|
|
385
|
+
$filter: { id: { $in: userIds } },
|
|
386
|
+
$top: userIds.length,
|
|
387
|
+
$select: ['id', 'name', 'email', 'image'],
|
|
388
|
+
});
|
|
389
|
+
const users = Array.isArray(usersRes) ? usersRes : usersRes?.data || [];
|
|
390
|
+
userMap = new Map(users.map((u) => [u.id, { name: u.name || u.email || null, image: u.image || null }]));
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
console.warn('[RecordDetailView] Failed to resolve audit user names:', err);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// 2) Build a label map for the current object so diff lines show
|
|
397
|
+
// "Industry: …" rather than the snake_case "industry: …".
|
|
398
|
+
const fieldLabels = {};
|
|
399
|
+
for (const [name, def] of Object.entries(objectDef.fields || {})) {
|
|
400
|
+
if (def?.label)
|
|
401
|
+
fieldLabels[name] = def.label;
|
|
402
|
+
}
|
|
403
|
+
const parseJson = (v) => {
|
|
404
|
+
if (!v)
|
|
405
|
+
return null;
|
|
406
|
+
if (typeof v === 'object')
|
|
407
|
+
return v;
|
|
408
|
+
if (typeof v === 'string') {
|
|
409
|
+
try {
|
|
410
|
+
return JSON.parse(v);
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
};
|
|
418
|
+
const enriched = items.map((it) => {
|
|
419
|
+
const u = it?.user_id ? userMap.get(it.user_id) : undefined;
|
|
420
|
+
const oldObj = parseJson(it?.old_value) || {};
|
|
421
|
+
const newObj = parseJson(it?.new_value) || {};
|
|
422
|
+
const fields = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
|
|
423
|
+
const changes = Array.from(fields)
|
|
424
|
+
.filter((f) => JSON.stringify(oldObj[f]) !== JSON.stringify(newObj[f]))
|
|
425
|
+
.map((f) => ({
|
|
426
|
+
field: f,
|
|
427
|
+
label: fieldLabels[f] || f,
|
|
428
|
+
from: oldObj[f],
|
|
429
|
+
to: newObj[f],
|
|
430
|
+
}));
|
|
431
|
+
return {
|
|
432
|
+
...it,
|
|
433
|
+
user_name: u?.name ?? null,
|
|
434
|
+
user_avatar: u?.image ?? null,
|
|
435
|
+
changes: changes.length > 0 ? changes : undefined,
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
setHistoryEntries(enriched);
|
|
373
439
|
})
|
|
374
440
|
.catch((err) => {
|
|
375
441
|
if (cancelled)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -27,34 +27,34 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"lucide-react": "^1.16.0",
|
|
29
29
|
"sonner": "^2.0.7",
|
|
30
|
-
"@object-ui/auth": "4.
|
|
31
|
-
"@object-ui/collaboration": "4.
|
|
32
|
-
"@object-ui/components": "4.
|
|
33
|
-
"@object-ui/core": "4.
|
|
34
|
-
"@object-ui/data-objectstack": "4.
|
|
35
|
-
"@object-ui/fields": "4.
|
|
36
|
-
"@object-ui/i18n": "4.
|
|
37
|
-
"@object-ui/layout": "4.
|
|
38
|
-
"@object-ui/permissions": "4.
|
|
39
|
-
"@object-ui/react": "4.
|
|
40
|
-
"@object-ui/types": "4.
|
|
30
|
+
"@object-ui/auth": "4.5.0",
|
|
31
|
+
"@object-ui/collaboration": "4.5.0",
|
|
32
|
+
"@object-ui/components": "4.5.0",
|
|
33
|
+
"@object-ui/core": "4.5.0",
|
|
34
|
+
"@object-ui/data-objectstack": "4.5.0",
|
|
35
|
+
"@object-ui/fields": "4.5.0",
|
|
36
|
+
"@object-ui/i18n": "4.5.0",
|
|
37
|
+
"@object-ui/layout": "4.5.0",
|
|
38
|
+
"@object-ui/permissions": "4.5.0",
|
|
39
|
+
"@object-ui/react": "4.5.0",
|
|
40
|
+
"@object-ui/types": "4.5.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
44
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
45
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
46
|
-
"@object-ui/plugin-calendar": "^4.
|
|
47
|
-
"@object-ui/plugin-charts": "^4.
|
|
48
|
-
"@object-ui/plugin-chatbot": "^4.
|
|
49
|
-
"@object-ui/plugin-dashboard": "^4.
|
|
50
|
-
"@object-ui/plugin-designer": "^4.
|
|
51
|
-
"@object-ui/plugin-detail": "^4.
|
|
52
|
-
"@object-ui/plugin-form": "^4.
|
|
53
|
-
"@object-ui/plugin-grid": "^4.
|
|
54
|
-
"@object-ui/plugin-kanban": "^4.
|
|
55
|
-
"@object-ui/plugin-list": "^4.
|
|
56
|
-
"@object-ui/plugin-report": "^4.
|
|
57
|
-
"@object-ui/plugin-view": "^4.
|
|
46
|
+
"@object-ui/plugin-calendar": "^4.5.0",
|
|
47
|
+
"@object-ui/plugin-charts": "^4.5.0",
|
|
48
|
+
"@object-ui/plugin-chatbot": "^4.5.0",
|
|
49
|
+
"@object-ui/plugin-dashboard": "^4.5.0",
|
|
50
|
+
"@object-ui/plugin-designer": "^4.5.0",
|
|
51
|
+
"@object-ui/plugin-detail": "^4.5.0",
|
|
52
|
+
"@object-ui/plugin-form": "^4.5.0",
|
|
53
|
+
"@object-ui/plugin-grid": "^4.5.0",
|
|
54
|
+
"@object-ui/plugin-kanban": "^4.5.0",
|
|
55
|
+
"@object-ui/plugin-list": "^4.5.0",
|
|
56
|
+
"@object-ui/plugin-report": "^4.5.0",
|
|
57
|
+
"@object-ui/plugin-view": "^4.5.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^25.9.0",
|