@object-ui/app-shell 11.4.0 → 11.5.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 +178 -0
- package/README.md +9 -0
- package/dist/chrome/KeyboardShortcutsDialog.js +2 -1
- package/dist/console/AppContent.js +145 -26
- package/dist/console/ConsoleShell.js +8 -1
- package/dist/context/CommandPaletteProvider.js +2 -1
- package/dist/hooks/useObjectActions.js +16 -4
- package/dist/layout/AppHeader.js +13 -5
- package/dist/layout/AppSidebar.js +10 -4
- package/dist/observability/sentry.d.ts +5 -0
- package/dist/observability/sentry.js +6 -1
- package/dist/preview/DraftChangesPanel.d.ts +29 -1
- package/dist/preview/DraftChangesPanel.js +141 -14
- package/dist/urlParams.d.ts +68 -0
- package/dist/urlParams.js +76 -0
- package/dist/utils/appRoute.d.ts +15 -0
- package/dist/utils/appRoute.js +22 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/pageTabsUrlSync.d.ts +32 -0
- package/dist/utils/pageTabsUrlSync.js +43 -0
- package/dist/utils/recordFormNavigation.d.ts +40 -0
- package/dist/utils/recordFormNavigation.js +30 -0
- package/dist/views/InterfaceListPage.d.ts +1 -0
- package/dist/views/InterfaceListPage.js +1 -1
- package/dist/views/ObjectDataPage.d.ts +29 -0
- package/dist/views/ObjectDataPage.js +227 -0
- package/dist/views/ObjectView.js +4 -3
- package/dist/views/RecordDetailView.js +61 -20
- package/dist/views/RelatedRecordActionsBridge.d.ts +10 -1
- package/dist/views/RelatedRecordActionsBridge.js +49 -16
- package/dist/views/metadata-admin/ResourceEditPage.js +39 -0
- package/dist/views/metadata-admin/i18n.js +214 -4
- package/dist/views/metadata-admin/inspectors/AppNavInspector.d.ts +11 -4
- package/dist/views/metadata-admin/inspectors/AppNavInspector.js +141 -7
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.d.ts +14 -0
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.js +76 -5
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +35 -19
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +8 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +3 -2
- package/dist/views/metadata-admin/inspectors/nav-target.d.ts +52 -0
- package/dist/views/metadata-admin/inspectors/nav-target.js +149 -0
- package/dist/views/metadata-admin/nav-selection.d.ts +20 -0
- package/dist/views/metadata-admin/nav-selection.js +81 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.js +9 -1
- package/dist/views/metadata-admin/previews/AppPreview.js +4 -2
- package/dist/views/studio-design/BuilderLanding.d.ts +1 -1
- package/dist/views/studio-design/BuilderLanding.js +12 -19
- package/dist/views/studio-design/ObjectFormDesigner.d.ts +5 -3
- package/dist/views/studio-design/ObjectFormDesigner.js +17 -12
- package/dist/views/studio-design/ObjectSettingsPanel.d.ts +1 -1
- package/dist/views/studio-design/ObjectSettingsPanel.js +4 -3
- package/dist/views/studio-design/ObjectValidationsPanel.js +6 -4
- package/dist/views/studio-design/PackageIdInput.d.ts +31 -0
- package/dist/views/studio-design/PackageIdInput.js +40 -0
- package/dist/views/studio-design/StudioDesignSurface.d.ts +13 -0
- package/dist/views/studio-design/StudioDesignSurface.js +227 -57
- package/dist/views/studio-design/packageSurfaces.d.ts +49 -0
- package/dist/views/studio-design/packageSurfaces.js +34 -0
- package/dist/views/studio-design/packages-io.d.ts +11 -0
- package/dist/views/studio-design/packages-io.js +12 -0
- package/dist/views/studio-design/skeletons.d.ts +16 -0
- package/dist/views/studio-design/skeletons.js +51 -0
- package/package.json +38 -38
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,183 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 11.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 544d8eb: Add the app → Studio reverse bridge (ADR-0080): workspace admins see a "Design in Studio" entry in the app top bar that deep-links to the running app's owning package on the Studio design surface (`/studio/:packageId/data`). Hidden for non-admins and for apps with no owning package; package writability stays server-side (read-only packages open as browse-only).
|
|
8
|
+
- 6fffd3d: Client-side data-invalidation bus — refresh data, don't rebuild UI (objectui#2269 P1).
|
|
9
|
+
|
|
10
|
+
- `@object-ui/react` gains the bus: `notifyDataChanged({objectName, recordId?})`, `useDataInvalidation(objectName, recordId?)` (reader nonce), `subscribeDataChanges`, and `useMutationInvalidationBridge(dataSource)` which fans every dataSource write (`MutationEvent`) onto the bus. The bus also dispatches the legacy `objectui:related-changed` window event, so pre-bus listeners keep working.
|
|
11
|
+
- The `key={refreshKey}` remount of `RecordDetailView` (AppContent) and the `key={actionRefreshKey}` remount of `DetailView` (RecordDetailView) are GONE: record data now refetches in place via the bus — scroll, collapsed sections, tabs and in-progress inline edits survive every save/action/undo. All nine action-success bumps became precisely-scoped `notifyDataChanged` calls; undo/redo use the operation's own `objectName`/`recordId`.
|
|
12
|
+
- `RelatedCountStore` is wired to the bus (tab count badges refetch after any change to their object) and its `useSyncExternalStore` snapshot is now a monotonic version — previously it returned the same `Map` reference, so `emit()` never re-rendered subscribers and invalidations left badges stale; `useRelatedCountVersion()` is exported and drives the probe effect's re-fetch.
|
|
13
|
+
- app-shell also gains the reserved URL-param registry (`urlParams.ts` — `form`/`formObject`/`formLink`/`tab`/`recordId`/`palette`/`shortcuts` constants replace scattered string literals) and AGENTS.md Commandment #8 (UI-state classification: state that must survive a data refresh may never live only in an uncontrolled component).
|
|
14
|
+
|
|
15
|
+
- 9255686: Record detail tabs are URL-addressable (`?tab=`) and survive subtree remounts (objectui#2257, ADR-0054 C3).
|
|
16
|
+
|
|
17
|
+
- `buildDefaultTabs` emits STABLE semantic tab values (`details` / `related:<child>` / `related` / `activity` / `history`) instead of leaving the renderer to synthesize index-derived ones.
|
|
18
|
+
- `PageTabsRenderer` honors `item.value`, a host-provided `schema.defaultTab` (validated against actual tabs) and `schema.onTabChange`; index fallback kept for authored schemas without values.
|
|
19
|
+
- app-shell `RecordDetailView` restores the active tab from `?tab=` and writes it back with `replace` (tab switches never stack history), via the pure `withPageTabsUrlSync` page-tree injector (never mutates authored/memoized page schemas). Legacy `DetailView.autoTabs` wired to the same contract (`defaultTab`/`onTabChange`).
|
|
20
|
+
- Fixes the tab strip resetting to Details after save-refresh remounts (`refreshKey`-style) and dev-StrictMode URL churn; enables `?tab=` deep links; invalid values fall back to Details.
|
|
21
|
+
|
|
22
|
+
- 6c1ad9e: Record task flows open as derived overlays with lossless return (framework#2604, extends framework#2578).
|
|
23
|
+
|
|
24
|
+
- **Create/Edit never route** — the global record form is URL-driven (`?form=new` / `?form=<id>`): browser Back closes the overlay with the origin (list scroll/filters, detail state) intact; field-heavy objects derive a full-screen modal (`modalSize:'full'`) via the new `deriveRecordFlowSurface` mirror in plugin-view, light ones keep the auto-sized modal. `editMode:'page'` opt-in unchanged.
|
|
25
|
+
- **Save invariant** — _edit never moves you_ (origin refetches in place); _create lands on the new record's detail_ on its derived surface (drawer over the still-intact list for light objects, detail route for heavy), with `replace:true` so Back skips the transient form entry.
|
|
26
|
+
- **Subtable child create/edit = overlay over the parent detail, never a route** — related-list New/Edit push `?form=…&formObject=<child>&formLink=<fk>:<parentId>`; the one global overlay pre-links the parent (refresh-safe), sizes to the CHILD object, and on save stays on the parent while only the child's related lists refetch. ModalForm now forwards `initialValues` into its master-detail (subforms) branch so pre-links survive for children with inline line items.
|
|
27
|
+
|
|
28
|
+
- fbec4e1: feat(studio): pick a connector action from the chosen connector (no more hand-typed action ids)
|
|
29
|
+
|
|
30
|
+
In a flow's **Connector Action** node, the `actionId` field was a free-text box
|
|
31
|
+
(`sendMessage · send` placeholder) — a typo silently produced a node that fails
|
|
32
|
+
at run time. It was left as text because a connector's actions have "no flat
|
|
33
|
+
catalog"; but each connector already advertises its actions in the runtime
|
|
34
|
+
descriptors (`GET /api/v1/automation/connectors` → `{ name, actions:[{key,label}] }`).
|
|
35
|
+
|
|
36
|
+
`actionId` is now a **picker of the chosen connector's actions**, resolved from
|
|
37
|
+
the sibling `connectorId` (mirroring how `object-field` lists the fields of its
|
|
38
|
+
resolved object). New reference kind `connector-action` + `connectorSource` on
|
|
39
|
+
`FlowReferenceSpec`; `useConnectorActionOptions` fetches the descriptors and
|
|
40
|
+
`resolveConnectorName` reads the connector from the node's `connectorConfig`. Like
|
|
41
|
+
every reference in the designer it stays an **editable combobox** — with no
|
|
42
|
+
connector chosen (or none installed) it degrades to free text with a hint
|
|
43
|
+
("Choose a Connector above to list its actions" / "Actions of <connector>.").
|
|
44
|
+
|
|
45
|
+
Closes the last critical hand-typed-identifier gap in flow-node config (the
|
|
46
|
+
object / field / flow / role / connector / template references were already
|
|
47
|
+
pickers). Unit-tested (`resolveConnectorName`, `connectorActionsToOptions`).
|
|
48
|
+
|
|
49
|
+
- 7a6837c: Studio package-create dogfood follow-ups (objectstack-ai/framework#2615):
|
|
50
|
+
|
|
51
|
+
- Read-only packages now gate authoring affordances client-side (Add field, New object/flow/permission set, nav Edit, Save draft, Publish, Create app) with a "switch to a writable package" hint, instead of letting doomed edits pile up until the server 422s (objectui#2259). Records stay fully usable; the field inspector opens read-only.
|
|
52
|
+
- New fields auto-derive their API name from the label while still auto-named — now also for the Data pillar's generic `field_N` names, so relabeling "New field" to "Status" yields a `status` column instead of `field_2` forever (objectui#2260).
|
|
53
|
+
- Publish is review-then-confirm: the header button opens the pending-changes panel, whose footer "Publish N change(s)" fires the atomic package publish; panel entries expand to a per-item field/property diff against the live version (objectui#2261).
|
|
54
|
+
- Create app can scaffold navigation from the package's objects (checkbox, on by default): one spec-valid object menu item per object, closing the "fresh app has zero nav" dead-end (objectui#2262).
|
|
55
|
+
|
|
56
|
+
- 5ed8d2d: feat(studio): automation enable/disable switch + live status in the Automations rail
|
|
57
|
+
|
|
58
|
+
The Automations pillar showed only an icon + label per flow, and no way to turn a
|
|
59
|
+
flow on or off — so an author couldn't tell whether an automation was live, or
|
|
60
|
+
stop one without deleting it (the header even said "Off by default · review before
|
|
61
|
+
enabling", but nothing reflected or controlled it). UX eval #6.
|
|
62
|
+
|
|
63
|
+
- **Live status dot** on every flow in the rail — a green "On" / gray "Off",
|
|
64
|
+
fetched from the engine's `GET /api/v1/automation/_status` (persisted `status`
|
|
65
|
+
is intent; this is what's actually enabled + bound to its trigger). Refetched
|
|
66
|
+
after a publish; degrades silently on an older backend. A flow the engine
|
|
67
|
+
doesn't know yet (never published) shows no dot — the amber "unpublished draft"
|
|
68
|
+
chip already covers that.
|
|
69
|
+
- **Enable/Disable switch** in the flow header. It flips the flow's deployment
|
|
70
|
+
`status` (active ↔ obsolete) and saves the draft immediately; the change goes
|
|
71
|
+
live when the package is published (so "review before enabling" is preserved).
|
|
72
|
+
Pairs with framework's engine-side gate (`obsolete`/`invalid` → not bound).
|
|
73
|
+
|
|
74
|
+
New `engine.studio.auto.*` i18n keys (en + zh). Unit-tested (`FlowStatusDot`:
|
|
75
|
+
enabled→On, disabled→Off, no-state→nothing, bound-vs-unbound tooltip). Verified in
|
|
76
|
+
a live browser: the rail shows a green "On" against every showcase flow and the
|
|
77
|
+
header switch reads "Enabled".
|
|
78
|
+
|
|
79
|
+
- 70c4a3f: Studio package-create dogfood follow-ups (framework#2615 — P2 wizard + P3 polish):
|
|
80
|
+
|
|
81
|
+
- **Package-id wizard feedback.** The three package wizards (switcher create,
|
|
82
|
+
landing create, landing duplicate) share a new `PackageIdInput`: illegal
|
|
83
|
+
characters are still normalized away, but no longer silently — a notice
|
|
84
|
+
says what was removed — a reverse-domain format hint shows while the id
|
|
85
|
+
doesn't parse, and a CJK-only name that yields no id suggestion is told to
|
|
86
|
+
type one manually instead of leaving the id box mysteriously empty.
|
|
87
|
+
- **Records-grid duplicate "Actions" column.** A field literally named
|
|
88
|
+
`actions` is now dropped from the Studio grid's data columns, so it no
|
|
89
|
+
longer collides with the always-pinned row-actions column (it stays
|
|
90
|
+
editable in the form designer).
|
|
91
|
+
- **Record-create verb consistency.** The `ObjectView` toolbar create button
|
|
92
|
+
resolved a hardcoded English "Create"; it now uses the same
|
|
93
|
+
`console.objectView.new` ("New" / 新建) key as the runtime object pages so
|
|
94
|
+
Studio and the running app agree.
|
|
95
|
+
- **Branded cold-load splash.** The console's pre-auth loading gate rendered a
|
|
96
|
+
bare "Loading…"; it now shows the branded, boot-safe `LoadingScreen`.
|
|
97
|
+
- **Picklist option editor.** Value/label inputs and CJK option labels no
|
|
98
|
+
longer truncate — the six controls that shared one cramped row are split
|
|
99
|
+
into a two-row layout so the inputs get the full panel width.
|
|
100
|
+
- **Draft-save confirmation.** The Data pillar's "Save draft" now shows a
|
|
101
|
+
success toast and a "last saved HH:MM" indicator, matching the App and
|
|
102
|
+
Automations pillars.
|
|
103
|
+
|
|
104
|
+
### Patch Changes
|
|
105
|
+
|
|
106
|
+
- ec6bb16: Studio Automations rail now shows authored-but-unpublished (draft) flows.
|
|
107
|
+
|
|
108
|
+
The Automations pillar loaded its rail with `client.list('flow', …)` only, which
|
|
109
|
+
returns published/active metadata — so a flow authored (saved as a draft) but not
|
|
110
|
+
yet published was invisible in the rail, even while the "Changes · N" counter
|
|
111
|
+
showed a pending draft existed. Every sibling pillar (Data / Interfaces / Access)
|
|
112
|
+
already merged `client.listDrafts`; Automations was the sole outlier.
|
|
113
|
+
|
|
114
|
+
The published ∪ draft merge is extracted into a shared, unit-tested
|
|
115
|
+
`loadPackageSurfaces` helper and adopted by the Automations pillar, which also now
|
|
116
|
+
re-reads on `publishNonce` so drafts that go live collapse back into the published
|
|
117
|
+
rail after a package publish. A draft-only flow now appears in its rail (badged
|
|
118
|
+
"Unpublished draft"), is selectable, and loads its draft body for editing —
|
|
119
|
+
matching the other pillars. Fixes the empty-rail report for writable-base packages
|
|
120
|
+
whose flows are all still drafts.
|
|
121
|
+
|
|
122
|
+
- 4fbf910: Stop double-firing action toasts on record-detail script actions and the delete handler.
|
|
123
|
+
|
|
124
|
+
`ActionRunner.handlePostExecution` already surfaces a result's `error` as a toast
|
|
125
|
+
(and a success toast unless `silent`). Two handlers ALSO toasted themselves while
|
|
126
|
+
returning `{success:false, error}` (or a non-`silent` success), so on a runner
|
|
127
|
+
seeded with `onToast` the same message fired twice:
|
|
128
|
+
|
|
129
|
+
- **`RecordDetailView` `serverActionHandler`** (script actions): the HTTP/inner-fail
|
|
130
|
+
branch and the catch branch each called `toast.error` before returning the error.
|
|
131
|
+
#2177 fixed the twin in `useConsoleActionRuntime` (interface pages) but not this
|
|
132
|
+
copy, so record-detail script-action failures (e.g. a `RECORD_LOCKED` from an
|
|
133
|
+
approval-locked record) still showed the error twice for everyone on the published
|
|
134
|
+
console bundle. Both branches now return the error and let the runner toast it once.
|
|
135
|
+
|
|
136
|
+
- **`useObjectActions` `delete` handler** (ObjectView list/detail deletes): kept its
|
|
137
|
+
richer localized toast (label + description, or the bulk succeeded/failed summary)
|
|
138
|
+
and now returns WITHOUT `error` on failure so the runner doesn't re-toast it, and
|
|
139
|
+
marks successful deletes `silent` so the runner doesn't append a second generic
|
|
140
|
+
"Action completed successfully" toast.
|
|
141
|
+
|
|
142
|
+
Adds `useObjectActions.test.tsx` asserting exactly one toast on delete
|
|
143
|
+
success / failure / partial-bulk-failure.
|
|
144
|
+
|
|
145
|
+
- 6f15e43: test(studio): extend the create-conformance gate to the inline pillar creators
|
|
146
|
+
|
|
147
|
+
`createConformance.test.ts` guards that every authorable type's default
|
|
148
|
+
create-form output passes spec validation — catching the recurring "the designer
|
|
149
|
+
emits a minimal shape the spec rejects, so create→save 422s" dead-end family. But
|
|
150
|
+
it read only the metadata-admin registry, so the Studio's **inline** "New X"
|
|
151
|
+
creators (Data → object, Automations → flow, Interfaces → app, Access →
|
|
152
|
+
permission) — which build their skeletons directly in `StudioDesignSurface.tsx`,
|
|
153
|
+
bypassing the registry — were **uncovered**. A future edit to one of those shapes
|
|
154
|
+
could turn its "New" button into a silent dead-end with nothing to catch it.
|
|
155
|
+
|
|
156
|
+
Extracted the four inline skeletons into pure, exported builders
|
|
157
|
+
(`studio-design/skeletons.ts`) consumed by BOTH the pillars and a new gate block,
|
|
158
|
+
so the test can't drift from what the "New" button actually emits. No behavior
|
|
159
|
+
change — the builders return the byte-identical skeletons. The gate now covers all
|
|
160
|
+
create paths (registry + inline); the four inline skeletons validate clean.
|
|
161
|
+
|
|
162
|
+
- Updated dependencies [544d8eb]
|
|
163
|
+
- Updated dependencies [6fffd3d]
|
|
164
|
+
- Updated dependencies [9255686]
|
|
165
|
+
- Updated dependencies [fae75e2]
|
|
166
|
+
- Updated dependencies [1072701]
|
|
167
|
+
- @object-ui/i18n@11.5.0
|
|
168
|
+
- @object-ui/react@11.5.0
|
|
169
|
+
- @object-ui/components@11.5.0
|
|
170
|
+
- @object-ui/types@11.5.0
|
|
171
|
+
- @object-ui/data-objectstack@11.5.0
|
|
172
|
+
- @object-ui/fields@11.5.0
|
|
173
|
+
- @object-ui/layout@11.5.0
|
|
174
|
+
- @object-ui/plugin-editor@11.5.0
|
|
175
|
+
- @object-ui/auth@11.5.0
|
|
176
|
+
- @object-ui/collaboration@11.5.0
|
|
177
|
+
- @object-ui/core@11.5.0
|
|
178
|
+
- @object-ui/permissions@11.5.0
|
|
179
|
+
- @object-ui/providers@11.5.0
|
|
180
|
+
|
|
3
181
|
## 11.4.0
|
|
4
182
|
|
|
5
183
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -152,6 +152,15 @@ for each metadata type. Every type has a pure-renderer **preview** that doubles
|
|
|
152
152
|
as its **designer** when given `editing` + `onPatch` props — no backend round
|
|
153
153
|
trip is required to edit a draft.
|
|
154
154
|
|
|
155
|
+
### App → Studio reverse bridge
|
|
156
|
+
|
|
157
|
+
Inside a running app, workspace admins get a "Design in Studio" entry in the
|
|
158
|
+
top bar (`AppHeader`) that deep-links to the app's owning package on the Studio
|
|
159
|
+
design surface (`/studio/:packageId/data`). It is the reverse of the builder's
|
|
160
|
+
"Open app" bridge (ADR-0080): the entry only renders for admins and only when
|
|
161
|
+
the app has an owning package (`_packageId`), and package writability stays a
|
|
162
|
+
server-side concern — a read-only package opens in Studio as browse-only.
|
|
163
|
+
|
|
155
164
|
### Studio package scope
|
|
156
165
|
|
|
157
166
|
Studio treats the selected package as the authoring scope. The package selector
|
|
@@ -9,12 +9,13 @@ import { useEffect, useMemo } from 'react';
|
|
|
9
9
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from '@object-ui/components';
|
|
10
10
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
11
11
|
import { useUrlOverlay } from '../hooks/useUrlOverlay';
|
|
12
|
+
import { KEYBOARD_SHORTCUTS_PARAM } from '../urlParams';
|
|
12
13
|
export function KeyboardShortcutsDialog() {
|
|
13
14
|
const { t } = useObjectTranslation();
|
|
14
15
|
// URL-addressable (?shortcuts=1) so the dialog is deep-linkable and openable
|
|
15
16
|
// from the header Help menu, not only via the `?` keyboard accelerator (ADR-0054
|
|
16
17
|
// C1/C2/C3).
|
|
17
|
-
const { open, setOpen, toggleOverlay } = useUrlOverlay(
|
|
18
|
+
const { open, setOpen, toggleOverlay } = useUrlOverlay(KEYBOARD_SHORTCUTS_PARAM);
|
|
18
19
|
const shortcutGroups = useMemo(() => [
|
|
19
20
|
{
|
|
20
21
|
title: t('console.shortcuts.groups.general'),
|
|
@@ -8,13 +8,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* AuthGuard, AdapterProvider, MetadataProvider, theme/toaster, /home, /login,
|
|
9
9
|
* /organizations) is provided by `createConsole` from @object-ui/app-shell.
|
|
10
10
|
*/
|
|
11
|
-
import { Routes, Route, Navigate, useNavigate, useLocation, useParams } from 'react-router-dom';
|
|
11
|
+
import { Routes, Route, Navigate, useNavigate, useLocation, useParams, useSearchParams } from 'react-router-dom';
|
|
12
12
|
import { useState, useEffect, useCallback, useRef, lazy, Suspense, useMemo } from 'react';
|
|
13
13
|
import { useAssistant } from '../assistant/assistantBus';
|
|
14
14
|
import { ModalForm } from '@object-ui/plugin-form';
|
|
15
15
|
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
|
|
16
16
|
import { toast } from 'sonner';
|
|
17
|
-
import { useActionRunner, useGlobalUndo } from '@object-ui/react';
|
|
17
|
+
import { useActionRunner, useGlobalUndo, useMutationInvalidationBridge, notifyDataChanged } from '@object-ui/react';
|
|
18
18
|
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
19
19
|
import { useAuth } from '@object-ui/auth';
|
|
20
20
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
@@ -23,7 +23,9 @@ import { usePreviewDrafts } from '../preview/PreviewModeContext';
|
|
|
23
23
|
import { PreviewDraftEmptyState } from '../preview/PreviewDraftEmptyState';
|
|
24
24
|
import { ExpressionProvider, evaluateVisibility } from '../providers/ExpressionProvider';
|
|
25
25
|
import { useTrackRouteAsRecent } from '../hooks/useTrackRouteAsRecent';
|
|
26
|
-
import { resolveRecordFormTarget, resolveFormViewLayout, resolveNavigateCreateUrl, resolveNavigateEditUrl } from '../utils/recordFormNavigation';
|
|
26
|
+
import { resolveRecordFormTarget, resolveFormViewLayout, resolveNavigateCreateUrl, resolveNavigateEditUrl, resolvePostCreateTarget } from '../utils/recordFormNavigation';
|
|
27
|
+
import { deriveRecordSurface, deriveRecordFlowSurface } from '@object-ui/plugin-view';
|
|
28
|
+
import { RECORD_FORM_PARAM, RECORD_FORM_OBJECT_PARAM, RECORD_FORM_LINK_PARAM } from '../urlParams';
|
|
27
29
|
import { matchAppBySegment } from '../utils/appRoute';
|
|
28
30
|
import { resolveHref } from '@object-ui/layout';
|
|
29
31
|
import { ExpressionEvaluator } from '@object-ui/core';
|
|
@@ -45,6 +47,7 @@ const ReportView = lazy(() => import('../views/ReportView').then(m => ({ default
|
|
|
45
47
|
const SearchResultsPage = lazy(() => import('../views/SearchResultsPage').then(m => ({ default: m.SearchResultsPage })));
|
|
46
48
|
const RecordFormPage = lazy(() => import('../views/RecordFormPage').then(m => ({ default: m.RecordFormPage })));
|
|
47
49
|
const ComponentNavView = lazy(() => import('../views/ComponentNavView').then(m => ({ default: m.ComponentNavView })));
|
|
50
|
+
const ObjectDataPage = lazy(() => import('../views/ObjectDataPage').then(m => ({ default: m.ObjectDataPage })));
|
|
48
51
|
// Metadata admin — mounted under /apps/:app/metadata. Lives at the top
|
|
49
52
|
// level so URLs read like a normal nested resource (RFC-style) instead of
|
|
50
53
|
// piggy-backing on the legacy ComponentRegistry fan-out.
|
|
@@ -201,30 +204,80 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
201
204
|
navigate(`/apps/${seg}/component/developer/packages`, { replace: true });
|
|
202
205
|
}
|
|
203
206
|
}, [activeApp?.name, appName, location.pathname, navigate]);
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
// #2604 — the create/edit overlay is URL-driven (`?form=new` / `?form=<id>`),
|
|
208
|
+
// not component state: the record form is a TASK overlay over the origin
|
|
209
|
+
// route, and putting its open-state in the URL makes browser Back close the
|
|
210
|
+
// overlay (returning to the intact origin) instead of abandoning the route
|
|
211
|
+
// with the overlay marooned on top. Same pattern as the detail drawer's
|
|
212
|
+
// `?recordId=…` (useUrlOverlay / ADR-0054 C3). Open pushes a history entry;
|
|
213
|
+
// every close strips the param with `replace` so no stale reopen-entry stays
|
|
214
|
+
// ahead in history.
|
|
215
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
216
|
+
const recordFormParam = searchParams.get(RECORD_FORM_PARAM);
|
|
217
|
+
// #2604 D3 — child-task extension of the record-form URL contract:
|
|
218
|
+
// `formObject` names the object the form edits when it is NOT the route's
|
|
219
|
+
// object (a subtable child opened over its parent's detail); `formLink`
|
|
220
|
+
// ("field:id") pre-links the parent on create. Keeping the whole task in
|
|
221
|
+
// the URL means Back closes the overlay and a refresh reopens it still
|
|
222
|
+
// correctly parent-linked — no transient component state to lose.
|
|
223
|
+
const formObjectParam = searchParams.get(RECORD_FORM_OBJECT_PARAM);
|
|
224
|
+
const formLinkParam = searchParams.get(RECORD_FORM_LINK_PARAM);
|
|
225
|
+
const editingRecord = useMemo(() => (recordFormParam && recordFormParam !== 'new' ? { id: recordFormParam } : null), [recordFormParam]);
|
|
226
|
+
const formLinkValues = useMemo(() => {
|
|
227
|
+
if (!formLinkParam)
|
|
228
|
+
return undefined;
|
|
229
|
+
const i = formLinkParam.indexOf(':');
|
|
230
|
+
if (i <= 0)
|
|
231
|
+
return undefined;
|
|
232
|
+
return { [formLinkParam.slice(0, i)]: formLinkParam.slice(i + 1) };
|
|
233
|
+
}, [formLinkParam]);
|
|
206
234
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
235
|
+
const isDialogOpen = !!recordFormParam;
|
|
236
|
+
// Close the record-form overlay by stripping `?form` in place. Reads the
|
|
237
|
+
// LIVE location (not the hook's render-time snapshot) so it stays a no-op
|
|
238
|
+
// when a success handler already navigated away in the same tick (the
|
|
239
|
+
// post-create redirect below).
|
|
240
|
+
const closeRecordForm = useCallback(() => {
|
|
241
|
+
const sp = new URLSearchParams(window.location.search);
|
|
242
|
+
if (!sp.has(RECORD_FORM_PARAM) && !sp.has(RECORD_FORM_OBJECT_PARAM) && !sp.has(RECORD_FORM_LINK_PARAM))
|
|
243
|
+
return;
|
|
244
|
+
sp.delete(RECORD_FORM_PARAM);
|
|
245
|
+
sp.delete(RECORD_FORM_OBJECT_PARAM);
|
|
246
|
+
sp.delete(RECORD_FORM_LINK_PARAM);
|
|
247
|
+
setSearchParams(sp, { replace: true });
|
|
248
|
+
}, [setSearchParams]);
|
|
207
249
|
const { execute: executeAction, runner } = useActionRunner();
|
|
250
|
+
// objectui#2269 — bridge every dataSource write (create/update/delete →
|
|
251
|
+
// MutationEvent) onto the invalidation bus, ONCE for the whole console.
|
|
252
|
+
// Readers (record detail, related lists, count badges) refetch in place;
|
|
253
|
+
// nothing is remounted for a data refresh.
|
|
254
|
+
useMutationInvalidationBridge(dataSource);
|
|
208
255
|
useGlobalUndo({
|
|
209
256
|
dataSource: dataSource ?? undefined,
|
|
210
257
|
onUndo: (op) => {
|
|
211
258
|
toast.info(`Undo: ${op.description}`, { duration: 4000 });
|
|
212
259
|
setRefreshKey(k => k + 1);
|
|
260
|
+
// Precisely-scoped invalidation — UndoableOperation carries the target
|
|
261
|
+
// (objectName + recordId), so detail readers refresh in place too.
|
|
262
|
+
if (op?.objectName)
|
|
263
|
+
notifyDataChanged({ objectName: op.objectName, recordId: op.recordId });
|
|
213
264
|
},
|
|
214
265
|
onRedo: (op) => {
|
|
215
266
|
toast.info(`Redo: ${op.description}`, { duration: 3000 });
|
|
216
267
|
setRefreshKey(k => k + 1);
|
|
268
|
+
if (op?.objectName)
|
|
269
|
+
notifyDataChanged({ objectName: op.objectName, recordId: op.recordId });
|
|
217
270
|
},
|
|
218
271
|
});
|
|
219
272
|
useEffect(() => {
|
|
220
273
|
runner.registerHandler('crud_success', async (action) => {
|
|
221
|
-
|
|
274
|
+
closeRecordForm();
|
|
222
275
|
setRefreshKey(k => k + 1);
|
|
223
276
|
toast.success(action.params?.message ?? 'Record saved successfully');
|
|
224
277
|
return { success: true, reload: true };
|
|
225
278
|
});
|
|
226
279
|
runner.registerHandler('dialog_cancel', async () => {
|
|
227
|
-
|
|
280
|
+
closeRecordForm();
|
|
228
281
|
return { success: true };
|
|
229
282
|
});
|
|
230
283
|
// Page-mode navigation handlers — declarative counterparts to the
|
|
@@ -274,7 +327,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
274
327
|
// `flow` handler on this top-level useActionRunner — it lives on a
|
|
275
328
|
// different ActionRunner instance and would never be invoked from the
|
|
276
329
|
// record/list action buttons.
|
|
277
|
-
}, [runner, navigate, appName]);
|
|
330
|
+
}, [runner, navigate, appName, closeRecordForm]);
|
|
278
331
|
useEffect(() => {
|
|
279
332
|
if (!dataSource)
|
|
280
333
|
return;
|
|
@@ -296,8 +349,15 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
296
349
|
objectNameFromPath = '';
|
|
297
350
|
}
|
|
298
351
|
const currentObjectDef = allObjects.find((o) => o.name === objectNameFromPath);
|
|
352
|
+
// The object the record-form overlay edits: the route's object by default,
|
|
353
|
+
// or the `formObject` child override (#2604 D3 — subtable child task opened
|
|
354
|
+
// over its parent's detail).
|
|
355
|
+
const formObjectDef = formObjectParam
|
|
356
|
+
? allObjects.find((o) => o.name === formObjectParam)
|
|
357
|
+
: currentObjectDef;
|
|
358
|
+
const isChildFormTask = !!formObjectParam && formObjectParam !== currentObjectDef?.name;
|
|
299
359
|
const handleCrudSuccess = useCallback(() => {
|
|
300
|
-
const label =
|
|
360
|
+
const label = formObjectDef ? objectLabel(formObjectDef) : t('common.record', { defaultValue: 'Record' });
|
|
301
361
|
executeAction({
|
|
302
362
|
type: 'crud_success',
|
|
303
363
|
params: {
|
|
@@ -306,10 +366,45 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
306
366
|
: t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` }),
|
|
307
367
|
},
|
|
308
368
|
});
|
|
309
|
-
}, [executeAction, editingRecord,
|
|
369
|
+
}, [executeAction, editingRecord, formObjectDef, objectLabel, t]);
|
|
310
370
|
const handleDialogCancel = useCallback(() => {
|
|
311
371
|
executeAction({ type: 'dialog_cancel' });
|
|
312
372
|
}, [executeAction]);
|
|
373
|
+
// #2604 save invariant — *edit never moves you; create takes you to the
|
|
374
|
+
// record you made.* Edit save: the origin route is untouched (crud_success
|
|
375
|
+
// bumps refreshKey → origin refetches in place). Create save: land on the
|
|
376
|
+
// new record's detail, on ITS derived surface — a light object's detail is
|
|
377
|
+
// the drawer over the still-intact list; a heavy one is the detail route.
|
|
378
|
+
// `replace: true` swaps out the transient `?form=…` entry so Back returns
|
|
379
|
+
// to the pre-create origin.
|
|
380
|
+
const handleRecordFormSuccess = useCallback(async (saved) => {
|
|
381
|
+
// Child task (#2604 D3): the parent detail must stay EXACTLY as it was —
|
|
382
|
+
// active tab, scroll, everything. So do NOT go through crud_success;
|
|
383
|
+
// the child's open related lists refetch on their own via the
|
|
384
|
+
// invalidation bus (#2269): the dataSource write already emitted a
|
|
385
|
+
// MutationEvent that the bridge fans out — no manual notify needed.
|
|
386
|
+
if (isChildFormTask) {
|
|
387
|
+
const label = formObjectDef ? objectLabel(formObjectDef) : t('common.record', { defaultValue: 'Record' });
|
|
388
|
+
toast.success(editingRecord
|
|
389
|
+
? t('form.updateSuccess', { object: label, defaultValue: `${label} updated successfully` })
|
|
390
|
+
: t('form.createSuccess', { object: label, defaultValue: `${label} created successfully` }));
|
|
391
|
+
closeRecordForm();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
handleCrudSuccess();
|
|
395
|
+
if (editingRecord || !currentObjectDef)
|
|
396
|
+
return;
|
|
397
|
+
const target = resolvePostCreateTarget({
|
|
398
|
+
objectName: currentObjectDef.name,
|
|
399
|
+
baseUrl: appName ? `/apps/${appName}` : (activeApp?.name ? `/apps/${activeApp.name}` : ''),
|
|
400
|
+
pathname: location.pathname,
|
|
401
|
+
search: window.location.search,
|
|
402
|
+
surface: deriveRecordSurface(currentObjectDef),
|
|
403
|
+
recordId: saved?.id ?? saved?._id,
|
|
404
|
+
});
|
|
405
|
+
if (target.kind !== 'none')
|
|
406
|
+
navigate(target.url, { replace: true });
|
|
407
|
+
}, [handleCrudSuccess, isChildFormTask, formObjectDef, editingRecord, currentObjectDef, appName, activeApp?.name, location.pathname, navigate, closeRecordForm, objectLabel, t]);
|
|
313
408
|
// Track recent items on route change.
|
|
314
409
|
useTrackRouteAsRecent({
|
|
315
410
|
pathname: location.pathname,
|
|
@@ -330,8 +425,16 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
330
425
|
navigate(target.url);
|
|
331
426
|
return;
|
|
332
427
|
}
|
|
333
|
-
|
|
334
|
-
|
|
428
|
+
// Open the overlay via the URL (pushes one history entry → Back closes
|
|
429
|
+
// the overlay, origin intact). `new` = create; a record id = edit.
|
|
430
|
+
const rawId = record?.id ?? record?._id;
|
|
431
|
+
const sp = new URLSearchParams(window.location.search);
|
|
432
|
+
sp.set(RECORD_FORM_PARAM, rawId != null && rawId !== '' ? String(rawId) : 'new');
|
|
433
|
+
// Top-level task on the route's own object — drop any stale child-task
|
|
434
|
+
// overrides (see the record-form URL contract above).
|
|
435
|
+
sp.delete(RECORD_FORM_OBJECT_PARAM);
|
|
436
|
+
sp.delete(RECORD_FORM_LINK_PARAM);
|
|
437
|
+
setSearchParams(sp);
|
|
335
438
|
};
|
|
336
439
|
const handleAppChange = (newAppName) => {
|
|
337
440
|
navigate(`/apps/${newAppName}`);
|
|
@@ -395,12 +498,27 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
395
498
|
// overview instead of a blank `<Navigate to="">`.
|
|
396
499
|
const landing = resolveLandingRoute(activeApp, { currentUserId: user?.id ?? null });
|
|
397
500
|
return landing ? _jsx(Navigate, { to: landing, replace: true }) : _jsx(StudioHomePage, {});
|
|
398
|
-
})() }), _jsx(Route, { path: "metadata/package/*", element: _jsx(Navigate, { to: `/apps/${appName ?? activeApp.name}/component/developer/packages`, replace: true }) }), _jsxs(Route, { path: "metadata", children: [_jsx(Route, { index: true, element: _jsx(MetadataDirectoryPage, {}) }), _jsx(Route, { path: "_diagnostics", element: _jsx(MetadataDiagnosticsPage, {}) }), _jsx(Route, { path: ":type", element: _jsx(MetadataResourceListPage, {}) }), _jsx(Route, { path: ":type/new", element: _jsx(MetadataResourceEditPage, { createMode: true }) }), _jsx(Route, { path: ":type/:name", element: _jsx(MetadataResourceEditPage, {}) }), _jsx(Route, { path: ":type/:name/history", element: _jsx(MetadataResourceHistoryPage, {}) })] }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }
|
|
501
|
+
})() }), _jsx(Route, { path: "metadata/package/*", element: _jsx(Navigate, { to: `/apps/${appName ?? activeApp.name}/component/developer/packages`, replace: true }) }), _jsxs(Route, { path: "metadata", children: [_jsx(Route, { index: true, element: _jsx(MetadataDirectoryPage, {}) }), _jsx(Route, { path: "_diagnostics", element: _jsx(MetadataDiagnosticsPage, {}) }), _jsx(Route, { path: ":type", element: _jsx(MetadataResourceListPage, {}) }), _jsx(Route, { path: ":type/new", element: _jsx(MetadataResourceEditPage, { createMode: true }) }), _jsx(Route, { path: ":type/:name", element: _jsx(MetadataResourceEditPage, {}) }), _jsx(Route, { path: ":type/:name/history", element: _jsx(MetadataResourceHistoryPage, {}) })] }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/data", element: _jsx(ObjectDataPage, { dataSource: dataSource, objects: allObjects }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "component/:ns/:name/*", element: _jsx(ComponentNavView, {}) }), _jsx(Route, { path: "component/metadata/directory", element: _jsx(LegacyMetadataRedirect, { mode: "directory" }) }), _jsx(Route, { path: "component/metadata/resource/*", element: _jsx(LegacyMetadataRedirect, { mode: "resource" }) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), _jsx(Route, { path: "system/marketplace", element: _jsx(MarketplacePage, {}) }), _jsx(Route, { path: "system/marketplace/installed", element: _jsx(MarketplaceInstalledPage, {}) }), _jsx(Route, { path: "system/marketplace/:packageId", element: _jsx(MarketplacePackagePage, {}) }), extraRoutes, _jsx(Route, { path: ":objectName/:maybeRecordId", element: _jsx(ShorthandRecordRedirect, {}) }), _jsx(Route, { path: "*", element: _jsx(RouteNotFound, {}) })] }) }) }) }), formObjectDef && (_jsx(ModalForm, { schema: {
|
|
399
502
|
type: 'object-form',
|
|
400
503
|
formType: 'modal',
|
|
401
|
-
objectName:
|
|
504
|
+
objectName: formObjectDef.name,
|
|
402
505
|
mode: editingRecord ? 'edit' : 'create',
|
|
403
506
|
recordId: editingRecord?.id,
|
|
507
|
+
// Child create task (#2604 D3): pre-link the parent from the
|
|
508
|
+
// `formLink` URL param (refresh-safe — the link survives).
|
|
509
|
+
...(formLinkValues && !editingRecord ? { initialValues: formLinkValues } : {}),
|
|
510
|
+
// #2604 D1: create/edit follow the flow-surface derivation —
|
|
511
|
+
// field-heavy → full-screen modal (the same big canvas the
|
|
512
|
+
// detail page gets, with overlay return semantics); light
|
|
513
|
+
// objects keep the existing auto-sized modal (ModalForm
|
|
514
|
+
// infers from columns when modalSize is unset). Derived
|
|
515
|
+
// default only — anything spread later (form view layout)
|
|
516
|
+
// would win. Child tasks size to the CHILD object's def.
|
|
517
|
+
...(deriveRecordFlowSurface(formObjectDef, isChildFormTask
|
|
518
|
+
? (editingRecord ? 'child-edit' : 'child-create')
|
|
519
|
+
: (editingRecord ? 'edit' : 'create')).size === 'full'
|
|
520
|
+
? { modalSize: 'full' }
|
|
521
|
+
: {}),
|
|
404
522
|
// Honor the object's DEFAULT FORM VIEW: curated sections (field
|
|
405
523
|
// selection + order + grouping), `contentLayout: 'tabbed'` when the
|
|
406
524
|
// view is tabbed, and inline child collections (master-detail).
|
|
@@ -408,30 +526,31 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
408
526
|
// win over the flat `fields` list below; otherwise this resolves to
|
|
409
527
|
// {} and `fields` (every field, raw schema order) is used as before.
|
|
410
528
|
// `formType` stays 'modal' (the container). (#1890 / ADR-0050.)
|
|
411
|
-
...resolveFormViewLayout(
|
|
529
|
+
...resolveFormViewLayout(formObjectDef),
|
|
412
530
|
title: editingRecord
|
|
413
|
-
? t('form.editTitle', { object: objectLabel(
|
|
414
|
-
: t('form.createTitle', { object: objectLabel(
|
|
531
|
+
? t('form.editTitle', { object: objectLabel(formObjectDef) })
|
|
532
|
+
: t('form.createTitle', { object: objectLabel(formObjectDef) }),
|
|
415
533
|
description: editingRecord
|
|
416
|
-
? t('form.editDescription', { object: objectLabel(
|
|
417
|
-
: t('form.createDescription', { object: objectLabel(
|
|
534
|
+
? t('form.editDescription', { object: objectLabel(formObjectDef) })
|
|
535
|
+
: t('form.createDescription', { object: objectLabel(formObjectDef) }),
|
|
418
536
|
open: isDialogOpen,
|
|
419
|
-
onOpenChange:
|
|
537
|
+
onOpenChange: (open) => { if (!open)
|
|
538
|
+
closeRecordForm(); },
|
|
420
539
|
layout: 'vertical',
|
|
421
|
-
fields:
|
|
422
|
-
? (Array.isArray(
|
|
423
|
-
?
|
|
540
|
+
fields: formObjectDef.fields
|
|
541
|
+
? (Array.isArray(formObjectDef.fields)
|
|
542
|
+
? formObjectDef.fields
|
|
424
543
|
.filter((f) => {
|
|
425
544
|
if (typeof f === 'string')
|
|
426
545
|
return true;
|
|
427
546
|
return evaluateVisibility(f.visible, expressionEvaluator);
|
|
428
547
|
})
|
|
429
548
|
.map((f) => typeof f === 'string' ? f : f.name)
|
|
430
|
-
: Object.entries(
|
|
549
|
+
: Object.entries(formObjectDef.fields)
|
|
431
550
|
.filter(([_, f]) => evaluateVisibility(f.visible, expressionEvaluator))
|
|
432
551
|
.map(([key]) => key))
|
|
433
552
|
: [],
|
|
434
|
-
onSuccess:
|
|
553
|
+
onSuccess: handleRecordFormSuccess,
|
|
435
554
|
onCancel: handleDialogCancel,
|
|
436
555
|
showSubmit: true,
|
|
437
556
|
showCancel: true,
|
|
@@ -439,7 +558,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
439
558
|
? t('form.update', { defaultValue: t('common.save', { defaultValue: 'Save' }) })
|
|
440
559
|
: t('form.create', { defaultValue: t('common.create', { defaultValue: 'Create' }) }),
|
|
441
560
|
cancelText: t('common.cancel'),
|
|
442
|
-
}, dataSource: dataSource }, editingRecord?.id || 'new'))] })] }));
|
|
561
|
+
}, dataSource: dataSource }, `${formObjectDef.name}:${editingRecord?.id || 'new'}`))] })] }));
|
|
443
562
|
}
|
|
444
563
|
function findFirstRoute(items, ctx) {
|
|
445
564
|
if (!items || items.length === 0)
|
|
@@ -26,9 +26,16 @@ import { FavoritesProvider } from '../context/FavoritesProvider';
|
|
|
26
26
|
import { RecentItemsProvider } from '../context/RecentItemsProvider';
|
|
27
27
|
import { UserStateAdaptersProvider, useAttachUserStateAdapters, } from '../context/UserStateAdapters';
|
|
28
28
|
import { ThemeProvider } from '../chrome/ThemeProvider';
|
|
29
|
+
import { LoadingScreen } from '../chrome/LoadingScreen';
|
|
29
30
|
import { RemediationOverlay } from './RemediationOverlay';
|
|
31
|
+
// The console's every pre-React / pre-auth gate (Suspense fallback, adapter
|
|
32
|
+
// not ready, org/auth loading) renders this. It used to be a bare, unbranded
|
|
33
|
+
// "Loading…" line — ~8s of blank chrome before the login redirect on a cold
|
|
34
|
+
// /_console load (framework#2615 P3). Delegate to the branded, boot-safe
|
|
35
|
+
// splash (logo + product name + step list) that the rest of the shell already
|
|
36
|
+
// uses, so the cold load looks like the product from the first frame.
|
|
30
37
|
export function LoadingFallback() {
|
|
31
|
-
return
|
|
38
|
+
return _jsx(LoadingScreen, {});
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
34
41
|
* Provide a MODAL handler at the console ROOT — the same level the global
|
|
@@ -19,9 +19,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
19
19
|
*/
|
|
20
20
|
import { createContext, useContext, useEffect, useMemo, } from 'react';
|
|
21
21
|
import { useUrlOverlay } from '../hooks/useUrlOverlay';
|
|
22
|
+
import { COMMAND_PALETTE_PARAM } from '../urlParams';
|
|
22
23
|
const CommandPaletteContext = createContext(null);
|
|
23
24
|
export function CommandPaletteProvider({ children }) {
|
|
24
|
-
const { open, setOpen, openOverlay, closeOverlay, toggleOverlay } = useUrlOverlay(
|
|
25
|
+
const { open, setOpen, openOverlay, closeOverlay, toggleOverlay } = useUrlOverlay(COMMAND_PALETTE_PARAM, {
|
|
25
26
|
alias: 'cmdk',
|
|
26
27
|
});
|
|
27
28
|
// ⌘K / Ctrl+K accelerator. Lives here (not in CommandPalette) so the command
|
|
@@ -58,14 +58,21 @@ export function useObjectActions({ objectName, objectLabel, dataSource, onEdit,
|
|
|
58
58
|
label: objectLabel || objectName,
|
|
59
59
|
defaultValue: `Deleted ${succeeded} ${objectLabel || objectName} records`,
|
|
60
60
|
}));
|
|
61
|
-
|
|
61
|
+
// `silent`: the handler already toasted the localized summary above;
|
|
62
|
+
// without this the runner's post-execution hook adds a second, generic
|
|
63
|
+
// "Action completed successfully" toast (double success toast).
|
|
64
|
+
return { success: true, reload: true, silent: true };
|
|
62
65
|
}
|
|
63
66
|
toast.error(t('objectActions.bulkDeletePartial', {
|
|
64
67
|
succeeded,
|
|
65
68
|
failed,
|
|
66
69
|
defaultValue: `${succeeded} deleted, ${failed} failed`,
|
|
67
70
|
}));
|
|
68
|
-
|
|
71
|
+
// The toast above is the authoritative feedback (it carries the
|
|
72
|
+
// succeeded/failed summary the runner can't reconstruct). Return
|
|
73
|
+
// WITHOUT `error` so the ActionRunner post-execution hook — this runner
|
|
74
|
+
// has a toastHandler (onToast) — doesn't fire a second, duplicate toast.
|
|
75
|
+
return { success: false };
|
|
69
76
|
}
|
|
70
77
|
const recordId = action.params?.recordId ??
|
|
71
78
|
action.params?.record?.id ??
|
|
@@ -77,13 +84,18 @@ export function useObjectActions({ objectName, objectLabel, dataSource, onEdit,
|
|
|
77
84
|
await dataSource.delete(objectName, recordId);
|
|
78
85
|
onRefresh?.();
|
|
79
86
|
toast.success(t('objectActions.deleteSuccess', { label: objectLabel || objectName }));
|
|
80
|
-
|
|
87
|
+
// `silent`: handler owns the localized success toast above — suppress the
|
|
88
|
+
// runner's generic duplicate (see the bulk branch).
|
|
89
|
+
return { success: true, reload: true, silent: true };
|
|
81
90
|
}
|
|
82
91
|
catch (err) {
|
|
83
92
|
toast.error(t('objectActions.deleteFailed', { label: objectLabel || objectName }), {
|
|
84
93
|
description: err.message,
|
|
85
94
|
});
|
|
86
|
-
|
|
95
|
+
// Keep the richer toast above (label + error description) and return
|
|
96
|
+
// WITHOUT `error` so the ActionRunner post-execution hook doesn't toast
|
|
97
|
+
// the raw message a second time. See the bulk branch for the rationale.
|
|
98
|
+
return { success: false };
|
|
87
99
|
}
|
|
88
100
|
});
|
|
89
101
|
// Handler: navigate
|