@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
|
-
|
|
83
|
+
key: 'ui.favorites',
|
|
84
84
|
});
|
|
85
85
|
const recent = createObjectStackUserStateAdapter({
|
|
86
86
|
dataSource,
|
|
87
87
|
userId: user.id,
|
|
88
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
}, [
|
|
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 (
|
|
1010
|
-
|
|
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": "
|
|
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": "
|
|
31
|
-
"@object-ui/collaboration": "
|
|
32
|
-
"@object-ui/components": "
|
|
33
|
-
"@object-ui/core": "
|
|
34
|
-
"@object-ui/data-objectstack": "
|
|
35
|
-
"@object-ui/fields": "
|
|
36
|
-
"@object-ui/i18n": "
|
|
37
|
-
"@object-ui/layout": "
|
|
38
|
-
"@object-ui/permissions": "
|
|
39
|
-
"@object-ui/react": "
|
|
40
|
-
"@object-ui/types": "
|
|
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": "^
|
|
47
|
-
"@object-ui/plugin-charts": "^
|
|
48
|
-
"@object-ui/plugin-chatbot": "^
|
|
49
|
-
"@object-ui/plugin-dashboard": "^
|
|
50
|
-
"@object-ui/plugin-designer": "^
|
|
51
|
-
"@object-ui/plugin-detail": "^
|
|
52
|
-
"@object-ui/plugin-form": "^
|
|
53
|
-
"@object-ui/plugin-grid": "^
|
|
54
|
-
"@object-ui/plugin-kanban": "^
|
|
55
|
-
"@object-ui/plugin-list": "^
|
|
56
|
-
"@object-ui/plugin-report": "^
|
|
57
|
-
"@object-ui/plugin-view": "^
|
|
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",
|