@object-ui/app-shell 4.8.0 → 5.0.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 CHANGED
@@ -1,5 +1,306 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8930b15: feat(detail): close the gap between Page-assigned and default record detail pages (Track 1)
8
+
9
+ Custom Lightning-style record detail pages (assigned via `assignedPage` /
10
+ `Page` schemas) used to feel meaningfully poorer than the auto-generated
11
+ default detail view. They were missing cross-cutting affordances and
12
+ shipped with English-only tab labels and heavy bordered section cards
13
+ even when the host locale was Chinese. Track 1 closes the visible gap:
14
+ - **app-shell `RecordDetailView`**: the `assignedPage` branch now wears
15
+ the same chrome as the default branch — lifecycle managed-by badge
16
+ and presence avatars in the top-right, `MetadataPanel` debug panel,
17
+ `ActionConfirmDialog` / `ActionParamDialog`, and an auto-appended
18
+ `RecordChatterPanel` at the bottom of the page. Authors opt out of
19
+ the auto-discussion with `assignedPage.disableDiscussion = true`.
20
+ - **plugin-detail `record:details`**: defaults to `inlineEdit: true` so
21
+ fields are click-to-edit just like the default page, and synthesises
22
+ sections with `showBorder: false` by default so a Lightning page
23
+ doesn't double-wrap every block in a heavy Card.
24
+ - **components `page:tabs` / `page:accordion`**: well-known English
25
+ labels (Details / Related / Activity / History / Notes / Files /
26
+ Tasks / Events / Attachments / Chatter / Discussion / Comments /
27
+ Overview / Summary) auto-translate to Chinese (`zh-CN` / `zh-TW`)
28
+ via a built-in dictionary keyed off `document.documentElement.lang`.
29
+ Authors supplying explicit localised labels (string or
30
+ `{ default, zh-CN, ... }`) are not affected.
31
+ - **i18n provider**: applies the initial language to
32
+ `document.documentElement.lang` on mount (i18next does not fire
33
+ `languageChanged` for the bootstrap language), so locale-aware
34
+ renderers downstream see the right value from the first render.
35
+
36
+ - 186aee8: feat(detail): default-on renderViaSchema for non-assignedPage records
37
+
38
+ Track 3 Phase G slice 6. The synthesized Page schema path (slice 2,
39
+ behind `?renderViaSchema=1`) is now the default rendering pipeline for
40
+ every object without a custom assignedPage. Visual and functional
41
+ parity verified on task and account before flipping.
42
+
43
+ Switches preserved: `?renderViaSchema=0` URL fallback,
44
+ `objectDef.detail.renderViaSchema = false` per-object opt-out.
45
+
46
+ - 927187a: Phase N.1 + N.2: visual polish for record detail pages.
47
+
48
+ **N.1 — System actions on full Lightning pages.** `PageHeaderRenderer`
49
+ now merges `headerSystemActions` from `RecordContext` with authored
50
+ actions (authored wins on name/id collision), so full custom pages
51
+ (lead, opportunity, ...) once again show 编辑 / 分享 / 删除 alongside
52
+ their authored actions. `sys_share` and `sys_delete` now use the
53
+ `outline` variant instead of `destructive` to read better in
54
+ multi-button clusters.
55
+
56
+ **N.2 — Hide empty fields by default in synth detail pages.**
57
+ `record:details` defaults `section.hideEmpty` to `true` so synthesized
58
+ pages don't render label graveyards on first load. The "显示 N 个空字段"
59
+ reveal toggle is preserved as the user-facing escape hatch. Authors can
60
+ opt back into showing every field by setting `hideEmpty: false` on the
61
+ section schema.
62
+
63
+ - 8435860: Phase N.4b: highlight↔body dedup now works for hand-authored Lightning
64
+ pages too.
65
+
66
+ Adds a small `HighlightFieldsContext` registry. `record:highlights`
67
+ registers the field names it currently surfaces; `record:details` unions
68
+ that live set into its `hideFieldNames` filter so a field shown in the
69
+ highlight strip is never duplicated in the section grid below.
70
+
71
+ Previously the dedup only fired for synth-generated pages (via the
72
+ `hideFields` prop passed by `buildDefaultPageSchema`). Custom Lightning
73
+ pages (e.g. opportunity) showed `所属客户` both in the strip and in the
74
+ body. The registry-based approach covers both code paths uniformly with
75
+ no schema author work required.
76
+
77
+ The registry uses `useSyncExternalStore` so adding/removing highlights
78
+ notifies consumers without triggering the provider value identity to
79
+ change — avoiding the update-loop that a naive context implementation
80
+ would cause.
81
+
82
+ `RecordDetailView` mounts `<HighlightFieldsProvider>` once per record
83
+ page so the two renderers share state.
84
+
85
+ - 74962b0: feat(detail): record:discussion schema component + flush accordion variant
86
+ - New `record:discussion` schema type lets authors place the record
87
+ chatter feed anywhere in a custom Page schema. Wired through a
88
+ shared `DiscussionContext` provider on the `assignedPage` branch
89
+ of `RecordDetailView`; auto-append still applies when no explicit
90
+ `record:discussion` / `record:chatter` node is present.
91
+ - `page:accordion` gains a `variant` prop. Default `flush` strips the
92
+ per-item border so accordion sections no longer double-wrap inner
93
+ Card-bearing renderers (RelatedList, etc.). Authors who want the
94
+ old visual pass `variant: 'card'`.
95
+ - `translateLabel` now handles compound labels split by `&`, `and`,
96
+ or `和` (e.g. `Notes & Attachments` → `备注与附件`).
97
+
98
+ - fa4c2cb: feat(detail): renderViaSchema opt-in routes default detail through SchemaRenderer (Track 3 Phase G slice 2)
99
+
100
+ When `?renderViaSchema=1` is in the URL, or `objectDef.detail.renderViaSchema === true`,
101
+ `RecordDetailView`'s no-assignedPage branch now synthesizes a canonical
102
+ Page schema (`page:header` → `record:highlights` → `record:path` →
103
+ `page:tabs(record:details)` → `record:discussion`) via
104
+ `buildDefaultPageSchema(objectDef, { sections, highlightFields })` and
105
+ renders it through the existing `<SchemaRenderer>` pipeline.
106
+
107
+ This means every object without a custom assigned page can opt in to
108
+ the same chrome (record-aware header chip, chevron path, flush
109
+ accordion, discussion slot) that custom Lightning pages already enjoy.
110
+
111
+ Changes:
112
+ - `buildDefaultPageSchema` now emits `page:tabs.items` (correct shape
113
+ for the renderer) rather than `tabs`.
114
+ - `PageHeaderRenderer.resolvedTitle` honors `objectSchema.primaryField`
115
+ before the legacy `name/title/display_name/label` fallbacks.
116
+ - `RecordDetailView` rebuilds the synthesized schema with
117
+ `detailSchema.sections` + `highlightFields` at render time so
118
+ `record:details` inherits the same field layout the legacy
119
+ `<DetailView>` would have produced.
120
+
121
+ Flag is intentionally off by default — flipping the default is a
122
+ separate explicit commit after empirical parity validation across
123
+ multiple objects. Known gaps tracked for slice 3: titleFormat
124
+ fallback for objects without `primaryField`, auto Activity / History
125
+ tabs, header-action buttons.
126
+
127
+ - 7213027: feat(detail): slotted record pages (Track 3 Phase I)
128
+
129
+ Introduce `kind: "slotted"` record pages that override one or more
130
+ named slots while letting the default-page synthesizer fill in the
131
+ rest. Authors no longer need to re-author the entire page just to
132
+ customize the header or one tab.
133
+
134
+ **Slot menu (v1):**
135
+ - `header` — replaces `page:header`
136
+ - `actions` — replaces the `record:quick_actions` action bar
137
+ - `highlights` — replaces the chips + chevron path strip
138
+ - `details` — replaces the Details tab body (other tabs stay synthesized)
139
+ - `tabs` — replaces the entire `page:tabs` node (wins over `details`)
140
+ - `discussion` — replaces the inline `record:discussion` footer
141
+
142
+ Each slot is a full replacement at the slot boundary. To compose
143
+ default + custom, call the corresponding `buildDefault*` sub-builder
144
+ (now exported from `@object-ui/plugin-detail`):
145
+ `buildDefaultHeader`, `buildDefaultActions`, `buildDefaultHighlights`,
146
+ `buildDefaultDetails`, `buildDefaultTabs`, `buildDefaultDiscussion`.
147
+
148
+ **Author shape:**
149
+
150
+ ```ts
151
+ {
152
+ type: 'record',
153
+ object: 'account',
154
+ kind: 'slotted',
155
+ slots: {
156
+ header: { type: 'page:header', properties: { ... } },
157
+ },
158
+ }
159
+ ```
160
+
161
+ **API changes:**
162
+ - `PageSchema` (in `@object-ui/types`): adds `kind?: 'full' | 'slotted'`
163
+ (default `'full'`) and `slots?: PageSlotMap`.
164
+ - `usePageAssignment` (in `@object-ui/react`): result now exposes a
165
+ `slots` field populated when the matched page has `kind === 'slotted'`.
166
+ Existing `page` field is unchanged for full pages.
167
+ - `buildDefaultPageSchema` (in `@object-ui/plugin-detail`): accepts an
168
+ `options.slots` map that overrides individual regions at synthesis time.
169
+
170
+ - 34b66bf: feat(detail): synthesize Related / Activity / History tabs + record:quick_actions header (Track 3 Phase G slice 4)
171
+ - `buildDefaultPageSchema` now accepts `headerActions`, `related`,
172
+ `showActivity`, and `history` options. When provided, the synthesizer
173
+ emits a `record:quick_actions` node after `page:header` and appends
174
+ the corresponding tabs to `page:tabs.items` in stable order
175
+ (Details / Related / Activity / History).
176
+ - New `record:history` renderer wraps the existing `HistoryTimeline`,
177
+ reading `entries` / `loading` from the schema. Host owns fetching.
178
+ - `RecordDetailView` forwards `detailSchema.actions[0].actions`,
179
+ `detailSchema.related[]` (unwrapped to `{objectName,relationshipField}`),
180
+ and `detailSchema.history` into the synthesizer call so the
181
+ `renderViaSchema` path reaches parity with the monolithic DetailView
182
+ tab strip and header action bar.
183
+ - 6 new unit tests covering headerActions emit/skip, Related tab
184
+ shape, Activity opt-in, History entries pass-through, and stable
185
+ tab ordering.
186
+
187
+ No behavior change for objects without the `renderViaSchema` opt-in.
188
+
189
+ - c7561a7: **Unify per-user UI state storage onto `sys_user_preference`.**
190
+
191
+ `createObjectStackUserStateAdapter` previously wrote to a bespoke
192
+ `user_app_state` object using `(user_id, kind, payload)` columns. That
193
+ parallel KV table duplicated the canonical per-user preference store
194
+ shipped by `@objectstack/plugin-auth`, and pulled UI traces (favorites,
195
+ recent items, grid widths) out of the place users actually look for
196
+ their settings.
197
+
198
+ The adapter now defaults to:
199
+ - `resource`: `sys_user_preference`
200
+ - field shape: `(user_id, key, value)` instead of `(user_id, kind, payload)`
201
+ - option name: **`key`** instead of `kind`
202
+
203
+ `ConsoleShell` is updated to attach favorites/recent under the namespaced
204
+ keys `ui.favorites` and `ui.recent`. Recommended convention for new
205
+ adapters: keep machine-written UI traces under `ui.*` so they stay
206
+ distinguishable from user-facing preferences (`theme`, `locale`, ...).
207
+
208
+ **Migration**: callers passing `kind:` need to switch to `key:`. Callers
209
+ relying on the old `user_app_state` table can pin
210
+ `resource: 'user_app_state'` to keep the legacy behaviour, but no
211
+ backend ships that schema and the new default works against any
212
+ plugin-auth-enabled environment with zero extra setup.
213
+
214
+ ### Patch Changes
215
+
216
+ - 983d5ad: fix(app-shell): suppress duplicate discussion panel on record detail pages
217
+
218
+ `RecordDetailView` auto-appends a `RecordChatterPanel` below the
219
+ rendered page unless an explicit `record:discussion` / `record:chatter`
220
+ node is found in the schema. The detection walker recursed into
221
+ `children / items / body / components / properties.*` but **not**
222
+ `regions[]`. Synthesised pages (`buildDefaultPageSchema`) and authored
223
+ full-Lightning pages place `record:discussion` inside
224
+ `regions[0].components`, so the walker missed it and a second
225
+ discussion panel rendered on top of the first.
226
+
227
+ Extracted the walker into `utils/pageSchemaIntrospect.ts`, added a
228
+ `regions` branch, and covered both shapes with unit tests.
229
+
230
+ Verified in browser on account (slotted), opportunity (full), lead,
231
+ contact, and task — each renders exactly one discussion panel.
232
+
233
+ - a4c10b2: Restore Edit / Share / Delete system actions on synthesized record detail headers.
234
+
235
+ Phase G slice 6 flipped the synth detail page on by default but did not
236
+ forward the legacy DetailView's built-in system actions to the new
237
+ `record:quick_actions` bar. Objects without authored `record_header`
238
+ business actions ended up with a bare header (only the ★ favorite +
239
+ copy-id chip from `page:header`).
240
+
241
+ This patch injects gated system actions into `synthHeaderActions` for
242
+ both the synth and slotted paths:
243
+ - `sys_edit` — visible when `affordances.edit`. Calls the existing
244
+ `onEdit` prop, opening the same form modal as before.
245
+ - `sys_share` — always visible. Uses `navigator.share` when available;
246
+ falls back to clipboard copy of the current URL with a toast.
247
+ - `sys_delete` — visible when `affordances.delete`. Confirms via
248
+ `window.confirm`, calls `dataSource.delete`, then navigates back to
249
+ the list.
250
+
251
+ Business / custom actions (e.g. Lead.convert, Contact.set_primary)
252
+ continue to render alongside the system actions, unchanged. Full
253
+ Lightning pages (objects with an `assignedPage`) are unaffected — they
254
+ remain author-owned.
255
+
256
+ - Updated dependencies [542cca9]
257
+ - Updated dependencies [8930b15]
258
+ - Updated dependencies [95b6b21]
259
+ - Updated dependencies [ddb08a7]
260
+ - Updated dependencies [f16a762]
261
+ - Updated dependencies [765d50f]
262
+ - Updated dependencies [927187a]
263
+ - Updated dependencies [bae8ba8]
264
+ - Updated dependencies [8435860]
265
+ - Updated dependencies [bece8ca]
266
+ - Updated dependencies [bb2ea48]
267
+ - Updated dependencies [77c1877]
268
+ - Updated dependencies [b14fe09]
269
+ - Updated dependencies [1911d34]
270
+ - Updated dependencies [ba98039]
271
+ - Updated dependencies [a7bef6e]
272
+ - Updated dependencies [86c04f1]
273
+ - Updated dependencies [74962b0]
274
+ - Updated dependencies [8b850b5]
275
+ - Updated dependencies [3154334]
276
+ - Updated dependencies [fa4c2cb]
277
+ - Updated dependencies [7213027]
278
+ - Updated dependencies [34b66bf]
279
+ - Updated dependencies [c7561a7]
280
+ - @object-ui/plugin-detail@5.0.0
281
+ - @object-ui/components@5.0.0
282
+ - @object-ui/i18n@5.0.0
283
+ - @object-ui/layout@5.0.0
284
+ - @object-ui/react@5.0.0
285
+ - @object-ui/types@5.0.0
286
+ - @object-ui/data-objectstack@5.0.0
287
+ - @object-ui/plugin-calendar@5.0.0
288
+ - @object-ui/plugin-kanban@5.0.0
289
+ - @object-ui/fields@5.0.0
290
+ - @object-ui/plugin-charts@5.0.0
291
+ - @object-ui/plugin-chatbot@5.0.0
292
+ - @object-ui/plugin-dashboard@5.0.0
293
+ - @object-ui/plugin-designer@5.0.0
294
+ - @object-ui/plugin-form@5.0.0
295
+ - @object-ui/plugin-grid@5.0.0
296
+ - @object-ui/plugin-list@5.0.0
297
+ - @object-ui/plugin-report@5.0.0
298
+ - @object-ui/plugin-view@5.0.0
299
+ - @object-ui/auth@5.0.0
300
+ - @object-ui/collaboration@5.0.0
301
+ - @object-ui/core@5.0.0
302
+ - @object-ui/permissions@5.0.0
303
+
3
304
  ## 4.8.0
4
305
 
5
306
  ### Minor Changes
@@ -80,12 +80,12 @@ function UserStateBridge() {
80
80
  const favorites = createObjectStackUserStateAdapter({
81
81
  dataSource,
82
82
  userId: user.id,
83
- kind: 'favorites',
83
+ key: 'ui.favorites',
84
84
  });
85
85
  const recent = createObjectStackUserStateAdapter({
86
86
  dataSource,
87
87
  userId: user.id,
88
- kind: 'recent',
88
+ key: 'ui.recent',
89
89
  });
90
90
  attach('favorites', favorites);
91
91
  attach('recent', recent);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * Helpers that introspect a page schema tree without needing the React
6
+ * runtime. Used by RecordDetailView to decide whether to auto-append
7
+ * a discussion / chatter slot at the bottom of the page.
8
+ */
9
+ /**
10
+ * Walks a page schema tree and returns true if any node has a
11
+ * `type` of `record:discussion` or `record:chatter`.
12
+ *
13
+ * Recurses into:
14
+ * - `children`, `items`, `body`, `components`
15
+ * - `properties.children`, `properties.items`
16
+ * - `regions` (synth + full-Lightning pages nest components here)
17
+ *
18
+ * Cycles are guarded with a WeakSet.
19
+ */
20
+ export declare function hasExplicitDiscussion(root: unknown): boolean;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * Helpers that introspect a page schema tree without needing the React
6
+ * runtime. Used by RecordDetailView to decide whether to auto-append
7
+ * a discussion / chatter slot at the bottom of the page.
8
+ */
9
+ const DISCUSSION_TYPES = new Set(['record:discussion', 'record:chatter']);
10
+ /**
11
+ * Walks a page schema tree and returns true if any node has a
12
+ * `type` of `record:discussion` or `record:chatter`.
13
+ *
14
+ * Recurses into:
15
+ * - `children`, `items`, `body`, `components`
16
+ * - `properties.children`, `properties.items`
17
+ * - `regions` (synth + full-Lightning pages nest components here)
18
+ *
19
+ * Cycles are guarded with a WeakSet.
20
+ */
21
+ export function hasExplicitDiscussion(root) {
22
+ const seen = new WeakSet();
23
+ const walk = (node) => {
24
+ if (!node || typeof node !== 'object')
25
+ return false;
26
+ if (seen.has(node))
27
+ return false;
28
+ seen.add(node);
29
+ if (Array.isArray(node))
30
+ return node.some(walk);
31
+ const t = node?.type;
32
+ if (typeof t === 'string' && DISCUSSION_TYPES.has(t))
33
+ return true;
34
+ const candidates = [
35
+ node.children,
36
+ node.items,
37
+ node.body,
38
+ node.components,
39
+ node.properties?.children,
40
+ node.properties?.items,
41
+ // Synth + full-Lightning pages nest components inside
42
+ // `regions[].components[]`. Without this branch the walker
43
+ // fails to see the `record:discussion` baked in by
44
+ // `buildDefaultPageSchema`, and the host appends a second
45
+ // chatter panel on top of it.
46
+ node.regions,
47
+ ];
48
+ return candidates.some(walk);
49
+ };
50
+ return walk(root);
51
+ }
@@ -8,11 +8,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  */
9
9
  import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
10
10
  import { useParams, useNavigate } from 'react-router-dom';
11
- import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
11
+ import { DetailView, RecordChatterPanel, buildDefaultPageSchema } from '@object-ui/plugin-detail';
12
12
  import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
13
13
  import { PresenceAvatars } from '@object-ui/collaboration';
14
14
  import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
15
- import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer } from '@object-ui/react';
15
+ import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider } from '@object-ui/react';
16
16
  import { buildExpandFields } from '@object-ui/core';
17
17
  import { toast } from 'sonner';
18
18
  import { Database, Users } from 'lucide-react';
@@ -20,6 +20,7 @@ import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
20
20
  import { SkeletonDetail } from '../skeletons';
21
21
  import { ManagedByBadge } from '../components/ManagedByBadge';
22
22
  import { resolveCrudAffordances } from '../utils/crudAffordances';
23
+ import { hasExplicitDiscussion } from '../utils/pageSchemaIntrospect';
23
24
  import { ActionConfirmDialog } from './ActionConfirmDialog';
24
25
  import { ActionParamDialog } from './ActionParamDialog';
25
26
  import { resolveActionParams } from '../utils/resolveActionParams';
@@ -79,11 +80,50 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
79
80
  // it via SchemaRenderer (which dispatches to the registered 'record'
80
81
  // PageRenderer in @object-ui/components). Otherwise we fall through to
81
82
  // the legacy auto-generated DetailView path below.
82
- const { page: assignedPage } = usePageAssignment(objectName);
83
+ //
84
+ // Track 3 Phase G slice 6 — `renderViaSchema` is now default-on. The
85
+ // no-assignedPage branch synthesizes a canonical Page via
86
+ // `buildDefaultPageSchema(objectDef)` so the default detail page rides
87
+ // the same SchemaRenderer pipeline as custom pages. Kill-switches:
88
+ // 1) URL query param `?renderViaSchema=0` (per-request fallback to
89
+ // the legacy DetailView monolith — useful for debugging regressions)
90
+ // 2) `objectDef.detail?.renderViaSchema === false` (per-object opt-out)
91
+ const { page: assignedPage, slots: assignedSlots } = usePageAssignment(objectName);
92
+ const renderViaSchemaFlag = useMemo(() => {
93
+ if (typeof window !== 'undefined') {
94
+ try {
95
+ const qp = new URLSearchParams(window.location.search).get('renderViaSchema');
96
+ if (qp === '0' || qp === 'false')
97
+ return false;
98
+ if (qp === '1' || qp === 'true')
99
+ return true;
100
+ }
101
+ catch { }
102
+ }
103
+ if (objectDef?.detail?.renderViaSchema === false)
104
+ return false;
105
+ return true;
106
+ }, [objectDef]);
107
+ const synthesizedPage = useMemo(() => {
108
+ // Synthesizer drives two cases:
109
+ // 1) no assignedPage at all → pure default detail page
110
+ // 2) assignedSlots (slotted page) → synth with slot overrides
111
+ // In either case the page-record load effect below only needs
112
+ // "is there a page?"; the fully-detailed schema is rebuilt at
113
+ // render time once `detailSchema.sections` are known.
114
+ if (assignedPage)
115
+ return null;
116
+ if (!objectDef)
117
+ return null;
118
+ if (!renderViaSchemaFlag && !assignedSlots)
119
+ return null;
120
+ return buildDefaultPageSchema(objectDef, assignedSlots ? { slots: assignedSlots } : undefined);
121
+ }, [renderViaSchemaFlag, assignedPage, assignedSlots, objectDef]);
122
+ const effectivePage = assignedPage || synthesizedPage;
83
123
  const [pageRecord, setPageRecord] = useState(null);
84
124
  useEffect(() => {
85
125
  let cancelled = false;
86
- if (!assignedPage || !pureRecordId || !objectName || !dataSource?.findOne) {
126
+ if (!effectivePage || !pureRecordId || !objectName || !dataSource?.findOne) {
87
127
  setPageRecord(null);
88
128
  return;
89
129
  }
@@ -107,7 +147,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
107
147
  return () => {
108
148
  cancelled = true;
109
149
  };
110
- }, [assignedPage, objectName, pureRecordId, dataSource, objectDef]);
150
+ }, [effectivePage, objectName, pureRecordId, dataSource, objectDef]);
111
151
  // ─── Action Provider Handlers ───────────────────────────────────────
112
152
  // Confirm dialog state (promise-based)
113
153
  const [confirmState, setConfirmState] = useState({ open: false, message: '' });
@@ -1006,8 +1046,158 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
1006
1046
  if (!objectDef) {
1007
1047
  return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.objectNotFound') }), _jsx(EmptyDescription, { children: t('empty.objectNotFoundDescription', { name: objectName }) })] }) }));
1008
1048
  }
1009
- if (assignedPage) {
1010
- return (_jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, 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: _jsx("div", { className: "bg-background p-3 sm:p-4 lg:p-6", children: _jsx(SchemaRenderer, { schema: assignedPage }) }) }) }));
1049
+ if (effectivePage) {
1050
+ const disableDiscussion = effectivePage?.disableDiscussion === true;
1051
+ // When the page schema embeds an explicit `record:discussion` /
1052
+ // `record:chatter` slot, skip the bottom auto-append so the
1053
+ // author placement (or synth default) wins. The walker recurses
1054
+ // into `regions[]` so `buildDefaultPageSchema` output and
1055
+ // full-Lightning authored pages are both detected.
1056
+ const hasDiscussion = hasExplicitDiscussion(effectivePage);
1057
+ const showAutoDiscussion = !disableDiscussion && !hasDiscussion;
1058
+ // Slice 2 — when we're synthesizing (no author assignedPage), rebuild
1059
+ // the schema with the actual detailSchema.sections + highlight fields
1060
+ // so record:details renders the same field layout the legacy
1061
+ // DetailView would have produced.
1062
+ // Slice 4 — also forward header actions, related lists, activities,
1063
+ // and history so the synthesized page reaches parity with the
1064
+ // monolithic DetailView (tabs strip + record_header quick actions).
1065
+ // Business / custom actions authored on objectDef and routed to the
1066
+ // record_header location (e.g. Lead.convert, Contact.set_primary).
1067
+ const synthBusinessActions = (() => {
1068
+ const acts = detailSchema.actions;
1069
+ if (!Array.isArray(acts))
1070
+ return [];
1071
+ // detailSchema wraps actions in a `{type:'action:bar', actions:[]}`
1072
+ // shape; unwrap to the flat ActionDef[] the renderer expects.
1073
+ const bar = acts.find((a) => Array.isArray(a?.actions));
1074
+ const flat = bar?.actions ?? acts;
1075
+ return Array.isArray(flat) ? flat : [];
1076
+ })();
1077
+ // System actions (Edit / Share / Delete) — the legacy DetailView
1078
+ // monolith always synthesized these. The synth-path replacement
1079
+ // (Phase G slice 6) initially dropped them, leaving objects without
1080
+ // authored record_header actions with a bare header. Re-inject here
1081
+ // so every record page surfaces the basic affordances.
1082
+ const synthSystemActions = (() => {
1083
+ const affordances = resolveCrudAffordances(objectDef);
1084
+ const items = [];
1085
+ if (affordances.edit) {
1086
+ items.push({
1087
+ name: 'sys_edit',
1088
+ label: t('detail.edit', { defaultValue: 'Edit' }),
1089
+ type: 'script',
1090
+ locations: ['record_header'],
1091
+ variant: 'default',
1092
+ onClick: () => onEdit({ id: pureRecordId }),
1093
+ });
1094
+ }
1095
+ items.push({
1096
+ name: 'sys_share',
1097
+ label: t('detail.share', { defaultValue: 'Share' }),
1098
+ type: 'script',
1099
+ locations: ['record_header'],
1100
+ variant: 'outline',
1101
+ onClick: async () => {
1102
+ try {
1103
+ if (navigator.share) {
1104
+ await navigator.share({
1105
+ title: document.title,
1106
+ url: window.location.href,
1107
+ });
1108
+ }
1109
+ else {
1110
+ await navigator.clipboard.writeText(window.location.href);
1111
+ toast.success(t('detail.linkCopied', { defaultValue: 'Link copied' }));
1112
+ }
1113
+ }
1114
+ catch {
1115
+ // user dismissed share sheet — no-op
1116
+ }
1117
+ },
1118
+ });
1119
+ if (affordances.delete) {
1120
+ items.push({
1121
+ name: 'sys_delete',
1122
+ label: t('detail.delete', { defaultValue: 'Delete' }),
1123
+ type: 'script',
1124
+ locations: ['record_header'],
1125
+ variant: 'outline',
1126
+ onClick: async () => {
1127
+ const msg = t('detail.deleteConfirmation', {
1128
+ defaultValue: 'Are you sure you want to delete this record?',
1129
+ });
1130
+ if (!window.confirm(msg))
1131
+ return;
1132
+ try {
1133
+ await dataSource.delete(objectName, pureRecordId);
1134
+ toast.success(t('detail.deleted', { defaultValue: 'Record deleted' }));
1135
+ const baseAppUrl = appName ? `/apps/${appName}` : '';
1136
+ navigate(`${baseAppUrl}/${objectName}`, { replace: true });
1137
+ }
1138
+ catch (err) {
1139
+ toast.error(err?.message || 'Delete failed');
1140
+ }
1141
+ },
1142
+ });
1143
+ }
1144
+ return items;
1145
+ })();
1146
+ // The synth path now hands ONLY business actions to the page schema.
1147
+ // System actions (Edit / Share / Delete) ride through
1148
+ // `RecordContext.headerSystemActions` instead, so they reach both
1149
+ // synth/slotted pages AND authored full-Lightning pages without
1150
+ // mutating the assignedPage tree. `PageHeaderRenderer` dedupes by
1151
+ // name so authored business actions still win on collision.
1152
+ const synthHeaderActions = synthBusinessActions.length > 0 ? synthBusinessActions : undefined;
1153
+ const synthRelated = Array.isArray(detailSchema.related)
1154
+ ? detailSchema.related
1155
+ .filter((r) => r?.api && r?.referenceField)
1156
+ .map((r) => ({
1157
+ title: r.title,
1158
+ objectName: r.api,
1159
+ relationshipField: r.referenceField,
1160
+ ...(Array.isArray(r.columns) ? { columns: r.columns } : {}),
1161
+ ...(typeof r.pageSize === 'number' ? { limit: r.pageSize } : {}),
1162
+ ...(r.icon ? { icon: r.icon } : {}),
1163
+ }))
1164
+ : undefined;
1165
+ const synthHistory = detailSchema.history
1166
+ ? {
1167
+ entries: detailSchema.history.entries ?? [],
1168
+ loading: !!detailSchema.history.loading,
1169
+ emptyText: detailSchema.history.emptyText,
1170
+ }
1171
+ : undefined;
1172
+ const renderedPage = assignedPage
1173
+ ? effectivePage
1174
+ : buildDefaultPageSchema(objectDef, {
1175
+ sections: detailSchema.sections,
1176
+ highlightFields: Array.isArray(detailSchema.highlightFields)
1177
+ ? detailSchema.highlightFields
1178
+ .map((f) => (typeof f === 'string' ? f : f?.name))
1179
+ .filter((n) => !!n)
1180
+ : undefined,
1181
+ headerActions: synthHeaderActions,
1182
+ related: synthRelated,
1183
+ history: synthHistory,
1184
+ ...(assignedSlots ? { slots: assignedSlots } : {}),
1185
+ });
1186
+ return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("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 }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, headerSystemActions: synthSystemActions, children: _jsx(HighlightFieldsProvider, { children: _jsx(DiscussionContextProvider, { items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, 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: [_jsx(SchemaRenderer, { schema: renderedPage }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
1187
+ position: 'bottom',
1188
+ collapsible: false,
1189
+ feed: {
1190
+ enableReactions: true,
1191
+ enableThreading: true,
1192
+ showCommentInput: true,
1193
+ },
1194
+ }, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) }))] }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Page Schema', data: renderedPage }] })] }) }) }) }) }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
1195
+ if (!open)
1196
+ setConfirmState(s => ({ ...s, open: false }));
1197
+ } }), _jsx(ActionParamDialog, { state: paramState, onOpenChange: (open) => {
1198
+ if (!open)
1199
+ setParamState(s => ({ ...s, open: false }));
1200
+ } })] }));
1011
1201
  }
1012
1202
  return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("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 }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _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 }), onDataLoaded: (record) => {
1013
1203
  if (!record || typeof record !== 'object')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "4.8.0",
3
+ "version": "5.0.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.8.0",
31
- "@object-ui/collaboration": "4.8.0",
32
- "@object-ui/components": "4.8.0",
33
- "@object-ui/core": "4.8.0",
34
- "@object-ui/data-objectstack": "4.8.0",
35
- "@object-ui/fields": "4.8.0",
36
- "@object-ui/i18n": "4.8.0",
37
- "@object-ui/layout": "4.8.0",
38
- "@object-ui/permissions": "4.8.0",
39
- "@object-ui/react": "4.8.0",
40
- "@object-ui/types": "4.8.0"
30
+ "@object-ui/auth": "5.0.0",
31
+ "@object-ui/collaboration": "5.0.0",
32
+ "@object-ui/components": "5.0.0",
33
+ "@object-ui/core": "5.0.0",
34
+ "@object-ui/data-objectstack": "5.0.0",
35
+ "@object-ui/fields": "5.0.0",
36
+ "@object-ui/i18n": "5.0.0",
37
+ "@object-ui/layout": "5.0.0",
38
+ "@object-ui/permissions": "5.0.0",
39
+ "@object-ui/react": "5.0.0",
40
+ "@object-ui/types": "5.0.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.8.0",
47
- "@object-ui/plugin-charts": "^4.8.0",
48
- "@object-ui/plugin-chatbot": "^4.8.0",
49
- "@object-ui/plugin-dashboard": "^4.8.0",
50
- "@object-ui/plugin-designer": "^4.8.0",
51
- "@object-ui/plugin-detail": "^4.8.0",
52
- "@object-ui/plugin-form": "^4.8.0",
53
- "@object-ui/plugin-grid": "^4.8.0",
54
- "@object-ui/plugin-kanban": "^4.8.0",
55
- "@object-ui/plugin-list": "^4.8.0",
56
- "@object-ui/plugin-report": "^4.8.0",
57
- "@object-ui/plugin-view": "^4.8.0"
46
+ "@object-ui/plugin-calendar": "^5.0.0",
47
+ "@object-ui/plugin-charts": "^5.0.0",
48
+ "@object-ui/plugin-chatbot": "^5.0.0",
49
+ "@object-ui/plugin-dashboard": "^5.0.0",
50
+ "@object-ui/plugin-designer": "^5.0.0",
51
+ "@object-ui/plugin-detail": "^5.0.0",
52
+ "@object-ui/plugin-form": "^5.0.0",
53
+ "@object-ui/plugin-grid": "^5.0.0",
54
+ "@object-ui/plugin-kanban": "^5.0.0",
55
+ "@object-ui/plugin-list": "^5.0.0",
56
+ "@object-ui/plugin-report": "^5.0.0",
57
+ "@object-ui/plugin-view": "^5.0.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^25.9.0",