@object-ui/app-shell 5.0.2 → 5.1.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 +128 -0
- package/dist/layout/AppHeader.js +10 -20
- package/dist/views/ObjectView.js +20 -7
- package/dist/views/RecordDetailView.js +141 -37
- package/package.json +25 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,133 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [8955b9c]
|
|
8
|
+
- @object-ui/components@5.1.1
|
|
9
|
+
- @object-ui/fields@5.1.1
|
|
10
|
+
- @object-ui/layout@5.1.1
|
|
11
|
+
- @object-ui/types@5.1.1
|
|
12
|
+
- @object-ui/core@5.1.1
|
|
13
|
+
- @object-ui/i18n@5.1.1
|
|
14
|
+
- @object-ui/react@5.1.1
|
|
15
|
+
- @object-ui/data-objectstack@5.1.1
|
|
16
|
+
- @object-ui/auth@5.1.1
|
|
17
|
+
- @object-ui/permissions@5.1.1
|
|
18
|
+
- @object-ui/collaboration@5.1.1
|
|
19
|
+
- @object-ui/providers@5.1.1
|
|
20
|
+
|
|
21
|
+
## 5.1.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- d1ec6a2: Fold inline-edit into the page-header overflow menu (HubSpot/Lightning
|
|
26
|
+
pattern) and remove the orphan "Edit fields" toolbar row that previously
|
|
27
|
+
floated between the tab strip and the first detail section.
|
|
28
|
+
- `@object-ui/app-shell` `RecordDetailView`: injects a new `sys_inline_edit`
|
|
29
|
+
system action that appears in the ⋯ overflow menu and dispatches a
|
|
30
|
+
`objectui:record:inline-edit-toggle` window CustomEvent (filtered by
|
|
31
|
+
recordId + objectName).
|
|
32
|
+
- `@object-ui/plugin-detail` `DetailView`: listens for that event to
|
|
33
|
+
toggle inline-edit mode; the in-page toolbar now renders only during
|
|
34
|
+
active editing / save error / locked states, so the idle layout flows
|
|
35
|
+
tabs → first section card with no orphan row.
|
|
36
|
+
- `@object-ui/components` layout containers: extended `KNOWN_LABEL_DICT`
|
|
37
|
+
with zh-CN + zh-TW translations for common CRM related-list labels
|
|
38
|
+
(Quotes / Products / Contacts / Accounts / Leads / Opportunities /
|
|
39
|
+
Cases / Campaigns / Approvals / Documents / Emails / Calls / Meetings
|
|
40
|
+
/ Open Tasks / Closed Tasks), so authored English labels auto-translate
|
|
41
|
+
in `page:accordion` / `page:tabs` items.
|
|
42
|
+
|
|
43
|
+
- cf30cc2: Polish Lightning record detail page layout.
|
|
44
|
+
- `record:details` sections now render with Card chrome by default when a `title` is present, restoring visual grouping that was missing on pages like the opportunity detail page.
|
|
45
|
+
- Section labels can be translated via the `{ns}.objects.{objectName}._sections.{name}.label` convention. Author each section with a stable `name` (e.g. `info`, `forecast`) and the renderer picks up the locale-specific label automatically. Falls back to the literal `label` when no translation exists.
|
|
46
|
+
- The `page:header` action toolbar now collapses into a `⋯` overflow menu when more than two actions are present. The first business action stays inline; secondary system actions (Edit / Share / Delete) move into the menu, with destructive styling applied to Delete.
|
|
47
|
+
- Header action labels resolve via the `{ns}.objects.{objectName}._actions.{name}.label` convention.
|
|
48
|
+
- Removed the meaningless field-count Badge from collapsible section headers (the `2` chip next to "Description"). Field-count metadata wasn't useful in the header and added visual noise.
|
|
49
|
+
- Synth-path `sys_delete` now carries `variant: 'destructive'` so the overflow menu can color it appropriately.
|
|
50
|
+
|
|
51
|
+
- c0b236f: Platform detail/form polish:
|
|
52
|
+
- **Auto-section grouping**: When an object has no authored `views.form.sections`, the detail page now splits fields into a primary section and a collapsible "More details" section based on a field-type/name heuristic (textarea / markdown / description / notes / remarks). Eliminates the wall-of-fields layout on objects without explicit detail metadata.
|
|
53
|
+
- **FormSection card chrome**: `FormSection` now accepts `showBorder`. Defaults to `true` for titled sections (Card wrapper) and `false` for untitled sections (flat). Same auto-default already applied to `DetailSection`.
|
|
54
|
+
- **Origin breadcrumb**: Navigating from a list/kanban into a record now records the source view; the detail page shows a `← <view label>` back-link above the page header.
|
|
55
|
+
- New i18n key `detail.sectionMoreDetails` (en + zh-CN).
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- d51a577: feat(platform): Discussion attachments + @mention directory + Reference Rail aside
|
|
60
|
+
- **Discussion attachments** — `RichTextCommentInput` now accepts an `extraSlot`
|
|
61
|
+
and a `canSubmitEmpty` flag so hosts can mount the existing
|
|
62
|
+
`CommentAttachment` composer beneath the editor without forking the toolbar.
|
|
63
|
+
`RecordActivityTimeline` plumbs the attachments through
|
|
64
|
+
`DiscussionContext.onUploadAttachments` and submits attachment-only comments.
|
|
65
|
+
- **@mention directory** — `DiscussionContext` gains a `mentionSuggestions`
|
|
66
|
+
field; `RecordDetailView` populates it from the host `sys_user` collection so
|
|
67
|
+
`@` autocomplete in the composer now resolves against real users.
|
|
68
|
+
- **Reference Rail** — New `record:reference_rail` renderer + a dedicated
|
|
69
|
+
`aside` region emitted by `buildDefaultPageSchema` whenever a record has
|
|
70
|
+
≥ 2 related lists. The rail surfaces a Salesforce/HubSpot-style snapshot
|
|
71
|
+
of related collections (count badge + top 3 records) on `xl+` viewports.
|
|
72
|
+
- **Layout** — `PageRenderer`'s structured-layout `<aside>` wrappers now honor
|
|
73
|
+
`aside.className`, letting schemas attach responsive utilities like
|
|
74
|
+
`hidden xl:flex` to the rail region.
|
|
75
|
+
|
|
76
|
+
- 1976691: Fix the drawer "Open as full page" (maximize) button on the record drawer
|
|
77
|
+
which threw `TypeError: name.indexOf is not a function` and prevented
|
|
78
|
+
navigation to the dedicated detail page.
|
|
79
|
+
- `@object-ui/app-shell` `ObjectView`: pass `objectDef.name` (string) — not
|
|
80
|
+
the whole `objectDef` — into `viewLabel(...)` when computing the
|
|
81
|
+
`originState.from.label` for both drawer-navigate and list-navigate
|
|
82
|
+
flows. Two call sites fixed.
|
|
83
|
+
- `@object-ui/i18n` `useObjectLabel`: harden `stripNamespace` so it
|
|
84
|
+
tolerates non-string inputs and returns an empty string instead of
|
|
85
|
+
throwing, providing a safety net for similar future regressions.
|
|
86
|
+
|
|
87
|
+
- a49f300: feat(detail): per-object Reference Rail opt-out via `objectDef.detail.hideReferenceRail`
|
|
88
|
+
|
|
89
|
+
The Record-detail Reference Rail (right-hand related-list summary cards)
|
|
90
|
+
can now be suppressed on a per-object basis without authoring a full
|
|
91
|
+
custom `Page`. Catalog-style objects (Product, Task) ship with the rail
|
|
92
|
+
off by default; hub objects (Account, Opportunity, Contact, Case) keep it
|
|
93
|
+
on.
|
|
94
|
+
- `RecordDetailView` now reads `(objectDef as any)?.detail?.hideReferenceRail`
|
|
95
|
+
and `…?.hideRelatedTab` and threads them to `buildDefaultPageSchema`.
|
|
96
|
+
- The Reference Rail renderer also accepts entries authored as either a
|
|
97
|
+
flat `entries` array or nested under `properties.entries`, so explicit
|
|
98
|
+
`Page` authors can opt-in via the standard spec shape.
|
|
99
|
+
- See `packages/plugin-detail/README.md` (Reference Rail decision matrix)
|
|
100
|
+
for the rationale and per-object guidance.
|
|
101
|
+
|
|
102
|
+
- e9767b0: Remove dead `sys_presence` REST probes from `RecordDetailView` and `AppHeader`. Real-time
|
|
103
|
+
presence does not belong in a regular REST collection — the feature is being redesigned
|
|
104
|
+
behind a transport-level `<PresenceProvider>` (see ROADMAP). This change removes the
|
|
105
|
+
probe (and associated state / unused UI mounts) so the browser no longer makes silently
|
|
106
|
+
swallowed 404 requests on every record open / app navigation. UI surface area is
|
|
107
|
+
unchanged for end users (the previous code never rendered viewers when the probe failed).
|
|
108
|
+
- Updated dependencies [bd8447d]
|
|
109
|
+
- Updated dependencies [fbd5052]
|
|
110
|
+
- Updated dependencies [d51a577]
|
|
111
|
+
- Updated dependencies [1976691]
|
|
112
|
+
- Updated dependencies [d1ec6a2]
|
|
113
|
+
- Updated dependencies [cf30cc2]
|
|
114
|
+
- Updated dependencies [5b80cfd]
|
|
115
|
+
- Updated dependencies [49b1760]
|
|
116
|
+
- Updated dependencies [c0b236f]
|
|
117
|
+
- Updated dependencies [d548d6b]
|
|
118
|
+
- @object-ui/components@5.1.0
|
|
119
|
+
- @object-ui/react@5.1.0
|
|
120
|
+
- @object-ui/i18n@5.1.0
|
|
121
|
+
- @object-ui/types@5.1.0
|
|
122
|
+
- @object-ui/core@5.1.0
|
|
123
|
+
- @object-ui/data-objectstack@5.1.0
|
|
124
|
+
- @object-ui/fields@5.1.0
|
|
125
|
+
- @object-ui/layout@5.1.0
|
|
126
|
+
- @object-ui/auth@5.1.0
|
|
127
|
+
- @object-ui/collaboration@5.1.0
|
|
128
|
+
- @object-ui/permissions@5.1.0
|
|
129
|
+
- @object-ui/providers@5.1.0
|
|
130
|
+
|
|
3
131
|
## 5.0.2
|
|
4
132
|
|
|
5
133
|
### Patch Changes
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -58,14 +58,12 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
58
58
|
const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
|
|
59
59
|
const { currentAppName, recordTitle } = useNavigationContext();
|
|
60
60
|
const mobileSwitcher = useMobileViewSwitcher();
|
|
61
|
-
const [apiPresenceUsers, setApiPresenceUsers] = useState(null);
|
|
62
61
|
const [apiActivities, setApiActivities] = useState(null);
|
|
63
62
|
/** M10.8: in-header notifications. Polled from sys_notification scoped to current user. */
|
|
64
63
|
const [notifications, setNotifications] = useState([]);
|
|
65
64
|
// Once the server returns 404 for these collections we stop retrying for
|
|
66
65
|
// the lifetime of the page — they're optional features and re-requesting
|
|
67
66
|
// on every navigation creates console noise + wasted round trips.
|
|
68
|
-
const presenceUnavailableRef = useRef(false);
|
|
69
67
|
const activityUnavailableRef = useRef(false);
|
|
70
68
|
const notificationsUnavailableRef = useRef(false);
|
|
71
69
|
/** M11.C15: pending approvals count for the topbar shortcut. */
|
|
@@ -79,29 +77,21 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
79
77
|
// registered on the server. Either signal means the feature is
|
|
80
78
|
// unavailable — disable it for the rest of the page.
|
|
81
79
|
const isMissingResource = (err) => err?.httpStatus === 404 || err?.status === 404 || err?.code === 'object_not_found';
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
: dataSource
|
|
80
|
+
// Tenant-wide presence ("who else is online?") is intentionally NOT
|
|
81
|
+
// probed here. Presence is real-time ephemeral state that does not
|
|
82
|
+
// belong in a regular REST collection. The feature is staged behind a
|
|
83
|
+
// transport-level provider (<PresenceProvider>) which is not yet
|
|
84
|
+
// wired — see ROADMAP for the realtime plan.
|
|
85
|
+
if (activityUnavailableRef.current)
|
|
86
|
+
return;
|
|
87
|
+
try {
|
|
88
|
+
const activityResult = await dataSource
|
|
92
89
|
.find('sys_activity', { $orderby: { timestamp: 'desc' }, $top: 20 })
|
|
93
90
|
.catch((err) => {
|
|
94
91
|
if (isMissingResource(err))
|
|
95
92
|
activityUnavailableRef.current = true;
|
|
96
93
|
return { data: [] };
|
|
97
94
|
});
|
|
98
|
-
try {
|
|
99
|
-
const [presenceResult, activityResult] = await Promise.all([presenceP, activityP]);
|
|
100
|
-
if (presenceResult.data?.length) {
|
|
101
|
-
const users = presenceResult.data.filter((u) => typeof u.userId === 'string');
|
|
102
|
-
if (users.length)
|
|
103
|
-
setApiPresenceUsers(users);
|
|
104
|
-
}
|
|
105
95
|
if (activityResult.data?.length) {
|
|
106
96
|
const items = activityResult.data.filter((a) => typeof a.type === 'string');
|
|
107
97
|
if (items.length)
|
|
@@ -277,7 +267,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
277
267
|
const now = new Date().toISOString();
|
|
278
268
|
await Promise.all(unread.map(n => dataSource.update('sys_notification', n.id, { is_read: true, read_at: now }).catch(() => { })));
|
|
279
269
|
}, [dataSource, notifications]);
|
|
280
|
-
const activeUsers = presenceUsers ??
|
|
270
|
+
const activeUsers = presenceUsers ?? EMPTY_PRESENCE_USERS;
|
|
281
271
|
const activeActivities = activities ?? apiActivities ?? [];
|
|
282
272
|
const orgList = organizations ?? [];
|
|
283
273
|
const hasOrgSection = isOrganizationsLoading || orgList.length > 0 || !!activeOrganization;
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
* - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
|
|
11
11
|
*/
|
|
12
12
|
import { useMemo, useState, useCallback, useEffect, useRef, lazy, Suspense } from 'react';
|
|
13
|
-
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
|
13
|
+
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
|
14
14
|
const ObjectChart = lazy(() => import('@object-ui/plugin-charts').then((m) => ({ default: m.ObjectChart })));
|
|
15
15
|
const ImportWizard = lazy(() => import('@object-ui/plugin-grid').then((m) => ({ default: m.ImportWizard })));
|
|
16
16
|
import { ListView } from '@object-ui/plugin-list';
|
|
@@ -197,6 +197,7 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
197
197
|
const navigate = useNavigate();
|
|
198
198
|
const { appName, objectName, viewId } = useParams();
|
|
199
199
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
200
|
+
const location = useLocation();
|
|
200
201
|
const { showDebug } = useMetadataInspector();
|
|
201
202
|
const { t } = useObjectTranslation();
|
|
202
203
|
const { objectLabel, objectDescription: objectDesc, viewLabel, viewEmptyState, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
|
|
@@ -1239,13 +1240,19 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1239
1240
|
// forwarded from `navigation.view` (e.g. 'detail_form'). The view
|
|
1240
1241
|
// variant is resolved by RecordDetailView from its own config, so
|
|
1241
1242
|
// any non-`new_window` action lands on the record detail route.
|
|
1243
|
+
const originState = {
|
|
1244
|
+
from: {
|
|
1245
|
+
pathname: location.pathname + (location.search || ''),
|
|
1246
|
+
label: viewLabel(objectDef.name, activeView?.name ?? '', activeView?.label ?? '') || objectLabel(objectDef),
|
|
1247
|
+
},
|
|
1248
|
+
};
|
|
1242
1249
|
if (viewId) {
|
|
1243
|
-
navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path' });
|
|
1250
|
+
navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path', state: originState });
|
|
1244
1251
|
}
|
|
1245
1252
|
else {
|
|
1246
|
-
navigate(`record/${encodeURIComponent(String(recordId))}
|
|
1253
|
+
navigate(`record/${encodeURIComponent(String(recordId))}`, { state: originState });
|
|
1247
1254
|
}
|
|
1248
|
-
}, [navigate, viewId]);
|
|
1255
|
+
}, [navigate, viewId, location.pathname, location.search, objectDef, activeView?.name, activeView?.label, viewLabel, objectLabel]);
|
|
1249
1256
|
const navOverlay = useNavigationOverlay({
|
|
1250
1257
|
navigation: detailNavigation,
|
|
1251
1258
|
objectName: objectDef.name,
|
|
@@ -1566,15 +1573,21 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1566
1573
|
onEdit?.({ id: recordId });
|
|
1567
1574
|
}
|
|
1568
1575
|
else if (mode === 'view') {
|
|
1576
|
+
const originState = {
|
|
1577
|
+
from: {
|
|
1578
|
+
pathname: location.pathname + (location.search || ''),
|
|
1579
|
+
label: viewLabel(objectDef.name, activeView?.name ?? '', activeView?.label ?? '') || objectLabel(objectDef),
|
|
1580
|
+
},
|
|
1581
|
+
};
|
|
1569
1582
|
if (viewId) {
|
|
1570
|
-
navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path' });
|
|
1583
|
+
navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path', state: originState });
|
|
1571
1584
|
}
|
|
1572
1585
|
else {
|
|
1573
|
-
navigate(`record/${encodeURIComponent(String(recordId))}
|
|
1586
|
+
navigate(`record/${encodeURIComponent(String(recordId))}`, { state: originState });
|
|
1574
1587
|
}
|
|
1575
1588
|
}
|
|
1576
1589
|
},
|
|
1577
|
-
}), [objectDef
|
|
1590
|
+
}), [objectDef, onEdit, activeView?.showSearch, activeView?.showFilters, activeView?.showSort, activeView?.name, activeView?.label, navigate, viewId, isAdmin, location.pathname, location.search, viewLabel, objectLabel]);
|
|
1578
1591
|
return (_jsxs(ActionProvider, { context: {
|
|
1579
1592
|
objectName: objectDef.name,
|
|
1580
1593
|
user: currentUser,
|
|
@@ -7,15 +7,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
* the object field definitions.
|
|
8
8
|
*/
|
|
9
9
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
10
|
-
import { useParams, useNavigate } from 'react-router-dom';
|
|
10
|
+
import { useParams, useNavigate, useLocation, Link } from 'react-router-dom';
|
|
11
11
|
import { DetailView, RecordChatterPanel, buildDefaultPageSchema } from '@object-ui/plugin-detail';
|
|
12
12
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
13
|
-
import { PresenceAvatars } from '@object-ui/collaboration';
|
|
14
13
|
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
15
14
|
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider } from '@object-ui/react';
|
|
16
15
|
import { buildExpandFields } from '@object-ui/core';
|
|
17
16
|
import { toast } from 'sonner';
|
|
18
|
-
import { Database,
|
|
17
|
+
import { Database, ChevronLeft } from 'lucide-react';
|
|
19
18
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
20
19
|
import { SkeletonDetail } from '../skeletons';
|
|
21
20
|
import { ManagedByBadge } from '../components/ManagedByBadge';
|
|
@@ -50,6 +49,24 @@ const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'up
|
|
|
50
49
|
const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
|
|
51
50
|
'organization_id', 'tenant_id', 'is_deleted', 'deleted_at',
|
|
52
51
|
]);
|
|
52
|
+
/**
|
|
53
|
+
* Field-type signals that suggest a "secondary / system / metadata"
|
|
54
|
+
* placement when auto-grouping fields. These move out of the main
|
|
55
|
+
* section and into a collapsible "More details" section by default,
|
|
56
|
+
* keeping the primary section dense with business-critical fields.
|
|
57
|
+
*
|
|
58
|
+
* The heuristic is conservative: when no objectDef metadata is available
|
|
59
|
+
* we surface most fields in the main section; long-form text and
|
|
60
|
+
* audit-by-name fields drop down.
|
|
61
|
+
*/
|
|
62
|
+
const SECONDARY_FIELD_NAME_HINTS = ['description', 'notes', 'note', 'remark', 'remarks', 'comments'];
|
|
63
|
+
const SECONDARY_FIELD_TYPES = new Set(['textarea', 'markdown', 'html', 'rich-text', 'json', 'code']);
|
|
64
|
+
function isSecondaryField(fieldName, fieldDef) {
|
|
65
|
+
if (SECONDARY_FIELD_TYPES.has(fieldDef?.type))
|
|
66
|
+
return true;
|
|
67
|
+
const lc = fieldName.toLowerCase();
|
|
68
|
+
return SECONDARY_FIELD_NAME_HINTS.some((hint) => lc === hint || lc.endsWith(`_${hint}`));
|
|
69
|
+
}
|
|
53
70
|
export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverride, recordIdOverride, embedded }) {
|
|
54
71
|
const params = useParams();
|
|
55
72
|
const appName = params.appName;
|
|
@@ -58,13 +75,15 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
58
75
|
const { showDebug } = useMetadataInspector();
|
|
59
76
|
const { user } = useAuth();
|
|
60
77
|
const navigate = useNavigate();
|
|
78
|
+
const location = useLocation();
|
|
79
|
+
const originFrom = location.state?.from;
|
|
61
80
|
const { t } = useObjectTranslation();
|
|
62
81
|
const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
|
|
63
82
|
const { isFavorite, toggleFavorite, refreshLabel: refreshFavoriteLabel } = useFavorites();
|
|
64
83
|
const { addRecentItem } = useRecentItems();
|
|
65
84
|
const [isLoading, setIsLoading] = useState(true);
|
|
66
85
|
const [feedItems, setFeedItems] = useState([]);
|
|
67
|
-
const [
|
|
86
|
+
const [mentionSuggestions, setMentionSuggestions] = useState([]);
|
|
68
87
|
const [actionRefreshKey, setActionRefreshKey] = useState(0);
|
|
69
88
|
const [childRelatedData, setChildRelatedData] = useState({});
|
|
70
89
|
const [historyEntries, setHistoryEntries] = useState(null);
|
|
@@ -553,20 +572,56 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
553
572
|
});
|
|
554
573
|
return () => { cancelled = true; };
|
|
555
574
|
}, [dataSource, pureRecordId, objectDef, historyEnabled]);
|
|
575
|
+
// Fetch a directory of active users once per dataSource mount and expose
|
|
576
|
+
// them as @-mention suggestions to the DiscussionContext. Capped at 50 to
|
|
577
|
+
// keep the dropdown tight; hosts can swap in a paginated/server-search
|
|
578
|
+
// implementation later by mounting their own DiscussionContextProvider.
|
|
579
|
+
useEffect(() => {
|
|
580
|
+
if (!dataSource)
|
|
581
|
+
return;
|
|
582
|
+
let cancelled = false;
|
|
583
|
+
(async () => {
|
|
584
|
+
try {
|
|
585
|
+
const res = await dataSource.find('sys_user', {
|
|
586
|
+
$top: 50,
|
|
587
|
+
$select: ['id', 'name', 'email', 'image'],
|
|
588
|
+
});
|
|
589
|
+
if (cancelled)
|
|
590
|
+
return;
|
|
591
|
+
const rows = Array.isArray(res) ? res : res?.data || [];
|
|
592
|
+
const suggestions = rows
|
|
593
|
+
.map((u) => ({
|
|
594
|
+
id: String(u.id),
|
|
595
|
+
label: u.name || u.email || String(u.id),
|
|
596
|
+
avatarUrl: u.image || undefined,
|
|
597
|
+
}))
|
|
598
|
+
.filter((s) => s.label);
|
|
599
|
+
setMentionSuggestions(suggestions);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
// Silently fall back to free-text @mention when the user dir is
|
|
603
|
+
// unavailable (e.g. the backend has no sys_user collection).
|
|
604
|
+
}
|
|
605
|
+
})();
|
|
606
|
+
return () => { cancelled = true; };
|
|
607
|
+
}, [dataSource]);
|
|
556
608
|
// Memoize so the object identity is stable across renders — otherwise
|
|
557
609
|
// any effect that depends on it (e.g. the feed loader below) would
|
|
558
610
|
// re-fire every render and create an infinite request loop.
|
|
559
611
|
const currentUser = useMemo(() => (user ? { id: user.id, name: user.name, avatar: user.image } : FALLBACK_USER), [user?.id, user?.name, user?.image]);
|
|
560
|
-
// Fetch
|
|
612
|
+
// Fetch comments from API.
|
|
613
|
+
//
|
|
614
|
+
// NOTE: Record-level presence ("who else is viewing this record") used to
|
|
615
|
+
// be probed here by `dataSource.find('sys_presence', …)`, but that was an
|
|
616
|
+
// architectural mistake: presence is real-time ephemeral state and does
|
|
617
|
+
// not belong in a regular REST collection. The probe has been removed
|
|
618
|
+
// pending a proper transport-level design (WebSocket-backed
|
|
619
|
+
// `<PresenceProvider>` in @object-ui/collaboration). See ROADMAP for the
|
|
620
|
+
// realtime / OCC plan.
|
|
561
621
|
useEffect(() => {
|
|
562
622
|
if (!dataSource || !objectName || !pureRecordId)
|
|
563
623
|
return;
|
|
564
624
|
const threadId = `${objectName}:${pureRecordId}`;
|
|
565
|
-
// Fetch record viewers
|
|
566
|
-
dataSource.find('sys_presence', { $filter: { recordId: pureRecordId } })
|
|
567
|
-
.then((res) => { if (res.data?.length)
|
|
568
|
-
setRecordViewers(res.data); })
|
|
569
|
-
.catch(() => { });
|
|
570
625
|
// M10.10: Fetch persisted comments from sys_comment. Field names
|
|
571
626
|
// are snake_case to match the platform-objects schema
|
|
572
627
|
// (`packages/platform-objects/src/audit/sys-comment.object.ts`):
|
|
@@ -867,29 +922,55 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
867
922
|
};
|
|
868
923
|
}),
|
|
869
924
|
}))
|
|
870
|
-
:
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
925
|
+
: (() => {
|
|
926
|
+
// Auto-grouping (platform B): when no form sections are authored,
|
|
927
|
+
// split fields into a primary section and a collapsible
|
|
928
|
+
// "More details" section so long-form/secondary fields don't
|
|
929
|
+
// dilute the main grid. The primary section stays untitled so
|
|
930
|
+
// DetailSection still flattens its chrome when alone.
|
|
931
|
+
const allFields = Object.keys(objectDef.fields || {})
|
|
932
|
+
.filter((key) => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden);
|
|
933
|
+
const toField = (key) => {
|
|
934
|
+
const fieldDef = objectDef.fields[key];
|
|
935
|
+
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
936
|
+
return {
|
|
937
|
+
name: key,
|
|
938
|
+
label: fieldDef.label || key,
|
|
939
|
+
type: fieldDef.type || 'text',
|
|
940
|
+
...(fieldDef.options && { options: fieldDef.options }),
|
|
941
|
+
...(refTarget && { reference_to: refTarget }),
|
|
942
|
+
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
|
|
943
|
+
...(fieldDef.currency && { currency: fieldDef.currency }),
|
|
944
|
+
};
|
|
945
|
+
};
|
|
946
|
+
const primaryKeys = allFields.filter((k) => !isSecondaryField(k, objectDef.fields[k]));
|
|
947
|
+
const secondaryKeys = allFields.filter((k) => isSecondaryField(k, objectDef.fields[k]));
|
|
948
|
+
// Below ~6 primary fields the second section often looks awkward
|
|
949
|
+
// — keep the legacy single-untitled-section behaviour. Also
|
|
950
|
+
// honour the "no secondary fields" case the same way.
|
|
951
|
+
if (secondaryKeys.length === 0 || primaryKeys.length === 0) {
|
|
952
|
+
return [
|
|
953
|
+
{
|
|
954
|
+
showBorder: false,
|
|
955
|
+
fields: allFields.map(toField),
|
|
956
|
+
},
|
|
957
|
+
];
|
|
958
|
+
}
|
|
959
|
+
return [
|
|
960
|
+
{
|
|
961
|
+
showBorder: false,
|
|
962
|
+
fields: primaryKeys.map(toField),
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
name: 'details',
|
|
966
|
+
title: sectionLabel(objectDef.name, 'details', t('detail.sectionMoreDetails', 'More details')),
|
|
967
|
+
collapsible: true,
|
|
968
|
+
defaultCollapsed: false,
|
|
969
|
+
showBorder: true,
|
|
970
|
+
fields: secondaryKeys.map(toField),
|
|
971
|
+
},
|
|
972
|
+
];
|
|
973
|
+
})();
|
|
893
974
|
// Audit fields (created_at/created_by/updated_at/updated_by) are NOT
|
|
894
975
|
// appended as a section here — they are surfaced by `<RecordMetaFooter>`
|
|
895
976
|
// (rendered by DetailView) as a single subtle line below the content,
|
|
@@ -1143,6 +1224,24 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1143
1224
|
const affordances = resolveCrudAffordances(objectDef);
|
|
1144
1225
|
const items = [];
|
|
1145
1226
|
if (affordances.edit) {
|
|
1227
|
+
// Inline-edit toggle. Surfaced ABOVE `sys_edit` so the
|
|
1228
|
+
// overflow menu lists field-level editing first — Lightning /
|
|
1229
|
+
// HubSpot put inline edit ("Edit details") above the modal /
|
|
1230
|
+
// form-page edit because in-page editing is the higher-frequency
|
|
1231
|
+
// interaction. Communicates with DetailView via a window event
|
|
1232
|
+
// so we don't need to lift inline-edit state out of the plugin.
|
|
1233
|
+
items.push({
|
|
1234
|
+
name: 'sys_inline_edit',
|
|
1235
|
+
label: t('detail.editFieldsInline', { defaultValue: 'Edit fields' }),
|
|
1236
|
+
type: 'script',
|
|
1237
|
+
locations: ['record_header'],
|
|
1238
|
+
variant: 'outline',
|
|
1239
|
+
onClick: () => {
|
|
1240
|
+
window.dispatchEvent(new CustomEvent('objectui:record:inline-edit-toggle', {
|
|
1241
|
+
detail: { recordId: pureRecordId, objectName },
|
|
1242
|
+
}));
|
|
1243
|
+
},
|
|
1244
|
+
});
|
|
1146
1245
|
items.push({
|
|
1147
1246
|
name: 'sys_edit',
|
|
1148
1247
|
label: t('detail.edit', { defaultValue: 'Edit' }),
|
|
@@ -1191,7 +1290,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1191
1290
|
label: t('detail.delete', { defaultValue: 'Delete' }),
|
|
1192
1291
|
type: 'script',
|
|
1193
1292
|
locations: ['record_header'],
|
|
1194
|
-
variant: '
|
|
1293
|
+
variant: 'destructive',
|
|
1195
1294
|
onClick: async () => {
|
|
1196
1295
|
const msg = t('detail.deleteConfirmation', {
|
|
1197
1296
|
defaultValue: 'Are you sure you want to delete this record?',
|
|
@@ -1250,9 +1349,14 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1250
1349
|
headerActions: synthHeaderActions,
|
|
1251
1350
|
related: synthRelated,
|
|
1252
1351
|
history: synthHistory,
|
|
1352
|
+
// Per-object opt-outs read from `objectDef.detail.*`. Lets
|
|
1353
|
+
// catalog/atomic objects (product, task, ...) keep a focused
|
|
1354
|
+
// single-column layout instead of inheriting the rail.
|
|
1355
|
+
hideReferenceRail: objectDef?.detail?.hideReferenceRail === true || undefined,
|
|
1356
|
+
hideRelatedTab: objectDef?.detail?.hideRelatedTab === true || undefined,
|
|
1253
1357
|
...(assignedSlots ? { slots: assignedSlots } : {}),
|
|
1254
1358
|
});
|
|
1255
|
-
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [
|
|
1359
|
+
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsx("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }) }), _jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, headerSystemActions: synthSystemActions, isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, children: _jsx(HighlightFieldsProvider, { children: _jsx(DiscussionContextProvider, { items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, mentionSuggestions: mentionSuggestions, children: _jsx(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsxs("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: [originFrom?.pathname && originFrom?.label && (_jsxs(Link, { to: originFrom.pathname, className: "inline-flex items-center gap-1 mb-3 text-sm text-muted-foreground hover:text-foreground transition-colors", children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), _jsx("span", { children: originFrom.label })] })), _jsx(SchemaRenderer, { schema: renderedPage }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
|
|
1256
1360
|
position: 'bottom',
|
|
1257
1361
|
collapsible: false,
|
|
1258
1362
|
feed: {
|
|
@@ -1260,7 +1364,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1260
1364
|
enableThreading: true,
|
|
1261
1365
|
showCommentInput: true,
|
|
1262
1366
|
},
|
|
1263
|
-
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) }))] }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Page Schema', data: renderedPage }] })] }) }) }) }) }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
|
|
1367
|
+
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, mentionSuggestions: mentionSuggestions }) }))] }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Page Schema', data: renderedPage }] })] }) }) }) }) }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
|
|
1264
1368
|
if (!open)
|
|
1265
1369
|
setConfirmState(s => ({ ...s, open: false }));
|
|
1266
1370
|
} }), _jsx(ActionParamDialog, { state: paramState, onOpenChange: (open) => {
|
|
@@ -1268,7 +1372,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1268
1372
|
setParamState(s => ({ ...s, open: false }));
|
|
1269
1373
|
} })] }));
|
|
1270
1374
|
}
|
|
1271
|
-
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [
|
|
1375
|
+
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsx("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }) }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, onDataLoaded: (record) => {
|
|
1272
1376
|
if (!record || typeof record !== 'object')
|
|
1273
1377
|
return;
|
|
1274
1378
|
// Resolve the same way DetailView's header does, so the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -27,35 +27,35 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"lucide-react": "^1.16.0",
|
|
29
29
|
"sonner": "^2.0.7",
|
|
30
|
-
"@object-ui/auth": "5.
|
|
31
|
-
"@object-ui/collaboration": "5.
|
|
32
|
-
"@object-ui/components": "5.
|
|
33
|
-
"@object-ui/core": "5.
|
|
34
|
-
"@object-ui/data-objectstack": "5.
|
|
35
|
-
"@object-ui/fields": "5.
|
|
36
|
-
"@object-ui/i18n": "5.
|
|
37
|
-
"@object-ui/layout": "5.
|
|
38
|
-
"@object-ui/permissions": "5.
|
|
39
|
-
"@object-ui/providers": "5.
|
|
40
|
-
"@object-ui/react": "5.
|
|
41
|
-
"@object-ui/types": "5.
|
|
30
|
+
"@object-ui/auth": "5.1.1",
|
|
31
|
+
"@object-ui/collaboration": "5.1.1",
|
|
32
|
+
"@object-ui/components": "5.1.1",
|
|
33
|
+
"@object-ui/core": "5.1.1",
|
|
34
|
+
"@object-ui/data-objectstack": "5.1.1",
|
|
35
|
+
"@object-ui/fields": "5.1.1",
|
|
36
|
+
"@object-ui/i18n": "5.1.1",
|
|
37
|
+
"@object-ui/layout": "5.1.1",
|
|
38
|
+
"@object-ui/permissions": "5.1.1",
|
|
39
|
+
"@object-ui/providers": "5.1.1",
|
|
40
|
+
"@object-ui/react": "5.1.1",
|
|
41
|
+
"@object-ui/types": "5.1.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"react": "^18.0.0 || ^19.0.0",
|
|
45
45
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
46
46
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
47
|
-
"@object-ui/plugin-calendar": "^5.
|
|
48
|
-
"@object-ui/plugin-charts": "^5.
|
|
49
|
-
"@object-ui/plugin-chatbot": "^5.
|
|
50
|
-
"@object-ui/plugin-dashboard": "^5.
|
|
51
|
-
"@object-ui/plugin-designer": "^5.
|
|
52
|
-
"@object-ui/plugin-detail": "^5.
|
|
53
|
-
"@object-ui/plugin-form": "^5.
|
|
54
|
-
"@object-ui/plugin-grid": "^5.
|
|
55
|
-
"@object-ui/plugin-kanban": "^5.
|
|
56
|
-
"@object-ui/plugin-list": "^5.
|
|
57
|
-
"@object-ui/plugin-report": "^5.
|
|
58
|
-
"@object-ui/plugin-view": "^5.
|
|
47
|
+
"@object-ui/plugin-calendar": "^5.1.1",
|
|
48
|
+
"@object-ui/plugin-charts": "^5.1.1",
|
|
49
|
+
"@object-ui/plugin-chatbot": "^5.1.1",
|
|
50
|
+
"@object-ui/plugin-dashboard": "^5.1.1",
|
|
51
|
+
"@object-ui/plugin-designer": "^5.1.1",
|
|
52
|
+
"@object-ui/plugin-detail": "^5.1.1",
|
|
53
|
+
"@object-ui/plugin-form": "^5.1.1",
|
|
54
|
+
"@object-ui/plugin-grid": "^5.1.1",
|
|
55
|
+
"@object-ui/plugin-kanban": "^5.1.1",
|
|
56
|
+
"@object-ui/plugin-list": "^5.1.1",
|
|
57
|
+
"@object-ui/plugin-report": "^5.1.1",
|
|
58
|
+
"@object-ui/plugin-view": "^5.1.1"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^25.9.0",
|