@object-ui/app-shell 4.7.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +416 -0
- package/dist/console/ConsoleShell.js +2 -2
- package/dist/layout/AppHeader.js +14 -4
- package/dist/layout/AppSidebar.js +1 -0
- package/dist/layout/ConsoleLayout.js +12 -11
- package/dist/layout/MobileViewSwitcherContext.d.ts +79 -0
- package/dist/layout/MobileViewSwitcherContext.js +81 -0
- package/dist/layout/UnifiedSidebar.js +12 -43
- package/dist/utils/pageSchemaIntrospect.d.ts +20 -0
- package/dist/utils/pageSchemaIntrospect.js +51 -0
- package/dist/views/ObjectView.js +48 -28
- package/dist/views/RecordDetailView.js +197 -7
- package/package.json +24 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,421 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 5.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8930b15: feat(detail): close the gap between Page-assigned and default record detail pages (Track 1)
|
|
8
|
+
|
|
9
|
+
Custom Lightning-style record detail pages (assigned via `assignedPage` /
|
|
10
|
+
`Page` schemas) used to feel meaningfully poorer than the auto-generated
|
|
11
|
+
default detail view. They were missing cross-cutting affordances and
|
|
12
|
+
shipped with English-only tab labels and heavy bordered section cards
|
|
13
|
+
even when the host locale was Chinese. Track 1 closes the visible gap:
|
|
14
|
+
- **app-shell `RecordDetailView`**: the `assignedPage` branch now wears
|
|
15
|
+
the same chrome as the default branch — lifecycle managed-by badge
|
|
16
|
+
and presence avatars in the top-right, `MetadataPanel` debug panel,
|
|
17
|
+
`ActionConfirmDialog` / `ActionParamDialog`, and an auto-appended
|
|
18
|
+
`RecordChatterPanel` at the bottom of the page. Authors opt out of
|
|
19
|
+
the auto-discussion with `assignedPage.disableDiscussion = true`.
|
|
20
|
+
- **plugin-detail `record:details`**: defaults to `inlineEdit: true` so
|
|
21
|
+
fields are click-to-edit just like the default page, and synthesises
|
|
22
|
+
sections with `showBorder: false` by default so a Lightning page
|
|
23
|
+
doesn't double-wrap every block in a heavy Card.
|
|
24
|
+
- **components `page:tabs` / `page:accordion`**: well-known English
|
|
25
|
+
labels (Details / Related / Activity / History / Notes / Files /
|
|
26
|
+
Tasks / Events / Attachments / Chatter / Discussion / Comments /
|
|
27
|
+
Overview / Summary) auto-translate to Chinese (`zh-CN` / `zh-TW`)
|
|
28
|
+
via a built-in dictionary keyed off `document.documentElement.lang`.
|
|
29
|
+
Authors supplying explicit localised labels (string or
|
|
30
|
+
`{ default, zh-CN, ... }`) are not affected.
|
|
31
|
+
- **i18n provider**: applies the initial language to
|
|
32
|
+
`document.documentElement.lang` on mount (i18next does not fire
|
|
33
|
+
`languageChanged` for the bootstrap language), so locale-aware
|
|
34
|
+
renderers downstream see the right value from the first render.
|
|
35
|
+
|
|
36
|
+
- 186aee8: feat(detail): default-on renderViaSchema for non-assignedPage records
|
|
37
|
+
|
|
38
|
+
Track 3 Phase G slice 6. The synthesized Page schema path (slice 2,
|
|
39
|
+
behind `?renderViaSchema=1`) is now the default rendering pipeline for
|
|
40
|
+
every object without a custom assignedPage. Visual and functional
|
|
41
|
+
parity verified on task and account before flipping.
|
|
42
|
+
|
|
43
|
+
Switches preserved: `?renderViaSchema=0` URL fallback,
|
|
44
|
+
`objectDef.detail.renderViaSchema = false` per-object opt-out.
|
|
45
|
+
|
|
46
|
+
- 927187a: Phase N.1 + N.2: visual polish for record detail pages.
|
|
47
|
+
|
|
48
|
+
**N.1 — System actions on full Lightning pages.** `PageHeaderRenderer`
|
|
49
|
+
now merges `headerSystemActions` from `RecordContext` with authored
|
|
50
|
+
actions (authored wins on name/id collision), so full custom pages
|
|
51
|
+
(lead, opportunity, ...) once again show 编辑 / 分享 / 删除 alongside
|
|
52
|
+
their authored actions. `sys_share` and `sys_delete` now use the
|
|
53
|
+
`outline` variant instead of `destructive` to read better in
|
|
54
|
+
multi-button clusters.
|
|
55
|
+
|
|
56
|
+
**N.2 — Hide empty fields by default in synth detail pages.**
|
|
57
|
+
`record:details` defaults `section.hideEmpty` to `true` so synthesized
|
|
58
|
+
pages don't render label graveyards on first load. The "显示 N 个空字段"
|
|
59
|
+
reveal toggle is preserved as the user-facing escape hatch. Authors can
|
|
60
|
+
opt back into showing every field by setting `hideEmpty: false` on the
|
|
61
|
+
section schema.
|
|
62
|
+
|
|
63
|
+
- 8435860: Phase N.4b: highlight↔body dedup now works for hand-authored Lightning
|
|
64
|
+
pages too.
|
|
65
|
+
|
|
66
|
+
Adds a small `HighlightFieldsContext` registry. `record:highlights`
|
|
67
|
+
registers the field names it currently surfaces; `record:details` unions
|
|
68
|
+
that live set into its `hideFieldNames` filter so a field shown in the
|
|
69
|
+
highlight strip is never duplicated in the section grid below.
|
|
70
|
+
|
|
71
|
+
Previously the dedup only fired for synth-generated pages (via the
|
|
72
|
+
`hideFields` prop passed by `buildDefaultPageSchema`). Custom Lightning
|
|
73
|
+
pages (e.g. opportunity) showed `所属客户` both in the strip and in the
|
|
74
|
+
body. The registry-based approach covers both code paths uniformly with
|
|
75
|
+
no schema author work required.
|
|
76
|
+
|
|
77
|
+
The registry uses `useSyncExternalStore` so adding/removing highlights
|
|
78
|
+
notifies consumers without triggering the provider value identity to
|
|
79
|
+
change — avoiding the update-loop that a naive context implementation
|
|
80
|
+
would cause.
|
|
81
|
+
|
|
82
|
+
`RecordDetailView` mounts `<HighlightFieldsProvider>` once per record
|
|
83
|
+
page so the two renderers share state.
|
|
84
|
+
|
|
85
|
+
- 74962b0: feat(detail): record:discussion schema component + flush accordion variant
|
|
86
|
+
- New `record:discussion` schema type lets authors place the record
|
|
87
|
+
chatter feed anywhere in a custom Page schema. Wired through a
|
|
88
|
+
shared `DiscussionContext` provider on the `assignedPage` branch
|
|
89
|
+
of `RecordDetailView`; auto-append still applies when no explicit
|
|
90
|
+
`record:discussion` / `record:chatter` node is present.
|
|
91
|
+
- `page:accordion` gains a `variant` prop. Default `flush` strips the
|
|
92
|
+
per-item border so accordion sections no longer double-wrap inner
|
|
93
|
+
Card-bearing renderers (RelatedList, etc.). Authors who want the
|
|
94
|
+
old visual pass `variant: 'card'`.
|
|
95
|
+
- `translateLabel` now handles compound labels split by `&`, `and`,
|
|
96
|
+
or `和` (e.g. `Notes & Attachments` → `备注与附件`).
|
|
97
|
+
|
|
98
|
+
- fa4c2cb: feat(detail): renderViaSchema opt-in routes default detail through SchemaRenderer (Track 3 Phase G slice 2)
|
|
99
|
+
|
|
100
|
+
When `?renderViaSchema=1` is in the URL, or `objectDef.detail.renderViaSchema === true`,
|
|
101
|
+
`RecordDetailView`'s no-assignedPage branch now synthesizes a canonical
|
|
102
|
+
Page schema (`page:header` → `record:highlights` → `record:path` →
|
|
103
|
+
`page:tabs(record:details)` → `record:discussion`) via
|
|
104
|
+
`buildDefaultPageSchema(objectDef, { sections, highlightFields })` and
|
|
105
|
+
renders it through the existing `<SchemaRenderer>` pipeline.
|
|
106
|
+
|
|
107
|
+
This means every object without a custom assigned page can opt in to
|
|
108
|
+
the same chrome (record-aware header chip, chevron path, flush
|
|
109
|
+
accordion, discussion slot) that custom Lightning pages already enjoy.
|
|
110
|
+
|
|
111
|
+
Changes:
|
|
112
|
+
- `buildDefaultPageSchema` now emits `page:tabs.items` (correct shape
|
|
113
|
+
for the renderer) rather than `tabs`.
|
|
114
|
+
- `PageHeaderRenderer.resolvedTitle` honors `objectSchema.primaryField`
|
|
115
|
+
before the legacy `name/title/display_name/label` fallbacks.
|
|
116
|
+
- `RecordDetailView` rebuilds the synthesized schema with
|
|
117
|
+
`detailSchema.sections` + `highlightFields` at render time so
|
|
118
|
+
`record:details` inherits the same field layout the legacy
|
|
119
|
+
`<DetailView>` would have produced.
|
|
120
|
+
|
|
121
|
+
Flag is intentionally off by default — flipping the default is a
|
|
122
|
+
separate explicit commit after empirical parity validation across
|
|
123
|
+
multiple objects. Known gaps tracked for slice 3: titleFormat
|
|
124
|
+
fallback for objects without `primaryField`, auto Activity / History
|
|
125
|
+
tabs, header-action buttons.
|
|
126
|
+
|
|
127
|
+
- 7213027: feat(detail): slotted record pages (Track 3 Phase I)
|
|
128
|
+
|
|
129
|
+
Introduce `kind: "slotted"` record pages that override one or more
|
|
130
|
+
named slots while letting the default-page synthesizer fill in the
|
|
131
|
+
rest. Authors no longer need to re-author the entire page just to
|
|
132
|
+
customize the header or one tab.
|
|
133
|
+
|
|
134
|
+
**Slot menu (v1):**
|
|
135
|
+
- `header` — replaces `page:header`
|
|
136
|
+
- `actions` — replaces the `record:quick_actions` action bar
|
|
137
|
+
- `highlights` — replaces the chips + chevron path strip
|
|
138
|
+
- `details` — replaces the Details tab body (other tabs stay synthesized)
|
|
139
|
+
- `tabs` — replaces the entire `page:tabs` node (wins over `details`)
|
|
140
|
+
- `discussion` — replaces the inline `record:discussion` footer
|
|
141
|
+
|
|
142
|
+
Each slot is a full replacement at the slot boundary. To compose
|
|
143
|
+
default + custom, call the corresponding `buildDefault*` sub-builder
|
|
144
|
+
(now exported from `@object-ui/plugin-detail`):
|
|
145
|
+
`buildDefaultHeader`, `buildDefaultActions`, `buildDefaultHighlights`,
|
|
146
|
+
`buildDefaultDetails`, `buildDefaultTabs`, `buildDefaultDiscussion`.
|
|
147
|
+
|
|
148
|
+
**Author shape:**
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
{
|
|
152
|
+
type: 'record',
|
|
153
|
+
object: 'account',
|
|
154
|
+
kind: 'slotted',
|
|
155
|
+
slots: {
|
|
156
|
+
header: { type: 'page:header', properties: { ... } },
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**API changes:**
|
|
162
|
+
- `PageSchema` (in `@object-ui/types`): adds `kind?: 'full' | 'slotted'`
|
|
163
|
+
(default `'full'`) and `slots?: PageSlotMap`.
|
|
164
|
+
- `usePageAssignment` (in `@object-ui/react`): result now exposes a
|
|
165
|
+
`slots` field populated when the matched page has `kind === 'slotted'`.
|
|
166
|
+
Existing `page` field is unchanged for full pages.
|
|
167
|
+
- `buildDefaultPageSchema` (in `@object-ui/plugin-detail`): accepts an
|
|
168
|
+
`options.slots` map that overrides individual regions at synthesis time.
|
|
169
|
+
|
|
170
|
+
- 34b66bf: feat(detail): synthesize Related / Activity / History tabs + record:quick_actions header (Track 3 Phase G slice 4)
|
|
171
|
+
- `buildDefaultPageSchema` now accepts `headerActions`, `related`,
|
|
172
|
+
`showActivity`, and `history` options. When provided, the synthesizer
|
|
173
|
+
emits a `record:quick_actions` node after `page:header` and appends
|
|
174
|
+
the corresponding tabs to `page:tabs.items` in stable order
|
|
175
|
+
(Details / Related / Activity / History).
|
|
176
|
+
- New `record:history` renderer wraps the existing `HistoryTimeline`,
|
|
177
|
+
reading `entries` / `loading` from the schema. Host owns fetching.
|
|
178
|
+
- `RecordDetailView` forwards `detailSchema.actions[0].actions`,
|
|
179
|
+
`detailSchema.related[]` (unwrapped to `{objectName,relationshipField}`),
|
|
180
|
+
and `detailSchema.history` into the synthesizer call so the
|
|
181
|
+
`renderViaSchema` path reaches parity with the monolithic DetailView
|
|
182
|
+
tab strip and header action bar.
|
|
183
|
+
- 6 new unit tests covering headerActions emit/skip, Related tab
|
|
184
|
+
shape, Activity opt-in, History entries pass-through, and stable
|
|
185
|
+
tab ordering.
|
|
186
|
+
|
|
187
|
+
No behavior change for objects without the `renderViaSchema` opt-in.
|
|
188
|
+
|
|
189
|
+
- c7561a7: **Unify per-user UI state storage onto `sys_user_preference`.**
|
|
190
|
+
|
|
191
|
+
`createObjectStackUserStateAdapter` previously wrote to a bespoke
|
|
192
|
+
`user_app_state` object using `(user_id, kind, payload)` columns. That
|
|
193
|
+
parallel KV table duplicated the canonical per-user preference store
|
|
194
|
+
shipped by `@objectstack/plugin-auth`, and pulled UI traces (favorites,
|
|
195
|
+
recent items, grid widths) out of the place users actually look for
|
|
196
|
+
their settings.
|
|
197
|
+
|
|
198
|
+
The adapter now defaults to:
|
|
199
|
+
- `resource`: `sys_user_preference`
|
|
200
|
+
- field shape: `(user_id, key, value)` instead of `(user_id, kind, payload)`
|
|
201
|
+
- option name: **`key`** instead of `kind`
|
|
202
|
+
|
|
203
|
+
`ConsoleShell` is updated to attach favorites/recent under the namespaced
|
|
204
|
+
keys `ui.favorites` and `ui.recent`. Recommended convention for new
|
|
205
|
+
adapters: keep machine-written UI traces under `ui.*` so they stay
|
|
206
|
+
distinguishable from user-facing preferences (`theme`, `locale`, ...).
|
|
207
|
+
|
|
208
|
+
**Migration**: callers passing `kind:` need to switch to `key:`. Callers
|
|
209
|
+
relying on the old `user_app_state` table can pin
|
|
210
|
+
`resource: 'user_app_state'` to keep the legacy behaviour, but no
|
|
211
|
+
backend ships that schema and the new default works against any
|
|
212
|
+
plugin-auth-enabled environment with zero extra setup.
|
|
213
|
+
|
|
214
|
+
### Patch Changes
|
|
215
|
+
|
|
216
|
+
- 983d5ad: fix(app-shell): suppress duplicate discussion panel on record detail pages
|
|
217
|
+
|
|
218
|
+
`RecordDetailView` auto-appends a `RecordChatterPanel` below the
|
|
219
|
+
rendered page unless an explicit `record:discussion` / `record:chatter`
|
|
220
|
+
node is found in the schema. The detection walker recursed into
|
|
221
|
+
`children / items / body / components / properties.*` but **not**
|
|
222
|
+
`regions[]`. Synthesised pages (`buildDefaultPageSchema`) and authored
|
|
223
|
+
full-Lightning pages place `record:discussion` inside
|
|
224
|
+
`regions[0].components`, so the walker missed it and a second
|
|
225
|
+
discussion panel rendered on top of the first.
|
|
226
|
+
|
|
227
|
+
Extracted the walker into `utils/pageSchemaIntrospect.ts`, added a
|
|
228
|
+
`regions` branch, and covered both shapes with unit tests.
|
|
229
|
+
|
|
230
|
+
Verified in browser on account (slotted), opportunity (full), lead,
|
|
231
|
+
contact, and task — each renders exactly one discussion panel.
|
|
232
|
+
|
|
233
|
+
- a4c10b2: Restore Edit / Share / Delete system actions on synthesized record detail headers.
|
|
234
|
+
|
|
235
|
+
Phase G slice 6 flipped the synth detail page on by default but did not
|
|
236
|
+
forward the legacy DetailView's built-in system actions to the new
|
|
237
|
+
`record:quick_actions` bar. Objects without authored `record_header`
|
|
238
|
+
business actions ended up with a bare header (only the ★ favorite +
|
|
239
|
+
copy-id chip from `page:header`).
|
|
240
|
+
|
|
241
|
+
This patch injects gated system actions into `synthHeaderActions` for
|
|
242
|
+
both the synth and slotted paths:
|
|
243
|
+
- `sys_edit` — visible when `affordances.edit`. Calls the existing
|
|
244
|
+
`onEdit` prop, opening the same form modal as before.
|
|
245
|
+
- `sys_share` — always visible. Uses `navigator.share` when available;
|
|
246
|
+
falls back to clipboard copy of the current URL with a toast.
|
|
247
|
+
- `sys_delete` — visible when `affordances.delete`. Confirms via
|
|
248
|
+
`window.confirm`, calls `dataSource.delete`, then navigates back to
|
|
249
|
+
the list.
|
|
250
|
+
|
|
251
|
+
Business / custom actions (e.g. Lead.convert, Contact.set_primary)
|
|
252
|
+
continue to render alongside the system actions, unchanged. Full
|
|
253
|
+
Lightning pages (objects with an `assignedPage`) are unaffected — they
|
|
254
|
+
remain author-owned.
|
|
255
|
+
|
|
256
|
+
- Updated dependencies [542cca9]
|
|
257
|
+
- Updated dependencies [8930b15]
|
|
258
|
+
- Updated dependencies [95b6b21]
|
|
259
|
+
- Updated dependencies [ddb08a7]
|
|
260
|
+
- Updated dependencies [f16a762]
|
|
261
|
+
- Updated dependencies [765d50f]
|
|
262
|
+
- Updated dependencies [927187a]
|
|
263
|
+
- Updated dependencies [bae8ba8]
|
|
264
|
+
- Updated dependencies [8435860]
|
|
265
|
+
- Updated dependencies [bece8ca]
|
|
266
|
+
- Updated dependencies [bb2ea48]
|
|
267
|
+
- Updated dependencies [77c1877]
|
|
268
|
+
- Updated dependencies [b14fe09]
|
|
269
|
+
- Updated dependencies [1911d34]
|
|
270
|
+
- Updated dependencies [ba98039]
|
|
271
|
+
- Updated dependencies [a7bef6e]
|
|
272
|
+
- Updated dependencies [86c04f1]
|
|
273
|
+
- Updated dependencies [74962b0]
|
|
274
|
+
- Updated dependencies [8b850b5]
|
|
275
|
+
- Updated dependencies [3154334]
|
|
276
|
+
- Updated dependencies [fa4c2cb]
|
|
277
|
+
- Updated dependencies [7213027]
|
|
278
|
+
- Updated dependencies [34b66bf]
|
|
279
|
+
- Updated dependencies [c7561a7]
|
|
280
|
+
- @object-ui/plugin-detail@5.0.0
|
|
281
|
+
- @object-ui/components@5.0.0
|
|
282
|
+
- @object-ui/i18n@5.0.0
|
|
283
|
+
- @object-ui/layout@5.0.0
|
|
284
|
+
- @object-ui/react@5.0.0
|
|
285
|
+
- @object-ui/types@5.0.0
|
|
286
|
+
- @object-ui/data-objectstack@5.0.0
|
|
287
|
+
- @object-ui/plugin-calendar@5.0.0
|
|
288
|
+
- @object-ui/plugin-kanban@5.0.0
|
|
289
|
+
- @object-ui/fields@5.0.0
|
|
290
|
+
- @object-ui/plugin-charts@5.0.0
|
|
291
|
+
- @object-ui/plugin-chatbot@5.0.0
|
|
292
|
+
- @object-ui/plugin-dashboard@5.0.0
|
|
293
|
+
- @object-ui/plugin-designer@5.0.0
|
|
294
|
+
- @object-ui/plugin-form@5.0.0
|
|
295
|
+
- @object-ui/plugin-grid@5.0.0
|
|
296
|
+
- @object-ui/plugin-list@5.0.0
|
|
297
|
+
- @object-ui/plugin-report@5.0.0
|
|
298
|
+
- @object-ui/plugin-view@5.0.0
|
|
299
|
+
- @object-ui/auth@5.0.0
|
|
300
|
+
- @object-ui/collaboration@5.0.0
|
|
301
|
+
- @object-ui/core@5.0.0
|
|
302
|
+
- @object-ui/permissions@5.0.0
|
|
303
|
+
|
|
304
|
+
## 4.8.0
|
|
305
|
+
|
|
306
|
+
### Minor Changes
|
|
307
|
+
|
|
308
|
+
- 3a17c8d: Mobile UI: aggressive chrome reduction to match real mobile-app conventions.
|
|
309
|
+
|
|
310
|
+
Real mobile CRMs (Salesforce, HubSpot, Notion, Linear) keep one row of
|
|
311
|
+
chrome on phones: title + 1 primary action, plus content. We were
|
|
312
|
+
shipping ~5 rows of toolbars + chips + tabs above the data. This commit
|
|
313
|
+
hides the desktop-only chrome at the `<sm` breakpoint:
|
|
314
|
+
- **ListView**: TabBar (view switcher), UserFilters chip row, quick-filters
|
|
315
|
+
chip row, Sort button, list-scoped Search popover, and the
|
|
316
|
+
(newly-added) mobile-only ViewSettingsPopover gear are all hidden on
|
|
317
|
+
phones. Only the **Filter** icon survives on mobile — paired with the
|
|
318
|
+
global ⌘K top-bar search, that is the entire mobile control surface.
|
|
319
|
+
- **Kanban**: previous commit replaced verbose swipe text with a dot
|
|
320
|
+
indicator; that stands.
|
|
321
|
+
- **ObjectView page header**: the Import (CSV upload) button is hidden
|
|
322
|
+
on mobile — CSV import is a desktop workflow.
|
|
323
|
+
|
|
324
|
+
Net effect on a 390px viewport: ListView toolbar collapses from
|
|
325
|
+
~10 controls (5 chips + 5 icons) to a single Filter icon next to the
|
|
326
|
+
title; the body of the page is reachable without scrolling past 3 rows
|
|
327
|
+
of chrome.
|
|
328
|
+
|
|
329
|
+
Desktop and tablet behavior is unchanged.
|
|
330
|
+
|
|
331
|
+
- 51e274a: feat(app-shell,plugin-list): mobile Airtable-style topbar + filter chip row
|
|
332
|
+
|
|
333
|
+
Refactor mobile object-view layout to match the Airtable Interface
|
|
334
|
+
pattern:
|
|
335
|
+
- **AppHeader**: the mobile topbar's static page label is now a
|
|
336
|
+
view-switcher dropdown (`<viewName> ▾`). Tapping opens a list of
|
|
337
|
+
available views with icons + active-state checkmark. Falls back to
|
|
338
|
+
plain text when only one view exists, or when the current page has
|
|
339
|
+
no view-switching surface (Home, Settings, …).
|
|
340
|
+
- **ObjectView**: drops the standalone mobile `sm:hidden` view-select
|
|
341
|
+
row that previously lived between the desktop tab bar and the
|
|
342
|
+
content area. View switching is now exposed exclusively via the
|
|
343
|
+
topbar dropdown on mobile, eliminating the duplicated `object name`
|
|
344
|
+
vs `view name` rows.
|
|
345
|
+
- **ListView**: un-hides the `UserFilters` chip row on mobile.
|
|
346
|
+
Single-line, horizontally scrollable, matches the Airtable Interface
|
|
347
|
+
filter chip strip.
|
|
348
|
+
- New lightweight `MobileViewSwitcherContext` provides a
|
|
349
|
+
page → header data channel (no zustand dependency added).
|
|
350
|
+
|
|
351
|
+
Net effect on mobile (390×844):
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
☰ 客户卡片 ▾ 🔍 🔔 M ← topbar
|
|
355
|
+
类型 ▾ 行业 ▾ 是否活跃 ▾ 更多 3 ▾ ⛛ ← chip row
|
|
356
|
+
[content cards] ← content
|
|
357
|
+
(+) ← FAB
|
|
358
|
+
[Leads | Accounts | Contacts | …] ← bottom nav
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
- 7feed12: Mobile UX: Home affordance + chrome reduction
|
|
362
|
+
|
|
363
|
+
Two fixes that match what users actually need on a 390px viewport:
|
|
364
|
+
- **Add Home link to mobile sidebar.** When inside an app, the sidebar
|
|
365
|
+
drawer previously listed only the current app's nav groups, with no
|
|
366
|
+
way back to the home page (the desktop topbar's logo and AppSwitcher
|
|
367
|
+
pill are hidden on phones). Now the mobile sidebar opens with a
|
|
368
|
+
prominent "Home" row (`/home`) at the top, gated to mobile + app
|
|
369
|
+
context so the desktop layout is untouched.
|
|
370
|
+
- **Cut a row of top chrome.** The list/object PageHeader (icon + title
|
|
371
|
+
- create / import / more actions) duplicated the page title already
|
|
372
|
+
shown in the topbar. On mobile it's hidden entirely; the primary
|
|
373
|
+
create action moves to a floating "+" button anchored above the
|
|
374
|
+
bottom nav. Desktop still renders the full PageHeader.
|
|
375
|
+
|
|
376
|
+
- 00363fd: feat(app-shell): remove mobile bottom-tab navigation
|
|
377
|
+
|
|
378
|
+
The mobile bottom-tab strip was rendering the first 5 leaf items of
|
|
379
|
+
the app's navigation tree — exactly the same items that the drawer
|
|
380
|
+
(`☰`) surfaces, just without grouping, favourites, or recents.
|
|
381
|
+
|
|
382
|
+
Per the Notion / Linear mobile convention, we now rely on the drawer
|
|
383
|
+
alone. Bottom-tab strips work when they expose **orthogonal**
|
|
384
|
+
top-level sections (Airtable's Home / Bases / Notifications / Account)
|
|
385
|
+
— but ours was a duplicate of the drawer, so it was pure visual
|
|
386
|
+
weight: ~52px of vertical real estate, redundant taps, and clashes
|
|
387
|
+
with the FAB and chat-bubble stack at the bottom-right corner.
|
|
388
|
+
|
|
389
|
+
Net effect:
|
|
390
|
+
- Drawer remains the single source of in-app navigation.
|
|
391
|
+
- ~52px reclaimed for list/kanban content on every mobile screen.
|
|
392
|
+
- FAB and chat-bubble keep their existing offsets (no overlap;
|
|
393
|
+
bottom-nav was already accounted for above them).
|
|
394
|
+
|
|
395
|
+
- faba0e3: Mobile UX cleanup:
|
|
396
|
+
- `app-shell/AppHeader`: hide the platform-logo, app-switcher pill, and
|
|
397
|
+
intermediate path separators on mobile when inside an app route. The
|
|
398
|
+
sidebar already exposes those affordances; the topbar now reads
|
|
399
|
+
`☰ + page title + Search + Inbox + Avatar`.
|
|
400
|
+
- `plugin-list`: replace the hidden mobile TabBar with a new compact
|
|
401
|
+
`TabBarSelect` dropdown (current view name + chevron → menu of every
|
|
402
|
+
view). Phone users keep view-switching without burning a row on chip
|
|
403
|
+
pills. Desktop continues to render the inline TabBar.
|
|
404
|
+
|
|
405
|
+
### Patch Changes
|
|
406
|
+
|
|
407
|
+
- @object-ui/types@4.8.0
|
|
408
|
+
- @object-ui/core@4.8.0
|
|
409
|
+
- @object-ui/i18n@4.8.0
|
|
410
|
+
- @object-ui/react@4.8.0
|
|
411
|
+
- @object-ui/components@4.8.0
|
|
412
|
+
- @object-ui/fields@4.8.0
|
|
413
|
+
- @object-ui/layout@4.8.0
|
|
414
|
+
- @object-ui/data-objectstack@4.8.0
|
|
415
|
+
- @object-ui/auth@4.8.0
|
|
416
|
+
- @object-ui/permissions@4.8.0
|
|
417
|
+
- @object-ui/collaboration@4.8.0
|
|
418
|
+
|
|
3
419
|
## 4.7.0
|
|
4
420
|
|
|
5
421
|
### Patch Changes
|
|
@@ -80,12 +80,12 @@ function UserStateBridge() {
|
|
|
80
80
|
const favorites = createObjectStackUserStateAdapter({
|
|
81
81
|
dataSource,
|
|
82
82
|
userId: user.id,
|
|
83
|
-
|
|
83
|
+
key: 'ui.favorites',
|
|
84
84
|
});
|
|
85
85
|
const recent = createObjectStackUserStateAdapter({
|
|
86
86
|
dataSource,
|
|
87
87
|
userId: user.id,
|
|
88
|
-
|
|
88
|
+
key: 'ui.recent',
|
|
89
89
|
});
|
|
90
90
|
attach('favorites', favorites);
|
|
91
91
|
attach('recent', recent);
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -18,8 +18,8 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
18
18
|
* @module
|
|
19
19
|
*/
|
|
20
20
|
import { useLocation, useParams, Link, useNavigate } from 'react-router-dom';
|
|
21
|
-
import { SidebarTrigger, Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, } from '@object-ui/components';
|
|
22
|
-
import { Search, HelpCircle, ChevronDown, Settings, LogOut, User as UserIcon, Boxes, } from 'lucide-react';
|
|
21
|
+
import { SidebarTrigger, Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, cn, } from '@object-ui/components';
|
|
22
|
+
import { Search, HelpCircle, ChevronDown, Check, Lock, Settings, LogOut, User as UserIcon, Boxes, } from 'lucide-react';
|
|
23
23
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
24
24
|
import { useOffline } from '@object-ui/react';
|
|
25
25
|
import { PresenceAvatars } from '@object-ui/collaboration';
|
|
@@ -32,6 +32,7 @@ import { useAdapter } from '../providers/AdapterProvider';
|
|
|
32
32
|
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
33
33
|
import { useAuth, getUserInitials } from '@object-ui/auth';
|
|
34
34
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
35
|
+
import { useMobileViewSwitcher } from './MobileViewSwitcherContext';
|
|
35
36
|
import { useNavigationContext } from '../context/NavigationContext';
|
|
36
37
|
function humanizeSlug(slug) {
|
|
37
38
|
return slug.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
@@ -56,6 +57,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
56
57
|
const { objectLabel, dashboardLabel, pageLabel, reportLabel, viewLabel } = useObjectLabel();
|
|
57
58
|
const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
|
|
58
59
|
const { currentAppName, recordTitle } = useNavigationContext();
|
|
60
|
+
const mobileSwitcher = useMobileViewSwitcher();
|
|
59
61
|
const [apiPresenceUsers, setApiPresenceUsers] = useState(null);
|
|
60
62
|
const [apiActivities, setApiActivities] = useState(null);
|
|
61
63
|
/** M10.8: in-header notifications. Polled from sys_notification scoped to current user. */
|
|
@@ -372,8 +374,16 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
372
374
|
}
|
|
373
375
|
}
|
|
374
376
|
const lastSegmentLabel = extraSegments[extraSegments.length - 1]?.label || appName || '';
|
|
375
|
-
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: "flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", 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(PathSep, {}), _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
|
|
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) => {
|
|
376
378
|
const isLast = i === extraSegments.length - 1;
|
|
377
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));
|
|
378
|
-
}),
|
|
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
|
+
mobileSwitcher.views.find((v) => v.id === mobileSwitcher.activeViewId)?.label ??
|
|
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) => {
|
|
383
|
+
const isActive = v.id === mobileSwitcher.activeViewId;
|
|
384
|
+
return (_jsxs(DropdownMenuItem, { onSelect: () => {
|
|
385
|
+
if (!isActive)
|
|
386
|
+
mobileSwitcher.onChange(v.id);
|
|
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...' }) }), _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' })] })] }))] })] })] })] })] }));
|
|
379
389
|
}
|
|
@@ -193,6 +193,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
|
|
|
193
193
|
{ id: 'sys-users', label: 'Users', type: 'url', url: '/apps/setup/system/users', icon: 'users' },
|
|
194
194
|
{ id: 'sys-orgs', label: 'Organizations', type: 'url', url: '/apps/setup/system/organizations', icon: 'building-2' },
|
|
195
195
|
{ id: 'sys-roles', label: 'Roles', type: 'url', url: '/apps/setup/system/roles', icon: 'shield' },
|
|
196
|
+
{ id: 'sys-config', label: 'Configuration', type: 'url', url: '/apps/setup/system/settings', icon: 'sliders-horizontal' },
|
|
196
197
|
{ id: 'sys-create-app', label: 'Create App', type: 'url', url: '/create-app', icon: 'plus' },
|
|
197
198
|
], []);
|
|
198
199
|
return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: activeApp ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", style: primaryColor ? { backgroundColor: primaryColor } : undefined, children: logo ? (_jsx("img", { src: logo, alt: resolveI18nLabel(activeApp.label, t), className: "size-6 object-contain" })) : (React.createElement(getIcon(activeApp.icon), { className: "size-4" })) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: resolveI18nLabel(activeApp.label, t) }), _jsx("span", { className: "truncate text-xs", children: resolveI18nLabel(activeApp.description, t) || `${activeApps.length} Apps Available` })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "Switch Application" }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(app.name), className: "gap-2 p-2", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-sm border", children: app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : _jsx(Database, { className: "size-3" }) }), resolveI18nLabel(app.label, t), activeApp.name === app.name && _jsx("span", { className: "ml-auto text-xs", children: "\u2713" })] }, app.name))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/home'), "data-testid": "home-link-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Home, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.home') })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/create-app`), "data-testid": "add-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Plus, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.addApp') })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/edit-app/${activeAppName}`), "data-testid": "edit-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Pencil, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.editApp') })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/apps/setup/system/apps'), "data-testid": "manage-all-apps-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Settings, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.manageAllApps') })] })] })] })) : (_jsxs(SidebarMenuButton, { size: "lg", onClick: () => navigate('/apps/setup'), "data-testid": "system-sidebar-header", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: _jsx(Settings, { className: "size-4" }) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: t('layout.appSwitcher.systemConsole') }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: t('layout.appSwitcher.noAppsConfigured') })] })] })) }) }) }), _jsx(SidebarContent, { children: activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Area"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
|
|
@@ -16,6 +16,7 @@ import { useDiscovery } from '@object-ui/react';
|
|
|
16
16
|
const ConsoleFloatingChatbot = lazy(() => import('./ConsoleFloatingChatbot'));
|
|
17
17
|
import { UnifiedSidebar } from './UnifiedSidebar';
|
|
18
18
|
import { AppHeader } from './AppHeader';
|
|
19
|
+
import { MobileViewSwitcherProvider } from './MobileViewSwitcherContext';
|
|
19
20
|
import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
|
|
20
21
|
import { useNavigationContext } from '../context/NavigationContext';
|
|
21
22
|
import { resolveI18nLabel } from '../utils';
|
|
@@ -39,15 +40,15 @@ export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange,
|
|
|
39
40
|
setContext('app');
|
|
40
41
|
setCurrentAppName(activeAppName);
|
|
41
42
|
}, [setContext, setCurrentAppName, activeAppName]);
|
|
42
|
-
return (_jsxs(AppShell, { sidebar: _jsx(UnifiedSidebar, { activeAppName: activeAppName, onAppChange: onAppChange }), navbar: _jsx(AppHeader, { variant: "app", appName: appLabel, objects: objects, connectionState: connectionState, activeAppName: activeAppName, onAppChange: onAppChange }), className: "!p-0 overflow-y-auto overflow-x-hidden bg-muted/5", branding: activeApp?.branding
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
return (_jsx(MobileViewSwitcherProvider, { children: _jsxs(AppShell, { sidebar: _jsx(UnifiedSidebar, { activeAppName: activeAppName, onAppChange: onAppChange }), navbar: _jsx(AppHeader, { variant: "app", appName: appLabel, objects: objects, connectionState: connectionState, activeAppName: activeAppName, onAppChange: onAppChange }), className: "!p-0 overflow-y-auto overflow-x-hidden bg-muted/5", branding: activeApp?.branding
|
|
44
|
+
? {
|
|
45
|
+
primaryColor: activeApp.branding.primaryColor,
|
|
46
|
+
accentColor: activeApp.branding.accentColor,
|
|
47
|
+
favicon: activeApp.branding.favicon,
|
|
48
|
+
logo: activeApp.branding.logo,
|
|
49
|
+
title: activeApp.label
|
|
50
|
+
? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
|
|
51
|
+
: undefined,
|
|
52
|
+
}
|
|
53
|
+
: undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects }) }))] }) }));
|
|
53
54
|
}
|