@object-ui/app-shell 4.8.0 → 5.0.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 +319 -0
- package/dist/components/ObjectRenderer.js +1 -1
- package/dist/console/ConsoleShell.js +2 -2
- package/dist/console/home/AppCard.js +6 -1
- package/dist/console/home/HomePage.js +4 -4
- package/dist/console/home/QuickActions.js +2 -2
- package/dist/console/home/RecentApps.js +2 -2
- package/dist/console/home/StarredApps.js +2 -2
- package/dist/console/organizations/manage/SettingsPage.js +34 -4
- package/dist/context/FavoritesProvider.d.ts +1 -1
- package/dist/layout/AppHeader.js +2 -2
- package/dist/utils/pageSchemaIntrospect.d.ts +20 -0
- package/dist/utils/pageSchemaIntrospect.js +51 -0
- package/dist/views/ObjectView.js +14 -3
- package/dist/views/RecordDetailView.js +220 -10
- package/package.json +25 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,324 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 5.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- cb4879e: form
|
|
8
|
+
- @object-ui/types@5.0.1
|
|
9
|
+
- @object-ui/core@5.0.1
|
|
10
|
+
- @object-ui/i18n@5.0.1
|
|
11
|
+
- @object-ui/react@5.0.1
|
|
12
|
+
- @object-ui/components@5.0.1
|
|
13
|
+
- @object-ui/fields@5.0.1
|
|
14
|
+
- @object-ui/layout@5.0.1
|
|
15
|
+
- @object-ui/data-objectstack@5.0.1
|
|
16
|
+
- @object-ui/auth@5.0.1
|
|
17
|
+
- @object-ui/permissions@5.0.1
|
|
18
|
+
- @object-ui/collaboration@5.0.1
|
|
19
|
+
- @object-ui/providers@5.0.1
|
|
20
|
+
|
|
21
|
+
## 5.0.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- 8930b15: feat(detail): close the gap between Page-assigned and default record detail pages (Track 1)
|
|
26
|
+
|
|
27
|
+
Custom Lightning-style record detail pages (assigned via `assignedPage` /
|
|
28
|
+
`Page` schemas) used to feel meaningfully poorer than the auto-generated
|
|
29
|
+
default detail view. They were missing cross-cutting affordances and
|
|
30
|
+
shipped with English-only tab labels and heavy bordered section cards
|
|
31
|
+
even when the host locale was Chinese. Track 1 closes the visible gap:
|
|
32
|
+
- **app-shell `RecordDetailView`**: the `assignedPage` branch now wears
|
|
33
|
+
the same chrome as the default branch — lifecycle managed-by badge
|
|
34
|
+
and presence avatars in the top-right, `MetadataPanel` debug panel,
|
|
35
|
+
`ActionConfirmDialog` / `ActionParamDialog`, and an auto-appended
|
|
36
|
+
`RecordChatterPanel` at the bottom of the page. Authors opt out of
|
|
37
|
+
the auto-discussion with `assignedPage.disableDiscussion = true`.
|
|
38
|
+
- **plugin-detail `record:details`**: defaults to `inlineEdit: true` so
|
|
39
|
+
fields are click-to-edit just like the default page, and synthesises
|
|
40
|
+
sections with `showBorder: false` by default so a Lightning page
|
|
41
|
+
doesn't double-wrap every block in a heavy Card.
|
|
42
|
+
- **components `page:tabs` / `page:accordion`**: well-known English
|
|
43
|
+
labels (Details / Related / Activity / History / Notes / Files /
|
|
44
|
+
Tasks / Events / Attachments / Chatter / Discussion / Comments /
|
|
45
|
+
Overview / Summary) auto-translate to Chinese (`zh-CN` / `zh-TW`)
|
|
46
|
+
via a built-in dictionary keyed off `document.documentElement.lang`.
|
|
47
|
+
Authors supplying explicit localised labels (string or
|
|
48
|
+
`{ default, zh-CN, ... }`) are not affected.
|
|
49
|
+
- **i18n provider**: applies the initial language to
|
|
50
|
+
`document.documentElement.lang` on mount (i18next does not fire
|
|
51
|
+
`languageChanged` for the bootstrap language), so locale-aware
|
|
52
|
+
renderers downstream see the right value from the first render.
|
|
53
|
+
|
|
54
|
+
- 186aee8: feat(detail): default-on renderViaSchema for non-assignedPage records
|
|
55
|
+
|
|
56
|
+
Track 3 Phase G slice 6. The synthesized Page schema path (slice 2,
|
|
57
|
+
behind `?renderViaSchema=1`) is now the default rendering pipeline for
|
|
58
|
+
every object without a custom assignedPage. Visual and functional
|
|
59
|
+
parity verified on task and account before flipping.
|
|
60
|
+
|
|
61
|
+
Switches preserved: `?renderViaSchema=0` URL fallback,
|
|
62
|
+
`objectDef.detail.renderViaSchema = false` per-object opt-out.
|
|
63
|
+
|
|
64
|
+
- 927187a: Phase N.1 + N.2: visual polish for record detail pages.
|
|
65
|
+
|
|
66
|
+
**N.1 — System actions on full Lightning pages.** `PageHeaderRenderer`
|
|
67
|
+
now merges `headerSystemActions` from `RecordContext` with authored
|
|
68
|
+
actions (authored wins on name/id collision), so full custom pages
|
|
69
|
+
(lead, opportunity, ...) once again show 编辑 / 分享 / 删除 alongside
|
|
70
|
+
their authored actions. `sys_share` and `sys_delete` now use the
|
|
71
|
+
`outline` variant instead of `destructive` to read better in
|
|
72
|
+
multi-button clusters.
|
|
73
|
+
|
|
74
|
+
**N.2 — Hide empty fields by default in synth detail pages.**
|
|
75
|
+
`record:details` defaults `section.hideEmpty` to `true` so synthesized
|
|
76
|
+
pages don't render label graveyards on first load. The "显示 N 个空字段"
|
|
77
|
+
reveal toggle is preserved as the user-facing escape hatch. Authors can
|
|
78
|
+
opt back into showing every field by setting `hideEmpty: false` on the
|
|
79
|
+
section schema.
|
|
80
|
+
|
|
81
|
+
- 8435860: Phase N.4b: highlight↔body dedup now works for hand-authored Lightning
|
|
82
|
+
pages too.
|
|
83
|
+
|
|
84
|
+
Adds a small `HighlightFieldsContext` registry. `record:highlights`
|
|
85
|
+
registers the field names it currently surfaces; `record:details` unions
|
|
86
|
+
that live set into its `hideFieldNames` filter so a field shown in the
|
|
87
|
+
highlight strip is never duplicated in the section grid below.
|
|
88
|
+
|
|
89
|
+
Previously the dedup only fired for synth-generated pages (via the
|
|
90
|
+
`hideFields` prop passed by `buildDefaultPageSchema`). Custom Lightning
|
|
91
|
+
pages (e.g. opportunity) showed `所属客户` both in the strip and in the
|
|
92
|
+
body. The registry-based approach covers both code paths uniformly with
|
|
93
|
+
no schema author work required.
|
|
94
|
+
|
|
95
|
+
The registry uses `useSyncExternalStore` so adding/removing highlights
|
|
96
|
+
notifies consumers without triggering the provider value identity to
|
|
97
|
+
change — avoiding the update-loop that a naive context implementation
|
|
98
|
+
would cause.
|
|
99
|
+
|
|
100
|
+
`RecordDetailView` mounts `<HighlightFieldsProvider>` once per record
|
|
101
|
+
page so the two renderers share state.
|
|
102
|
+
|
|
103
|
+
- 74962b0: feat(detail): record:discussion schema component + flush accordion variant
|
|
104
|
+
- New `record:discussion` schema type lets authors place the record
|
|
105
|
+
chatter feed anywhere in a custom Page schema. Wired through a
|
|
106
|
+
shared `DiscussionContext` provider on the `assignedPage` branch
|
|
107
|
+
of `RecordDetailView`; auto-append still applies when no explicit
|
|
108
|
+
`record:discussion` / `record:chatter` node is present.
|
|
109
|
+
- `page:accordion` gains a `variant` prop. Default `flush` strips the
|
|
110
|
+
per-item border so accordion sections no longer double-wrap inner
|
|
111
|
+
Card-bearing renderers (RelatedList, etc.). Authors who want the
|
|
112
|
+
old visual pass `variant: 'card'`.
|
|
113
|
+
- `translateLabel` now handles compound labels split by `&`, `and`,
|
|
114
|
+
or `和` (e.g. `Notes & Attachments` → `备注与附件`).
|
|
115
|
+
|
|
116
|
+
- fa4c2cb: feat(detail): renderViaSchema opt-in routes default detail through SchemaRenderer (Track 3 Phase G slice 2)
|
|
117
|
+
|
|
118
|
+
When `?renderViaSchema=1` is in the URL, or `objectDef.detail.renderViaSchema === true`,
|
|
119
|
+
`RecordDetailView`'s no-assignedPage branch now synthesizes a canonical
|
|
120
|
+
Page schema (`page:header` → `record:highlights` → `record:path` →
|
|
121
|
+
`page:tabs(record:details)` → `record:discussion`) via
|
|
122
|
+
`buildDefaultPageSchema(objectDef, { sections, highlightFields })` and
|
|
123
|
+
renders it through the existing `<SchemaRenderer>` pipeline.
|
|
124
|
+
|
|
125
|
+
This means every object without a custom assigned page can opt in to
|
|
126
|
+
the same chrome (record-aware header chip, chevron path, flush
|
|
127
|
+
accordion, discussion slot) that custom Lightning pages already enjoy.
|
|
128
|
+
|
|
129
|
+
Changes:
|
|
130
|
+
- `buildDefaultPageSchema` now emits `page:tabs.items` (correct shape
|
|
131
|
+
for the renderer) rather than `tabs`.
|
|
132
|
+
- `PageHeaderRenderer.resolvedTitle` honors `objectSchema.primaryField`
|
|
133
|
+
before the legacy `name/title/display_name/label` fallbacks.
|
|
134
|
+
- `RecordDetailView` rebuilds the synthesized schema with
|
|
135
|
+
`detailSchema.sections` + `highlightFields` at render time so
|
|
136
|
+
`record:details` inherits the same field layout the legacy
|
|
137
|
+
`<DetailView>` would have produced.
|
|
138
|
+
|
|
139
|
+
Flag is intentionally off by default — flipping the default is a
|
|
140
|
+
separate explicit commit after empirical parity validation across
|
|
141
|
+
multiple objects. Known gaps tracked for slice 3: titleFormat
|
|
142
|
+
fallback for objects without `primaryField`, auto Activity / History
|
|
143
|
+
tabs, header-action buttons.
|
|
144
|
+
|
|
145
|
+
- 7213027: feat(detail): slotted record pages (Track 3 Phase I)
|
|
146
|
+
|
|
147
|
+
Introduce `kind: "slotted"` record pages that override one or more
|
|
148
|
+
named slots while letting the default-page synthesizer fill in the
|
|
149
|
+
rest. Authors no longer need to re-author the entire page just to
|
|
150
|
+
customize the header or one tab.
|
|
151
|
+
|
|
152
|
+
**Slot menu (v1):**
|
|
153
|
+
- `header` — replaces `page:header`
|
|
154
|
+
- `actions` — replaces the `record:quick_actions` action bar
|
|
155
|
+
- `highlights` — replaces the chips + chevron path strip
|
|
156
|
+
- `details` — replaces the Details tab body (other tabs stay synthesized)
|
|
157
|
+
- `tabs` — replaces the entire `page:tabs` node (wins over `details`)
|
|
158
|
+
- `discussion` — replaces the inline `record:discussion` footer
|
|
159
|
+
|
|
160
|
+
Each slot is a full replacement at the slot boundary. To compose
|
|
161
|
+
default + custom, call the corresponding `buildDefault*` sub-builder
|
|
162
|
+
(now exported from `@object-ui/plugin-detail`):
|
|
163
|
+
`buildDefaultHeader`, `buildDefaultActions`, `buildDefaultHighlights`,
|
|
164
|
+
`buildDefaultDetails`, `buildDefaultTabs`, `buildDefaultDiscussion`.
|
|
165
|
+
|
|
166
|
+
**Author shape:**
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
{
|
|
170
|
+
type: 'record',
|
|
171
|
+
object: 'account',
|
|
172
|
+
kind: 'slotted',
|
|
173
|
+
slots: {
|
|
174
|
+
header: { type: 'page:header', properties: { ... } },
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**API changes:**
|
|
180
|
+
- `PageSchema` (in `@object-ui/types`): adds `kind?: 'full' | 'slotted'`
|
|
181
|
+
(default `'full'`) and `slots?: PageSlotMap`.
|
|
182
|
+
- `usePageAssignment` (in `@object-ui/react`): result now exposes a
|
|
183
|
+
`slots` field populated when the matched page has `kind === 'slotted'`.
|
|
184
|
+
Existing `page` field is unchanged for full pages.
|
|
185
|
+
- `buildDefaultPageSchema` (in `@object-ui/plugin-detail`): accepts an
|
|
186
|
+
`options.slots` map that overrides individual regions at synthesis time.
|
|
187
|
+
|
|
188
|
+
- 34b66bf: feat(detail): synthesize Related / Activity / History tabs + record:quick_actions header (Track 3 Phase G slice 4)
|
|
189
|
+
- `buildDefaultPageSchema` now accepts `headerActions`, `related`,
|
|
190
|
+
`showActivity`, and `history` options. When provided, the synthesizer
|
|
191
|
+
emits a `record:quick_actions` node after `page:header` and appends
|
|
192
|
+
the corresponding tabs to `page:tabs.items` in stable order
|
|
193
|
+
(Details / Related / Activity / History).
|
|
194
|
+
- New `record:history` renderer wraps the existing `HistoryTimeline`,
|
|
195
|
+
reading `entries` / `loading` from the schema. Host owns fetching.
|
|
196
|
+
- `RecordDetailView` forwards `detailSchema.actions[0].actions`,
|
|
197
|
+
`detailSchema.related[]` (unwrapped to `{objectName,relationshipField}`),
|
|
198
|
+
and `detailSchema.history` into the synthesizer call so the
|
|
199
|
+
`renderViaSchema` path reaches parity with the monolithic DetailView
|
|
200
|
+
tab strip and header action bar.
|
|
201
|
+
- 6 new unit tests covering headerActions emit/skip, Related tab
|
|
202
|
+
shape, Activity opt-in, History entries pass-through, and stable
|
|
203
|
+
tab ordering.
|
|
204
|
+
|
|
205
|
+
No behavior change for objects without the `renderViaSchema` opt-in.
|
|
206
|
+
|
|
207
|
+
- c7561a7: **Unify per-user UI state storage onto `sys_user_preference`.**
|
|
208
|
+
|
|
209
|
+
`createObjectStackUserStateAdapter` previously wrote to a bespoke
|
|
210
|
+
`user_app_state` object using `(user_id, kind, payload)` columns. That
|
|
211
|
+
parallel KV table duplicated the canonical per-user preference store
|
|
212
|
+
shipped by `@objectstack/plugin-auth`, and pulled UI traces (favorites,
|
|
213
|
+
recent items, grid widths) out of the place users actually look for
|
|
214
|
+
their settings.
|
|
215
|
+
|
|
216
|
+
The adapter now defaults to:
|
|
217
|
+
- `resource`: `sys_user_preference`
|
|
218
|
+
- field shape: `(user_id, key, value)` instead of `(user_id, kind, payload)`
|
|
219
|
+
- option name: **`key`** instead of `kind`
|
|
220
|
+
|
|
221
|
+
`ConsoleShell` is updated to attach favorites/recent under the namespaced
|
|
222
|
+
keys `ui.favorites` and `ui.recent`. Recommended convention for new
|
|
223
|
+
adapters: keep machine-written UI traces under `ui.*` so they stay
|
|
224
|
+
distinguishable from user-facing preferences (`theme`, `locale`, ...).
|
|
225
|
+
|
|
226
|
+
**Migration**: callers passing `kind:` need to switch to `key:`. Callers
|
|
227
|
+
relying on the old `user_app_state` table can pin
|
|
228
|
+
`resource: 'user_app_state'` to keep the legacy behaviour, but no
|
|
229
|
+
backend ships that schema and the new default works against any
|
|
230
|
+
plugin-auth-enabled environment with zero extra setup.
|
|
231
|
+
|
|
232
|
+
### Patch Changes
|
|
233
|
+
|
|
234
|
+
- 983d5ad: fix(app-shell): suppress duplicate discussion panel on record detail pages
|
|
235
|
+
|
|
236
|
+
`RecordDetailView` auto-appends a `RecordChatterPanel` below the
|
|
237
|
+
rendered page unless an explicit `record:discussion` / `record:chatter`
|
|
238
|
+
node is found in the schema. The detection walker recursed into
|
|
239
|
+
`children / items / body / components / properties.*` but **not**
|
|
240
|
+
`regions[]`. Synthesised pages (`buildDefaultPageSchema`) and authored
|
|
241
|
+
full-Lightning pages place `record:discussion` inside
|
|
242
|
+
`regions[0].components`, so the walker missed it and a second
|
|
243
|
+
discussion panel rendered on top of the first.
|
|
244
|
+
|
|
245
|
+
Extracted the walker into `utils/pageSchemaIntrospect.ts`, added a
|
|
246
|
+
`regions` branch, and covered both shapes with unit tests.
|
|
247
|
+
|
|
248
|
+
Verified in browser on account (slotted), opportunity (full), lead,
|
|
249
|
+
contact, and task — each renders exactly one discussion panel.
|
|
250
|
+
|
|
251
|
+
- a4c10b2: Restore Edit / Share / Delete system actions on synthesized record detail headers.
|
|
252
|
+
|
|
253
|
+
Phase G slice 6 flipped the synth detail page on by default but did not
|
|
254
|
+
forward the legacy DetailView's built-in system actions to the new
|
|
255
|
+
`record:quick_actions` bar. Objects without authored `record_header`
|
|
256
|
+
business actions ended up with a bare header (only the ★ favorite +
|
|
257
|
+
copy-id chip from `page:header`).
|
|
258
|
+
|
|
259
|
+
This patch injects gated system actions into `synthHeaderActions` for
|
|
260
|
+
both the synth and slotted paths:
|
|
261
|
+
- `sys_edit` — visible when `affordances.edit`. Calls the existing
|
|
262
|
+
`onEdit` prop, opening the same form modal as before.
|
|
263
|
+
- `sys_share` — always visible. Uses `navigator.share` when available;
|
|
264
|
+
falls back to clipboard copy of the current URL with a toast.
|
|
265
|
+
- `sys_delete` — visible when `affordances.delete`. Confirms via
|
|
266
|
+
`window.confirm`, calls `dataSource.delete`, then navigates back to
|
|
267
|
+
the list.
|
|
268
|
+
|
|
269
|
+
Business / custom actions (e.g. Lead.convert, Contact.set_primary)
|
|
270
|
+
continue to render alongside the system actions, unchanged. Full
|
|
271
|
+
Lightning pages (objects with an `assignedPage`) are unaffected — they
|
|
272
|
+
remain author-owned.
|
|
273
|
+
|
|
274
|
+
- Updated dependencies [542cca9]
|
|
275
|
+
- Updated dependencies [8930b15]
|
|
276
|
+
- Updated dependencies [95b6b21]
|
|
277
|
+
- Updated dependencies [ddb08a7]
|
|
278
|
+
- Updated dependencies [f16a762]
|
|
279
|
+
- Updated dependencies [765d50f]
|
|
280
|
+
- Updated dependencies [927187a]
|
|
281
|
+
- Updated dependencies [bae8ba8]
|
|
282
|
+
- Updated dependencies [8435860]
|
|
283
|
+
- Updated dependencies [bece8ca]
|
|
284
|
+
- Updated dependencies [bb2ea48]
|
|
285
|
+
- Updated dependencies [77c1877]
|
|
286
|
+
- Updated dependencies [b14fe09]
|
|
287
|
+
- Updated dependencies [1911d34]
|
|
288
|
+
- Updated dependencies [ba98039]
|
|
289
|
+
- Updated dependencies [a7bef6e]
|
|
290
|
+
- Updated dependencies [86c04f1]
|
|
291
|
+
- Updated dependencies [74962b0]
|
|
292
|
+
- Updated dependencies [8b850b5]
|
|
293
|
+
- Updated dependencies [3154334]
|
|
294
|
+
- Updated dependencies [fa4c2cb]
|
|
295
|
+
- Updated dependencies [7213027]
|
|
296
|
+
- Updated dependencies [34b66bf]
|
|
297
|
+
- Updated dependencies [c7561a7]
|
|
298
|
+
- @object-ui/plugin-detail@5.0.0
|
|
299
|
+
- @object-ui/components@5.0.0
|
|
300
|
+
- @object-ui/i18n@5.0.0
|
|
301
|
+
- @object-ui/layout@5.0.0
|
|
302
|
+
- @object-ui/react@5.0.0
|
|
303
|
+
- @object-ui/types@5.0.0
|
|
304
|
+
- @object-ui/data-objectstack@5.0.0
|
|
305
|
+
- @object-ui/plugin-calendar@5.0.0
|
|
306
|
+
- @object-ui/plugin-kanban@5.0.0
|
|
307
|
+
- @object-ui/fields@5.0.0
|
|
308
|
+
- @object-ui/plugin-charts@5.0.0
|
|
309
|
+
- @object-ui/plugin-chatbot@5.0.0
|
|
310
|
+
- @object-ui/plugin-dashboard@5.0.0
|
|
311
|
+
- @object-ui/plugin-designer@5.0.0
|
|
312
|
+
- @object-ui/plugin-form@5.0.0
|
|
313
|
+
- @object-ui/plugin-grid@5.0.0
|
|
314
|
+
- @object-ui/plugin-list@5.0.0
|
|
315
|
+
- @object-ui/plugin-report@5.0.0
|
|
316
|
+
- @object-ui/plugin-view@5.0.0
|
|
317
|
+
- @object-ui/auth@5.0.0
|
|
318
|
+
- @object-ui/collaboration@5.0.0
|
|
319
|
+
- @object-ui/core@5.0.0
|
|
320
|
+
- @object-ui/permissions@5.0.0
|
|
321
|
+
|
|
3
322
|
## 4.8.0
|
|
4
323
|
|
|
5
324
|
### Minor Changes
|
|
@@ -45,7 +45,7 @@ export function ObjectRenderer({ objectName, viewId, dataSource, onRecordClick:
|
|
|
45
45
|
}
|
|
46
46
|
}, [objectName, dataSource, externalObjectDef]);
|
|
47
47
|
if (loading) {
|
|
48
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: "Loading
|
|
48
|
+
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: "Loading\u2026" }) }));
|
|
49
49
|
}
|
|
50
50
|
if (error) {
|
|
51
51
|
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "text-destructive", children: ["Error: ", error] }) }));
|
|
@@ -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);
|
|
@@ -47,7 +47,12 @@ export function AppCard({ app, onClick, isFavorite, index = 0 }) {
|
|
|
47
47
|
type: 'object',
|
|
48
48
|
});
|
|
49
49
|
};
|
|
50
|
-
return (_jsxs(Card, { className: cn('group relative cursor-pointer overflow-hidden border border-border/70 bg-card/80 backdrop-blur-sm', 'transition-
|
|
50
|
+
return (_jsxs(Card, { role: "button", tabIndex: 0, "aria-label": label, className: cn('group relative cursor-pointer overflow-hidden border border-border/70 bg-card/80 backdrop-blur-sm', 'transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:shadow-lg', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', 'motion-reduce:transition-none motion-reduce:hover:transform-none', !primaryColor && accent.ring), onClick: onClick, onKeyDown: (e) => {
|
|
51
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
onClick();
|
|
54
|
+
}
|
|
55
|
+
}, "data-testid": `app-card-${app.name}`, style: primaryColor ? { borderColor: undefined } : undefined, children: [_jsx("div", { "aria-hidden": true, className: cn('absolute inset-x-0 top-0 h-1', primaryColor ? '' : accent.solid), style: primaryColor ? { backgroundColor: primaryColor } : undefined }), !primaryColor && (_jsx("div", { "aria-hidden": true, className: cn('absolute inset-0 bg-gradient-to-br opacity-0 transition-opacity duration-300 group-hover:opacity-100', accent.from, accent.to) })), _jsxs(CardContent, { className: "relative p-5", children: [_jsx(Button, { variant: "ghost", size: "sm", className: "absolute top-2 right-2 h-8 w-8 p-0 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity", onClick: handleToggleFavorite, "aria-label": isFavorite
|
|
51
56
|
? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' }) + ` — ${label}`
|
|
52
57
|
: t('common.addToFavorites', { defaultValue: 'Add to favorites' }) + ` — ${label}`, "aria-pressed": isFavorite, "data-testid": `favorite-btn-${app.name}`, children: isFavorite ? (_jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })) : (_jsx(StarOff, { className: "h-4 w-4" })) }), _jsx("div", { className: cn('inline-flex h-14 w-14 items-center justify-center rounded-xl mb-4 ring-1 ring-inset', primaryColor ? '' : cn('bg-gradient-to-br', accent.from, accent.to, 'ring-border/40')), style: primaryColor
|
|
53
58
|
? { backgroundColor: `${primaryColor}1f`, boxShadow: `inset 0 0 0 1px ${primaryColor}33` }
|
|
@@ -58,20 +58,20 @@ export function HomePage() {
|
|
|
58
58
|
const { user } = useAuth();
|
|
59
59
|
const activeApps = apps.filter((a) => a.active !== false);
|
|
60
60
|
const recentApps = recentItems
|
|
61
|
-
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page')
|
|
61
|
+
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page' || item.type === 'record')
|
|
62
62
|
.slice(0, 6);
|
|
63
63
|
const starredApps = favorites
|
|
64
|
-
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page')
|
|
64
|
+
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page' || item.type === 'record')
|
|
65
65
|
.slice(0, 8);
|
|
66
66
|
const greeting = useMemo(() => t(pickGreetingKey(new Date().getHours()), { defaultValue: 'Welcome' }), [t]);
|
|
67
67
|
const displayName = (user?.name?.trim() || user?.email?.split('@')[0] || '').trim();
|
|
68
68
|
if (loading) {
|
|
69
|
-
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-20", children: _jsx("div", { className: "text-muted-foreground", children: t('home.loading', { defaultValue: 'Loading workspace
|
|
69
|
+
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-20", children: _jsx("div", { className: "text-muted-foreground", children: t('home.loading', { defaultValue: 'Loading workspace…' }) }) }));
|
|
70
70
|
}
|
|
71
71
|
if (activeApps.length === 0) {
|
|
72
72
|
return (_jsx("div", { className: "flex flex-1 items-center justify-center p-6", children: _jsxs(Empty, { children: [_jsx(EmptyTitle, { children: t('home.welcome', { defaultValue: 'Welcome to ObjectUI' }) }), _jsx(EmptyDescription, { children: t('home.welcomeDescription', {
|
|
73
73
|
defaultValue: 'Get started by creating your first application or configure your system settings.',
|
|
74
74
|
}) }), _jsxs("div", { className: "mt-6 flex flex-col sm:flex-row items-center gap-3", children: [_jsxs(Button, { onClick: () => navigate('/create-app'), "data-testid": "create-first-app-btn", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t('home.createFirstApp', { defaultValue: 'Create Your First App' })] }), _jsxs(Button, { variant: "outline", onClick: () => navigate('/apps/setup'), "data-testid": "go-to-settings-btn", children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('home.systemSettings', { defaultValue: 'System Settings' })] })] })] }) }));
|
|
75
75
|
}
|
|
76
|
-
return (_jsxs("div", { className: "relative isolate min-h-full bg-gradient-to-b from-background via-background to-muted/40", children: [_jsxs("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-x-0 top-0 -z-10 h-[28rem] overflow-hidden", children: [_jsx("div", { className: "absolute -top-32 -left-24 h-[28rem] w-[28rem] rounded-full bg-primary/30 blur-3xl opacity-70 dark:opacity-40" }), _jsx("div", { className: "absolute -top-20 right-[-6rem] h-[26rem] w-[36rem] rounded-full bg-sky-400/30 blur-3xl opacity-70 dark:opacity-35" }), _jsx("div", { className: "absolute top-32 left-1/3 h-[18rem] w-[24rem] rounded-full bg-fuchsia-400/25 blur-3xl opacity-60 dark:opacity-25" }), _jsx("div", { className: "absolute inset-0 bg-gradient-to-b from-transparent via-background/40 to-background" })] }), _jsx("section", { className: "px-4 sm:px-6 lg:px-8 pt-10 pb-6", children: _jsxs("div", { className: "max-w-7xl mx-auto", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground mb-3", children: [_jsx(Sparkles, { className: "h-3.5 w-3.5 text-primary" }), _jsx("span", { className: "uppercase tracking-wider", children: t('home.title', { defaultValue: 'Home' }) })] }), _jsxs("h1", { className: "text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight", children: [_jsxs("span", { className: "bg-gradient-to-r from-foreground via-foreground to-foreground/70 bg-clip-text text-transparent", children: [greeting, displayName ? `, ${displayName}` : ''] }), _jsx("span", { className: "text-foreground/40", children: "." })] }), _jsx("p", { className: "text-base sm:text-lg text-muted-foreground mt-2 max-w-2xl", children: t('home.heroTagline', { defaultValue: 'Pick up where you left off, or explore something new.' }) })] }) }), _jsx("div", { className: "px-4 sm:px-6 lg:px-8 pb-16", children: _jsxs("div", { className: "max-w-7xl mx-auto space-y-10", children: [starredApps.length === 0 && recentApps.length === 0 && (_jsx(GettingStartedHint, { t: t })), starredApps.length > 0 && _jsx(StarredApps, { items: starredApps }), recentApps.length > 0 && _jsx(RecentApps, { items: recentApps }), _jsxs("section", { children: [_jsx("div", { className: "flex items-end justify-between mb-5", children: _jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.allApps', { defaultValue: 'All Applications' }) }), _jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [activeApps.length, ' · ', t('home.stats.apps', { defaultValue: 'Applications' })] })] }) }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4", children: activeApps.map((app, idx) => (_jsx(AppCard, { app: app, index: idx, onClick: () => navigate(`/apps/${app.name}`), isFavorite: favorites.some(f => f.id === `app:${app.name}`) }, app.name))) })] })] }) })] }));
|
|
76
|
+
return (_jsxs("div", { className: "relative isolate min-h-full bg-gradient-to-b from-background via-background to-muted/40", children: [_jsxs("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-x-0 top-0 -z-10 h-[28rem] overflow-hidden", children: [_jsx("div", { className: "absolute -top-32 -left-24 h-[28rem] w-[28rem] rounded-full bg-primary/30 blur-3xl opacity-70 dark:opacity-40" }), _jsx("div", { className: "absolute -top-20 right-[-6rem] h-[26rem] w-[36rem] rounded-full bg-sky-400/30 blur-3xl opacity-70 dark:opacity-35" }), _jsx("div", { className: "absolute top-32 left-1/3 h-[18rem] w-[24rem] rounded-full bg-fuchsia-400/25 blur-3xl opacity-60 dark:opacity-25" }), _jsx("div", { className: "absolute inset-0 bg-gradient-to-b from-transparent via-background/40 to-background" })] }), _jsx("section", { className: "px-4 sm:px-6 lg:px-8 pt-10 pb-6", children: _jsxs("div", { className: "max-w-7xl mx-auto", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground mb-3", children: [_jsx(Sparkles, { className: "h-3.5 w-3.5 text-primary" }), _jsx("span", { className: "uppercase tracking-wider", children: t('home.title', { defaultValue: 'Home' }) })] }), _jsxs("h1", { className: "text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-pretty", children: [_jsxs("span", { className: "bg-gradient-to-r from-foreground via-foreground to-foreground/70 bg-clip-text text-transparent", children: [greeting, displayName ? `, ${displayName}` : ''] }), _jsx("span", { className: "text-foreground/40", children: "." })] }), _jsx("p", { className: "text-base sm:text-lg text-muted-foreground mt-2 max-w-2xl", children: t('home.heroTagline', { defaultValue: 'Pick up where you left off, or explore something new.' }) })] }) }), _jsx("div", { className: "px-4 sm:px-6 lg:px-8 pb-16", children: _jsxs("div", { className: "max-w-7xl mx-auto space-y-10", children: [starredApps.length === 0 && recentApps.length === 0 && (_jsx(GettingStartedHint, { t: t })), starredApps.length > 0 && _jsx(StarredApps, { items: starredApps }), recentApps.length > 0 && _jsx(RecentApps, { items: recentApps }), _jsxs("section", { children: [_jsx("div", { className: "flex items-end justify-between mb-5", children: _jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.allApps', { defaultValue: 'All Applications' }) }), _jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [activeApps.length, ' · ', t('home.stats.apps', { defaultValue: 'Applications' })] })] }) }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4", children: activeApps.map((app, idx) => (_jsx(AppCard, { app: app, index: idx, onClick: () => navigate(`/apps/${app.name}`), isFavorite: favorites.some(f => f.id === `app:${app.name}`) }, app.name))) })] })] }) })] }));
|
|
77
77
|
}
|
|
@@ -49,11 +49,11 @@ export function QuickActions() {
|
|
|
49
49
|
];
|
|
50
50
|
return (_jsxs("section", { children: [_jsx("h2", { className: "text-2xl font-semibold tracking-tight mb-5", children: t('home.quickActions.title', { defaultValue: 'Quick Actions' }) }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4", children: actions.map((action) => {
|
|
51
51
|
const Icon = action.icon;
|
|
52
|
-
return (_jsx(Card, { className: cn('group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm', 'transition-
|
|
52
|
+
return (_jsx(Card, { className: cn('group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm', 'transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:shadow-md motion-reduce:transition-none motion-reduce:hover:transform-none', action.hoverBorder), onClick: () => navigate(action.href), "data-testid": `quick-action-${action.id}`, role: "link", tabIndex: 0, onKeyDown: (e) => {
|
|
53
53
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
54
54
|
e.preventDefault();
|
|
55
55
|
navigate(action.href);
|
|
56
56
|
}
|
|
57
|
-
}, "aria-label": action.label, children: _jsx(CardContent, { className: "p-5", children: _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: cn('inline-flex h-11 w-11 items-center justify-center rounded-xl ring-1 ring-inset shrink-0', action.iconBg), children: _jsx(Icon, { className: cn('h-5 w-5', action.iconText) }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("h3", { className: "font-semibold text-base leading-tight", children: action.label }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-
|
|
57
|
+
}, "aria-label": action.label, children: _jsx(CardContent, { className: "p-5", children: _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: cn('inline-flex h-11 w-11 items-center justify-center rounded-xl ring-1 ring-inset shrink-0', action.iconBg), children: _jsx(Icon, { className: cn('h-5 w-5', action.iconText) }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("h3", { className: "font-semibold text-base leading-tight", children: action.label }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-[opacity,transform] duration-200 group-hover:opacity-100 group-hover:translate-x-0" })] }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: action.description })] })] }) }) }, action.id));
|
|
58
58
|
}) })] }));
|
|
59
59
|
}
|
|
@@ -29,11 +29,11 @@ export function RecentApps({ items }) {
|
|
|
29
29
|
defaultValue: capitalizeFirst(item.type),
|
|
30
30
|
});
|
|
31
31
|
const tone = TYPE_TONES[item.type] || TYPE_TONES.object;
|
|
32
|
-
return (_jsx(Card, { className: "group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm transition-
|
|
32
|
+
return (_jsx(Card, { className: "group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-foreground/20 motion-reduce:transition-none motion-reduce:hover:transform-none", onClick: () => navigate(item.href), "data-testid": `recent-item-${item.id}`, role: "link", tabIndex: 0, onKeyDown: (e) => {
|
|
33
33
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
34
34
|
e.preventDefault();
|
|
35
35
|
navigate(item.href);
|
|
36
36
|
}
|
|
37
|
-
}, children: _jsx(CardContent, { className: "p-3.5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('inline-flex h-10 w-10 items-center justify-center rounded-lg ring-1 shrink-0', tone), children: _jsx(Icon, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-medium text-sm truncate", children: item.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: typeLabel })] }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-
|
|
37
|
+
}, children: _jsx(CardContent, { className: "p-3.5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('inline-flex h-10 w-10 items-center justify-center rounded-lg ring-1 shrink-0', tone), children: _jsx(Icon, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-medium text-sm truncate", children: item.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: typeLabel })] }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-[opacity,transform] duration-200 group-hover:opacity-100 group-hover:translate-x-0" })] }) }) }, item.id));
|
|
38
38
|
}) })] }));
|
|
39
39
|
}
|
|
@@ -26,11 +26,11 @@ export function StarredApps({ items }) {
|
|
|
26
26
|
return (_jsxs("section", { children: [_jsxs("div", { className: "flex items-center gap-2 mb-5", children: [_jsx("span", { className: "inline-flex h-8 w-8 items-center justify-center rounded-lg bg-amber-500/10 ring-1 ring-amber-500/20 text-amber-600 dark:text-amber-400", children: _jsx(Star, { className: "h-4 w-4 fill-current" }) }), _jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: t('home.starredApps.title', { defaultValue: 'Starred' }) })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3", children: items.map((item) => {
|
|
27
27
|
const Icon = getIcon(item.type);
|
|
28
28
|
const tone = TYPE_TONES[item.type] || TYPE_TONES.object;
|
|
29
|
-
return (_jsx(Card, { className: "group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm transition-
|
|
29
|
+
return (_jsx(Card, { className: "group cursor-pointer border border-border/70 bg-card/80 backdrop-blur-sm transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-foreground/20 motion-reduce:transition-none motion-reduce:hover:transform-none", onClick: () => navigate(item.href), "data-testid": `starred-item-${item.id}`, role: "link", tabIndex: 0, onKeyDown: (e) => {
|
|
30
30
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
31
31
|
e.preventDefault();
|
|
32
32
|
navigate(item.href);
|
|
33
33
|
}
|
|
34
|
-
}, children: _jsx(CardContent, { className: "p-3.5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('inline-flex h-10 w-10 items-center justify-center rounded-lg ring-1 shrink-0', tone), children: _jsx(Icon, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-medium text-sm truncate", children: item.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: capitalizeFirst(item.type) })] }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-
|
|
34
|
+
}, children: _jsx(CardContent, { className: "p-3.5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('inline-flex h-10 w-10 items-center justify-center rounded-lg ring-1 shrink-0', tone), children: _jsx(Icon, { className: "h-5 w-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "font-medium text-sm truncate", children: item.label }), _jsx("p", { className: "text-xs text-muted-foreground", children: capitalizeFirst(item.type) })] }), _jsx(ArrowUpRight, { className: "h-4 w-4 text-muted-foreground opacity-0 -translate-x-1 transition-[opacity,transform] duration-200 group-hover:opacity-100 group-hover:translate-x-0" })] }) }) }, item.id));
|
|
35
35
|
}) })] }));
|
|
36
36
|
}
|
|
@@ -4,11 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
*
|
|
5
5
|
* Organization settings: general info form + danger zone.
|
|
6
6
|
*/
|
|
7
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
8
|
-
import { Button, Input, Label, Separator, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@object-ui/components';
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
8
|
+
import { Avatar, AvatarFallback, AvatarImage, Button, Input, Label, Separator, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@object-ui/components';
|
|
9
9
|
import { useAuth } from '@object-ui/auth';
|
|
10
10
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
11
|
-
import {
|
|
11
|
+
import { useUpload } from '@object-ui/providers';
|
|
12
|
+
import { Loader2, Upload, X } from 'lucide-react';
|
|
12
13
|
import { toast } from 'sonner';
|
|
13
14
|
import { useNavigate } from 'react-router-dom';
|
|
14
15
|
import { useOrgContext } from './orgContext';
|
|
@@ -22,6 +23,9 @@ export function SettingsPage() {
|
|
|
22
23
|
const [slug, setSlug] = useState(org.slug ?? '');
|
|
23
24
|
const [logo, setLogo] = useState(org.logo ?? '');
|
|
24
25
|
const [isSaving, setIsSaving] = useState(false);
|
|
26
|
+
const [isUploadingLogo, setIsUploadingLogo] = useState(false);
|
|
27
|
+
const logoInputRef = useRef(null);
|
|
28
|
+
const { upload } = useUpload();
|
|
25
29
|
// Owner check
|
|
26
30
|
const [isOwner, setIsOwner] = useState(null);
|
|
27
31
|
const [membersLoading, setMembersLoading] = useState(true);
|
|
@@ -118,7 +122,33 @@ export function SettingsPage() {
|
|
|
118
122
|
defaultValue: 'Update your organization information.',
|
|
119
123
|
}) }), _jsx(Separator, { className: "my-4" }), !isOwner ? (_jsx("div", { className: "rounded-lg border bg-muted/50 p-4 text-sm text-muted-foreground", children: t('organization.settings.readOnlyNote', {
|
|
120
124
|
defaultValue: 'Only owners can change settings.',
|
|
121
|
-
}) })) : (_jsxs("form", { onSubmit: handleSave, className: "space-y-4 max-w-md", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-name", children: t('organization.settings.nameLabel', { defaultValue: 'Organization name' }) }), _jsx(Input, { id: "org-name", value: name, onChange: (e) => setName(e.target.value), required: true, "data-testid": "settings-name-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-slug", children: t('organization.settings.slugLabel', { defaultValue: 'Slug' }) }), _jsx(Input, { id: "org-slug", value: slug, onChange: (e) => setSlug(e.target.value), "data-testid": "settings-slug-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, {
|
|
125
|
+
}) })) : (_jsxs("form", { onSubmit: handleSave, className: "space-y-4 max-w-md", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-name", children: t('organization.settings.nameLabel', { defaultValue: 'Organization name' }) }), _jsx(Input, { id: "org-name", value: name, onChange: (e) => setName(e.target.value), required: true, "data-testid": "settings-name-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-slug", children: t('organization.settings.slugLabel', { defaultValue: 'Slug' }) }), _jsx(Input, { id: "org-slug", value: slug, onChange: (e) => setSlug(e.target.value), "data-testid": "settings-slug-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { children: t('organization.settings.logoLabel', { defaultValue: 'Logo' }) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Avatar, { className: "size-16 rounded-md", children: [logo ? (_jsx(AvatarImage, { src: logo, alt: name, className: "object-cover" })) : null, _jsx(AvatarFallback, { className: "rounded-md text-base", children: (name || 'O').slice(0, 2).toUpperCase() })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx("input", { ref: logoInputRef, type: "file", accept: "image/*", className: "hidden", onChange: async (e) => {
|
|
126
|
+
const file = e.target.files?.[0];
|
|
127
|
+
if (logoInputRef.current)
|
|
128
|
+
logoInputRef.current.value = '';
|
|
129
|
+
if (!file)
|
|
130
|
+
return;
|
|
131
|
+
setIsUploadingLogo(true);
|
|
132
|
+
try {
|
|
133
|
+
const result = await upload(file);
|
|
134
|
+
setLogo(result.url);
|
|
135
|
+
toast.success(t('organization.settings.logoUploaded', {
|
|
136
|
+
defaultValue: 'Logo uploaded — save to apply',
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
toast.error(err instanceof Error
|
|
141
|
+
? err.message
|
|
142
|
+
: t('organization.settings.logoUploadFailed', {
|
|
143
|
+
defaultValue: 'Failed to upload logo',
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
setIsUploadingLogo(false);
|
|
148
|
+
}
|
|
149
|
+
}, "data-testid": "settings-logo-file" }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: isUploadingLogo, onClick: () => logoInputRef.current?.click(), "data-testid": "settings-logo-upload-btn", children: [isUploadingLogo ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Upload, { className: "mr-2 h-4 w-4" })), logo
|
|
150
|
+
? t('organization.settings.logoReplace', { defaultValue: 'Replace' })
|
|
151
|
+
: t('organization.settings.logoUpload', { defaultValue: 'Upload' })] }), logo && (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setLogo(''), "data-testid": "settings-logo-clear-btn", children: [_jsx(X, { className: "mr-2 h-4 w-4" }), t('organization.settings.logoClear', { defaultValue: 'Remove' })] }))] }), _jsx(Input, { id: "org-logo", type: "url", value: logo, onChange: (e) => setLogo(e.target.value), placeholder: "https://example.com/logo.png", className: "text-xs", "data-testid": "settings-logo-input" })] })] })] }), _jsxs(Button, { type: "submit", disabled: isSaving, "data-testid": "settings-save-btn", children: [isSaving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t('organization.settings.save', { defaultValue: 'Save changes' })] })] }))] }), _jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-destructive", children: t('organization.settings.dangerZone', { defaultValue: 'Danger zone' }) }), _jsx(Separator, { className: "my-4" }), _jsxs("div", { className: "space-y-4 rounded-lg border border-destructive/50 bg-destructive/5 p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("p", { className: "font-medium text-sm", children: t('organization.settings.leaveTitle', { defaultValue: 'Leave organization' }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('organization.settings.leaveDescription', {
|
|
122
152
|
defaultValue: 'You will lose access to this organization.',
|
|
123
153
|
}) })] }), _jsx(Button, { variant: "destructive", size: "sm", onClick: () => setIsLeaveOpen(true), children: t('organization.settings.leaveAction', { defaultValue: 'Leave' }) })] }), _jsx(Separator, {}), _jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("p", { className: "font-medium text-sm", children: t('organization.settings.deleteTitle', { defaultValue: 'Delete organization' }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('organization.settings.deleteDescription', {
|
|
124
154
|
defaultValue: 'Permanently delete this organization and all its data.',
|
|
@@ -22,7 +22,7 @@ export interface FavoriteItem {
|
|
|
22
22
|
id: string;
|
|
23
23
|
label: string;
|
|
24
24
|
href: string;
|
|
25
|
-
type: 'object' | 'dashboard' | 'page' | 'report';
|
|
25
|
+
type: 'object' | 'dashboard' | 'page' | 'report' | 'record';
|
|
26
26
|
/** ISO timestamp of when the item was favorited */
|
|
27
27
|
favoritedAt: string;
|
|
28
28
|
}
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -376,7 +376,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
376
376
|
const lastSegmentLabel = extraSegments[extraSegments.length - 1]?.label || appName || '';
|
|
377
377
|
return (_jsxs("div", { className: "flex items-center justify-between w-full h-full", children: [_jsxs("div", { className: "flex items-center min-w-0 flex-1", children: [_jsx(Link, { to: "/home", className: cn("flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", isApp && "hidden sm:flex"), title: "ObjectStack", children: _jsx(Boxes, { className: "h-4 w-4" }) }), resolvedVariant === 'home' && (_jsx("span", { className: "hidden sm:inline ml-2 text-sm font-semibold tracking-tight", children: "ObjectStack" })), resolvedVariant === 'orgs' && (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: t('organizations.title', { defaultValue: 'Organizations' }) })] })), isApp && (_jsxs(_Fragment, { children: [_jsx(SidebarTrigger, { className: "md:hidden shrink-0 ml-1", "aria-label": t('common.toggleSidebar') || 'Toggle sidebar' }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("div", { className: "hidden sm:flex items-center", children: _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange }) })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("span", { className: "hidden sm:inline text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
|
|
378
378
|
const isLast = i === extraSegments.length - 1;
|
|
379
|
-
return (_jsxs("span", { className: "hidden sm:flex items-center min-w-0", children: [_jsx(PathSep, {}), seg.siblings && seg.siblings.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: `flex items-center gap-1 rounded-md px-1.5 py-1 text-sm font-medium transition-colors outline-none hover:bg-accent hover:text-foreground ${!isLast ? 'text-foreground/60' : 'text-foreground/80'}`, children: [seg.label, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", sideOffset: 8, className: "w-56 max-h-72 overflow-y-auto", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-normal", children: "Switch Object" }), _jsx(DropdownMenuSeparator, {}), seg.siblings.map((sibling) => (_jsx(DropdownMenuItem, { asChild: true, children: _jsx(Link, { to: sibling.href, className: "w-full", children: sibling.label }) }, sibling.href)))] })] })) : seg.href ? (_jsx(Link, { to: seg.href, className: `rounded-md px-1.5 py-1 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label })) : (_jsx("span", { className: `px-1.5 py-1 text-sm font-medium truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label }))] }, i));
|
|
379
|
+
return (_jsxs("span", { className: "hidden sm:flex items-center min-w-0", children: [_jsx(PathSep, {}), seg.siblings && seg.siblings.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: `flex items-center gap-1 rounded-md px-1.5 py-1 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring hover:bg-accent hover:text-foreground ${!isLast ? 'text-foreground/60' : 'text-foreground/80'}`, children: [seg.label, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", sideOffset: 8, className: "w-56 max-h-72 overflow-y-auto", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-normal", children: "Switch Object" }), _jsx(DropdownMenuSeparator, {}), seg.siblings.map((sibling) => (_jsx(DropdownMenuItem, { asChild: true, children: _jsx(Link, { to: sibling.href, className: "w-full", children: sibling.label }) }, sibling.href)))] })] })) : seg.href ? (_jsx(Link, { to: seg.href, className: `rounded-md px-1.5 py-1 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label })) : (_jsx("span", { className: `px-1.5 py-1 text-sm font-medium truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label }))] }, i));
|
|
380
380
|
}), mobileSwitcher && mobileSwitcher.views.length > 0 ? (mobileSwitcher.views.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "sm:hidden flex items-center gap-0.5 min-w-0 ml-1 rounded-md px-1.5 py-1 text-sm font-medium hover:bg-accent active:bg-accent/80 transition-colors", "aria-label": "Switch view", children: [_jsx("span", { className: "truncate max-w-[180px]", children: mobileSwitcher.triggerLabel ??
|
|
381
381
|
mobileSwitcher.views.find((v) => v.id === mobileSwitcher.activeViewId)?.label ??
|
|
382
382
|
lastSegmentLabel }), _jsx(ChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "min-w-[220px] max-w-[280px]", children: mobileSwitcher.views.map((v) => {
|
|
@@ -385,5 +385,5 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
385
385
|
if (!isActive)
|
|
386
386
|
mobileSwitcher.onChange(v.id);
|
|
387
387
|
}, className: "gap-2", children: [v.icon ? (_jsx("span", { className: "shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", children: v.icon })) : null, _jsx("span", { className: "flex-1 truncate", children: v.label }), v.locked ? (_jsx(Lock, { className: "h-3 w-3 shrink-0 text-muted-foreground", "aria-hidden": true })) : null, isActive ? (_jsx(Check, { className: "h-4 w-4 shrink-0 text-foreground", "aria-hidden": true })) : null] }, v.id));
|
|
388
|
-
}) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search
|
|
388
|
+
}) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", asChild: true, "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup/system/profile'), children: [_jsx(UserIcon, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('sidebar.settings', { defaultValue: 'Settings' })] })] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-[11px] font-normal text-muted-foreground uppercase tracking-wide px-2", children: t('user.preferences', { defaultValue: 'Preferences' }) }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.theme', { defaultValue: 'Theme' }) }), _jsx(ModeToggle, {})] }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.language', { defaultValue: 'Language' }) }), _jsx(LocaleSwitcher, {})] }), isAuthEnabled && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] }))] })] })] })] })] }));
|
|
389
389
|
}
|
|
@@ -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
|
+
}
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -19,7 +19,8 @@ import { ObjectView as PluginObjectView, ViewTabBar, ManageViewsDialog } from '@
|
|
|
19
19
|
// uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
|
|
20
20
|
// Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here.
|
|
21
21
|
import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay, } from '@object-ui/components';
|
|
22
|
-
import { Plus, Upload, Table as TableIcon, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
|
|
22
|
+
import { Plus, Upload, Star, StarOff, Table as TableIcon, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
|
|
23
|
+
import { useFavorites } from '../hooks/useFavorites';
|
|
23
24
|
import { getIcon } from '../utils/getIcon';
|
|
24
25
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
25
26
|
import { ViewConfigPanel } from './ViewConfigPanel';
|
|
@@ -194,11 +195,12 @@ function fromSysViewRecord(sv) {
|
|
|
194
195
|
}
|
|
195
196
|
export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
196
197
|
const navigate = useNavigate();
|
|
197
|
-
const { objectName, viewId } = useParams();
|
|
198
|
+
const { appName, objectName, viewId } = useParams();
|
|
198
199
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
199
200
|
const { showDebug } = useMetadataInspector();
|
|
200
201
|
const { t } = useObjectTranslation();
|
|
201
202
|
const { objectLabel, objectDescription: objectDesc, viewLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
|
|
203
|
+
const { isFavorite, toggleFavorite } = useFavorites();
|
|
202
204
|
// Inline view config panel state (Airtable-style right sidebar)
|
|
203
205
|
const [showViewConfigPanel, setShowViewConfigPanel] = useState(false);
|
|
204
206
|
const [viewConfigPanelMode, setViewConfigPanelMode] = useState('edit');
|
|
@@ -1581,7 +1583,16 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1581
1583
|
activeOrganization: activeOrganization
|
|
1582
1584
|
? { id: activeOrganization.id, slug: activeOrganization.slug, name: activeOrganization.name }
|
|
1583
1585
|
: null,
|
|
1584
|
-
}, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsx("div", { className: "hidden sm:block", children: _jsx(PageHeader, { title: _jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: objectLabel(objectDef) }), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), description: objectDef.description ? objectDesc(objectDef) : undefined, icon: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4" }); })(), actions: _jsxs(_Fragment, { children: [
|
|
1586
|
+
}, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsx("div", { className: "hidden sm:block", children: _jsx(PageHeader, { title: _jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: objectLabel(objectDef) }), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), description: objectDef.description ? objectDesc(objectDef) : undefined, icon: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4" }); })(), actions: _jsxs(_Fragment, { children: [objectName && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => toggleFavorite({
|
|
1587
|
+
id: `object:${objectName}`,
|
|
1588
|
+
label: objectLabel(objectDef),
|
|
1589
|
+
href: `/apps/${appName}/${objectName}`,
|
|
1590
|
+
type: 'object',
|
|
1591
|
+
}), className: "h-8 sm:h-9 px-2", "aria-pressed": isFavorite(`object:${objectName}`), "aria-label": isFavorite(`object:${objectName}`)
|
|
1592
|
+
? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
|
|
1593
|
+
: t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `object-favorite-btn-${objectName}`, children: isFavorite(`object:${objectName}`)
|
|
1594
|
+
? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
|
|
1595
|
+
: _jsx(StarOff, { className: "h-4 w-4" }) })), affordances.create && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), affordances.import && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setShowImport(true), className: "hidden sm:inline-flex shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", title: t('console.objectView.importTitle'), "data-testid": "object-view-import-button", children: [_jsx(Upload, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.import') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (_jsx(SchemaRenderer, { schema: {
|
|
1585
1596
|
type: 'action:bar',
|
|
1586
1597
|
location: 'list_toolbar',
|
|
1587
1598
|
actions: (objectDef.actions || []).map((a) => ({
|
|
@@ -8,24 +8,26 @@ 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';
|
|
12
|
-
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
11
|
+
import { DetailView, RecordChatterPanel, buildDefaultPageSchema } from '@object-ui/plugin-detail';
|
|
12
|
+
import { Empty, EmptyTitle, EmptyDescription, Button } 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
|
-
import { Database, Users } from 'lucide-react';
|
|
18
|
+
import { Database, Users, Star, StarOff } from 'lucide-react';
|
|
19
19
|
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';
|
|
26
27
|
import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
|
|
27
28
|
import { useRecordApprovals } from '../hooks/useRecordApprovals';
|
|
28
29
|
import { getRecordDisplayName } from '../utils';
|
|
30
|
+
import { useFavorites } from '../hooks/useFavorites';
|
|
29
31
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
|
|
30
32
|
/**
|
|
31
33
|
* Audit field names auto-injected by the framework's `applySystemFields`.
|
|
@@ -57,6 +59,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
57
59
|
const navigate = useNavigate();
|
|
58
60
|
const { t } = useObjectTranslation();
|
|
59
61
|
const { objectLabel, viewLabel: _vLabel, sectionLabel, actionLabel, actionConfirm, actionSuccess, fieldLabel, fieldOptionLabel } = useObjectLabel();
|
|
62
|
+
const { isFavorite, toggleFavorite } = useFavorites();
|
|
60
63
|
const [isLoading, setIsLoading] = useState(true);
|
|
61
64
|
const [feedItems, setFeedItems] = useState([]);
|
|
62
65
|
const [recordViewers, setRecordViewers] = useState([]);
|
|
@@ -79,11 +82,50 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
79
82
|
// it via SchemaRenderer (which dispatches to the registered 'record'
|
|
80
83
|
// PageRenderer in @object-ui/components). Otherwise we fall through to
|
|
81
84
|
// the legacy auto-generated DetailView path below.
|
|
82
|
-
|
|
85
|
+
//
|
|
86
|
+
// Track 3 Phase G slice 6 — `renderViaSchema` is now default-on. The
|
|
87
|
+
// no-assignedPage branch synthesizes a canonical Page via
|
|
88
|
+
// `buildDefaultPageSchema(objectDef)` so the default detail page rides
|
|
89
|
+
// the same SchemaRenderer pipeline as custom pages. Kill-switches:
|
|
90
|
+
// 1) URL query param `?renderViaSchema=0` (per-request fallback to
|
|
91
|
+
// the legacy DetailView monolith — useful for debugging regressions)
|
|
92
|
+
// 2) `objectDef.detail?.renderViaSchema === false` (per-object opt-out)
|
|
93
|
+
const { page: assignedPage, slots: assignedSlots } = usePageAssignment(objectName);
|
|
94
|
+
const renderViaSchemaFlag = useMemo(() => {
|
|
95
|
+
if (typeof window !== 'undefined') {
|
|
96
|
+
try {
|
|
97
|
+
const qp = new URLSearchParams(window.location.search).get('renderViaSchema');
|
|
98
|
+
if (qp === '0' || qp === 'false')
|
|
99
|
+
return false;
|
|
100
|
+
if (qp === '1' || qp === 'true')
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
}
|
|
105
|
+
if (objectDef?.detail?.renderViaSchema === false)
|
|
106
|
+
return false;
|
|
107
|
+
return true;
|
|
108
|
+
}, [objectDef]);
|
|
109
|
+
const synthesizedPage = useMemo(() => {
|
|
110
|
+
// Synthesizer drives two cases:
|
|
111
|
+
// 1) no assignedPage at all → pure default detail page
|
|
112
|
+
// 2) assignedSlots (slotted page) → synth with slot overrides
|
|
113
|
+
// In either case the page-record load effect below only needs
|
|
114
|
+
// "is there a page?"; the fully-detailed schema is rebuilt at
|
|
115
|
+
// render time once `detailSchema.sections` are known.
|
|
116
|
+
if (assignedPage)
|
|
117
|
+
return null;
|
|
118
|
+
if (!objectDef)
|
|
119
|
+
return null;
|
|
120
|
+
if (!renderViaSchemaFlag && !assignedSlots)
|
|
121
|
+
return null;
|
|
122
|
+
return buildDefaultPageSchema(objectDef, assignedSlots ? { slots: assignedSlots } : undefined);
|
|
123
|
+
}, [renderViaSchemaFlag, assignedPage, assignedSlots, objectDef]);
|
|
124
|
+
const effectivePage = assignedPage || synthesizedPage;
|
|
83
125
|
const [pageRecord, setPageRecord] = useState(null);
|
|
84
126
|
useEffect(() => {
|
|
85
127
|
let cancelled = false;
|
|
86
|
-
if (!
|
|
128
|
+
if (!effectivePage || !pureRecordId || !objectName || !dataSource?.findOne) {
|
|
87
129
|
setPageRecord(null);
|
|
88
130
|
return;
|
|
89
131
|
}
|
|
@@ -107,7 +149,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
107
149
|
return () => {
|
|
108
150
|
cancelled = true;
|
|
109
151
|
};
|
|
110
|
-
}, [
|
|
152
|
+
}, [effectivePage, objectName, pureRecordId, dataSource, objectDef]);
|
|
111
153
|
// ─── Action Provider Handlers ───────────────────────────────────────
|
|
112
154
|
// Confirm dialog state (promise-based)
|
|
113
155
|
const [confirmState, setConfirmState] = useState({ open: false, message: '' });
|
|
@@ -1006,10 +1048,178 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1006
1048
|
if (!objectDef) {
|
|
1007
1049
|
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
1050
|
}
|
|
1009
|
-
if (
|
|
1010
|
-
|
|
1051
|
+
if (effectivePage) {
|
|
1052
|
+
const disableDiscussion = effectivePage?.disableDiscussion === true;
|
|
1053
|
+
// When the page schema embeds an explicit `record:discussion` /
|
|
1054
|
+
// `record:chatter` slot, skip the bottom auto-append so the
|
|
1055
|
+
// author placement (or synth default) wins. The walker recurses
|
|
1056
|
+
// into `regions[]` so `buildDefaultPageSchema` output and
|
|
1057
|
+
// full-Lightning authored pages are both detected.
|
|
1058
|
+
const hasDiscussion = hasExplicitDiscussion(effectivePage);
|
|
1059
|
+
const showAutoDiscussion = !disableDiscussion && !hasDiscussion;
|
|
1060
|
+
// Slice 2 — when we're synthesizing (no author assignedPage), rebuild
|
|
1061
|
+
// the schema with the actual detailSchema.sections + highlight fields
|
|
1062
|
+
// so record:details renders the same field layout the legacy
|
|
1063
|
+
// DetailView would have produced.
|
|
1064
|
+
// Slice 4 — also forward header actions, related lists, activities,
|
|
1065
|
+
// and history so the synthesized page reaches parity with the
|
|
1066
|
+
// monolithic DetailView (tabs strip + record_header quick actions).
|
|
1067
|
+
// Business / custom actions authored on objectDef and routed to the
|
|
1068
|
+
// record_header location (e.g. Lead.convert, Contact.set_primary).
|
|
1069
|
+
const synthBusinessActions = (() => {
|
|
1070
|
+
const acts = detailSchema.actions;
|
|
1071
|
+
if (!Array.isArray(acts))
|
|
1072
|
+
return [];
|
|
1073
|
+
// detailSchema wraps actions in a `{type:'action:bar', actions:[]}`
|
|
1074
|
+
// shape; unwrap to the flat ActionDef[] the renderer expects.
|
|
1075
|
+
const bar = acts.find((a) => Array.isArray(a?.actions));
|
|
1076
|
+
const flat = bar?.actions ?? acts;
|
|
1077
|
+
return Array.isArray(flat) ? flat : [];
|
|
1078
|
+
})();
|
|
1079
|
+
// System actions (Edit / Share / Delete) — the legacy DetailView
|
|
1080
|
+
// monolith always synthesized these. The synth-path replacement
|
|
1081
|
+
// (Phase G slice 6) initially dropped them, leaving objects without
|
|
1082
|
+
// authored record_header actions with a bare header. Re-inject here
|
|
1083
|
+
// so every record page surfaces the basic affordances.
|
|
1084
|
+
const synthSystemActions = (() => {
|
|
1085
|
+
const affordances = resolveCrudAffordances(objectDef);
|
|
1086
|
+
const items = [];
|
|
1087
|
+
if (affordances.edit) {
|
|
1088
|
+
items.push({
|
|
1089
|
+
name: 'sys_edit',
|
|
1090
|
+
label: t('detail.edit', { defaultValue: 'Edit' }),
|
|
1091
|
+
type: 'script',
|
|
1092
|
+
locations: ['record_header'],
|
|
1093
|
+
variant: 'default',
|
|
1094
|
+
onClick: () => onEdit({ id: pureRecordId }),
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
items.push({
|
|
1098
|
+
name: 'sys_share',
|
|
1099
|
+
label: t('detail.share', { defaultValue: 'Share' }),
|
|
1100
|
+
type: 'script',
|
|
1101
|
+
locations: ['record_header'],
|
|
1102
|
+
variant: 'outline',
|
|
1103
|
+
onClick: async () => {
|
|
1104
|
+
try {
|
|
1105
|
+
if (navigator.share) {
|
|
1106
|
+
await navigator.share({
|
|
1107
|
+
title: document.title,
|
|
1108
|
+
url: window.location.href,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
await navigator.clipboard.writeText(window.location.href);
|
|
1113
|
+
toast.success(t('detail.linkCopied', { defaultValue: 'Link copied' }));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
// user dismissed share sheet — no-op
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
});
|
|
1121
|
+
if (affordances.delete) {
|
|
1122
|
+
items.push({
|
|
1123
|
+
name: 'sys_delete',
|
|
1124
|
+
label: t('detail.delete', { defaultValue: 'Delete' }),
|
|
1125
|
+
type: 'script',
|
|
1126
|
+
locations: ['record_header'],
|
|
1127
|
+
variant: 'outline',
|
|
1128
|
+
onClick: async () => {
|
|
1129
|
+
const msg = t('detail.deleteConfirmation', {
|
|
1130
|
+
defaultValue: 'Are you sure you want to delete this record?',
|
|
1131
|
+
});
|
|
1132
|
+
if (!window.confirm(msg))
|
|
1133
|
+
return;
|
|
1134
|
+
try {
|
|
1135
|
+
await dataSource.delete(objectName, pureRecordId);
|
|
1136
|
+
toast.success(t('detail.deleted', { defaultValue: 'Record deleted' }));
|
|
1137
|
+
const baseAppUrl = appName ? `/apps/${appName}` : '';
|
|
1138
|
+
navigate(`${baseAppUrl}/${objectName}`, { replace: true });
|
|
1139
|
+
}
|
|
1140
|
+
catch (err) {
|
|
1141
|
+
toast.error(err?.message || 'Delete failed');
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
return items;
|
|
1147
|
+
})();
|
|
1148
|
+
// The synth path now hands ONLY business actions to the page schema.
|
|
1149
|
+
// System actions (Edit / Share / Delete) ride through
|
|
1150
|
+
// `RecordContext.headerSystemActions` instead, so they reach both
|
|
1151
|
+
// synth/slotted pages AND authored full-Lightning pages without
|
|
1152
|
+
// mutating the assignedPage tree. `PageHeaderRenderer` dedupes by
|
|
1153
|
+
// name so authored business actions still win on collision.
|
|
1154
|
+
const synthHeaderActions = synthBusinessActions.length > 0 ? synthBusinessActions : undefined;
|
|
1155
|
+
const synthRelated = Array.isArray(detailSchema.related)
|
|
1156
|
+
? detailSchema.related
|
|
1157
|
+
.filter((r) => r?.api && r?.referenceField)
|
|
1158
|
+
.map((r) => ({
|
|
1159
|
+
title: r.title,
|
|
1160
|
+
objectName: r.api,
|
|
1161
|
+
relationshipField: r.referenceField,
|
|
1162
|
+
...(Array.isArray(r.columns) ? { columns: r.columns } : {}),
|
|
1163
|
+
...(typeof r.pageSize === 'number' ? { limit: r.pageSize } : {}),
|
|
1164
|
+
...(r.icon ? { icon: r.icon } : {}),
|
|
1165
|
+
}))
|
|
1166
|
+
: undefined;
|
|
1167
|
+
const synthHistory = detailSchema.history
|
|
1168
|
+
? {
|
|
1169
|
+
entries: detailSchema.history.entries ?? [],
|
|
1170
|
+
loading: !!detailSchema.history.loading,
|
|
1171
|
+
emptyText: detailSchema.history.emptyText,
|
|
1172
|
+
}
|
|
1173
|
+
: undefined;
|
|
1174
|
+
const renderedPage = assignedPage
|
|
1175
|
+
? effectivePage
|
|
1176
|
+
: buildDefaultPageSchema(objectDef, {
|
|
1177
|
+
sections: detailSchema.sections,
|
|
1178
|
+
highlightFields: Array.isArray(detailSchema.highlightFields)
|
|
1179
|
+
? detailSchema.highlightFields
|
|
1180
|
+
.map((f) => (typeof f === 'string' ? f : f?.name))
|
|
1181
|
+
.filter((n) => !!n)
|
|
1182
|
+
: undefined,
|
|
1183
|
+
headerActions: synthHeaderActions,
|
|
1184
|
+
related: synthRelated,
|
|
1185
|
+
history: synthHistory,
|
|
1186
|
+
...(assignedSlots ? { slots: assignedSlots } : {}),
|
|
1187
|
+
});
|
|
1188
|
+
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: [objectName && pureRecordId && (_jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", onClick: () => toggleFavorite({
|
|
1189
|
+
id: `record:${objectName}:${pureRecordId}`,
|
|
1190
|
+
label: recordTitle || pureRecordId || '',
|
|
1191
|
+
href: `/apps/${appName}/${objectName}/record/${pureRecordId}`,
|
|
1192
|
+
type: 'record',
|
|
1193
|
+
}), "aria-pressed": isFavorite(`record:${objectName}:${pureRecordId}`), "aria-label": isFavorite(`record:${objectName}:${pureRecordId}`)
|
|
1194
|
+
? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
|
|
1195
|
+
: t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `record-favorite-btn-${pureRecordId}`, children: isFavorite(`record:${objectName}:${pureRecordId}`)
|
|
1196
|
+
? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
|
|
1197
|
+
: _jsx(StarOff, { className: "h-4 w-4" }) })), _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: {
|
|
1198
|
+
position: 'bottom',
|
|
1199
|
+
collapsible: false,
|
|
1200
|
+
feed: {
|
|
1201
|
+
enableReactions: true,
|
|
1202
|
+
enableThreading: true,
|
|
1203
|
+
showCommentInput: true,
|
|
1204
|
+
},
|
|
1205
|
+
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) }))] }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Page Schema', data: renderedPage }] })] }) }) }) }) }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
|
|
1206
|
+
if (!open)
|
|
1207
|
+
setConfirmState(s => ({ ...s, open: false }));
|
|
1208
|
+
} }), _jsx(ActionParamDialog, { state: paramState, onOpenChange: (open) => {
|
|
1209
|
+
if (!open)
|
|
1210
|
+
setParamState(s => ({ ...s, open: false }));
|
|
1211
|
+
} })] }));
|
|
1011
1212
|
}
|
|
1012
|
-
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: [
|
|
1213
|
+
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: [objectName && pureRecordId && (_jsx(Button, { size: "sm", variant: "ghost", className: "h-8 w-8 p-0", onClick: () => toggleFavorite({
|
|
1214
|
+
id: `record:${objectName}:${pureRecordId}`,
|
|
1215
|
+
label: recordTitle || pureRecordId || '',
|
|
1216
|
+
href: `/apps/${appName}/${objectName}/record/${pureRecordId}`,
|
|
1217
|
+
type: 'record',
|
|
1218
|
+
}), "aria-pressed": isFavorite(`record:${objectName}:${pureRecordId}`), "aria-label": isFavorite(`record:${objectName}:${pureRecordId}`)
|
|
1219
|
+
? t('common.removeFromFavorites', { defaultValue: 'Remove from favorites' })
|
|
1220
|
+
: t('common.addToFavorites', { defaultValue: 'Add to favorites' }), "data-testid": `record-favorite-btn-${pureRecordId}`, children: isFavorite(`record:${objectName}:${pureRecordId}`)
|
|
1221
|
+
? _jsx(Star, { className: "h-4 w-4 fill-amber-400 text-amber-400" })
|
|
1222
|
+
: _jsx(StarOff, { className: "h-4 w-4" }) })), _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
1223
|
if (!record || typeof record !== 'object')
|
|
1014
1224
|
return;
|
|
1015
1225
|
// 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": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -27,34 +27,35 @@
|
|
|
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/
|
|
40
|
-
"@object-ui/
|
|
30
|
+
"@object-ui/auth": "5.0.1",
|
|
31
|
+
"@object-ui/collaboration": "5.0.1",
|
|
32
|
+
"@object-ui/components": "5.0.1",
|
|
33
|
+
"@object-ui/core": "5.0.1",
|
|
34
|
+
"@object-ui/data-objectstack": "5.0.1",
|
|
35
|
+
"@object-ui/fields": "5.0.1",
|
|
36
|
+
"@object-ui/i18n": "5.0.1",
|
|
37
|
+
"@object-ui/layout": "5.0.1",
|
|
38
|
+
"@object-ui/permissions": "5.0.1",
|
|
39
|
+
"@object-ui/providers": "5.0.1",
|
|
40
|
+
"@object-ui/react": "5.0.1",
|
|
41
|
+
"@object-ui/types": "5.0.1"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
44
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
45
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
46
|
"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": "^
|
|
47
|
+
"@object-ui/plugin-calendar": "^5.0.1",
|
|
48
|
+
"@object-ui/plugin-charts": "^5.0.1",
|
|
49
|
+
"@object-ui/plugin-chatbot": "^5.0.1",
|
|
50
|
+
"@object-ui/plugin-dashboard": "^5.0.1",
|
|
51
|
+
"@object-ui/plugin-designer": "^5.0.1",
|
|
52
|
+
"@object-ui/plugin-detail": "^5.0.1",
|
|
53
|
+
"@object-ui/plugin-form": "^5.0.1",
|
|
54
|
+
"@object-ui/plugin-grid": "^5.0.1",
|
|
55
|
+
"@object-ui/plugin-kanban": "^5.0.1",
|
|
56
|
+
"@object-ui/plugin-list": "^5.0.1",
|
|
57
|
+
"@object-ui/plugin-report": "^5.0.1",
|
|
58
|
+
"@object-ui/plugin-view": "^5.0.1"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@types/node": "^25.9.0",
|