@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
|
@@ -7,11 +7,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
* the object field definitions.
|
|
8
8
|
*/
|
|
9
9
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
10
|
-
import { useParams, useNavigate, useLocation, Link } from 'react-router-dom';
|
|
10
|
+
import { useParams, useNavigate, useLocation, useSearchParams, Link } from 'react-router-dom';
|
|
11
11
|
import { DetailView, RecordChatterPanel, buildDefaultPageSchema, deriveFieldGroupDetailSections, extractMentions } from '@object-ui/plugin-detail';
|
|
12
12
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
13
13
|
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
14
|
-
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider, useGlobalUndo } from '@object-ui/react';
|
|
14
|
+
import { ActionProvider, useObjectTranslation, useObjectLabel, usePageAssignment, RecordContextProvider, SchemaRenderer, DiscussionContextProvider, HighlightFieldsProvider, useGlobalUndo, useDataInvalidation, notifyDataChanged } from '@object-ui/react';
|
|
15
15
|
import { buildExpandFields } from '@object-ui/core';
|
|
16
16
|
import { toast } from 'sonner';
|
|
17
17
|
import { useRecordPresence, PresenceAvatars } from '@object-ui/collaboration';
|
|
@@ -27,6 +27,8 @@ import { ActionParamDialog } from './ActionParamDialog';
|
|
|
27
27
|
import { ActionResultDialog } from './ActionResultDialog';
|
|
28
28
|
import { FlowRunner } from './FlowRunner';
|
|
29
29
|
import { RelatedRecordActionsBridge } from './RelatedRecordActionsBridge';
|
|
30
|
+
import { withPageTabsUrlSync } from '../utils/pageTabsUrlSync';
|
|
31
|
+
import { RECORD_DETAIL_TAB_PARAM } from '../urlParams';
|
|
30
32
|
import { resolveActionParams } from '../utils/resolveActionParams';
|
|
31
33
|
import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
|
|
32
34
|
import { useRecordApprovals } from '../hooks/useRecordApprovals';
|
|
@@ -80,6 +82,20 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
80
82
|
const { showDebug } = useMetadataInspector();
|
|
81
83
|
const { user } = useAuth();
|
|
82
84
|
const navigate = useNavigate();
|
|
85
|
+
// objectui#2257 — the active detail tab is URL-addressable (`?tab=`), so it
|
|
86
|
+
// survives the page subtree remounting (refreshKey-style save refreshes;
|
|
87
|
+
// dev-StrictMode URL churn). Written with `replace` — switching tabs must
|
|
88
|
+
// not stack history entries (Back would page through tabs and fight the
|
|
89
|
+
// overlay contract where Back closes `?form=…`).
|
|
90
|
+
const [tabSearchParams, setTabSearchParams] = useSearchParams();
|
|
91
|
+
const activeTabParam = tabSearchParams.get(RECORD_DETAIL_TAB_PARAM) ?? undefined;
|
|
92
|
+
const handleTabChange = useCallback((value) => {
|
|
93
|
+
const sp = new URLSearchParams(window.location.search);
|
|
94
|
+
if (sp.get(RECORD_DETAIL_TAB_PARAM) === value)
|
|
95
|
+
return;
|
|
96
|
+
sp.set(RECORD_DETAIL_TAB_PARAM, value);
|
|
97
|
+
setTabSearchParams(sp, { replace: true });
|
|
98
|
+
}, [setTabSearchParams]);
|
|
83
99
|
const location = useLocation();
|
|
84
100
|
const originFrom = location.state?.from;
|
|
85
101
|
const { t } = useObjectTranslation();
|
|
@@ -89,7 +105,6 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
89
105
|
const [isLoading, setIsLoading] = useState(true);
|
|
90
106
|
const [feedItems, setFeedItems] = useState([]);
|
|
91
107
|
const [mentionSuggestions, setMentionSuggestions] = useState([]);
|
|
92
|
-
const [actionRefreshKey, setActionRefreshKey] = useState(0);
|
|
93
108
|
// Screen-flow runtime: a paused `screen`-node flow launched from a record action.
|
|
94
109
|
const [screenFlow, setScreenFlow] = useState(null);
|
|
95
110
|
const [childRelatedData, setChildRelatedData] = useState({});
|
|
@@ -105,6 +120,17 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
105
120
|
// Navigation code passes `record.id || record._id` directly into the URL
|
|
106
121
|
// without adding any prefix, so no stripping is needed.
|
|
107
122
|
const pureRecordId = recordId;
|
|
123
|
+
// #2269 — "refresh data, don't rebuild UI": after an action/save succeeds
|
|
124
|
+
// we INVALIDATE this record on the bus instead of bumping a key that
|
|
125
|
+
// remounted the whole DetailView (destroying tab/scroll/inline-edit state).
|
|
126
|
+
// `recordInvalidationNonce` is the read side: it bumps when THIS record (or
|
|
127
|
+
// its object) is invalidated by anyone — the form overlay, an action, an
|
|
128
|
+
// undo, or any dataSource write via the MutationEvent bridge.
|
|
129
|
+
const recordInvalidationNonce = useDataInvalidation(objectName || undefined, pureRecordId || undefined);
|
|
130
|
+
const notifyRecordChanged = useCallback(() => {
|
|
131
|
+
if (objectName)
|
|
132
|
+
notifyDataChanged({ objectName, recordId: pureRecordId || undefined });
|
|
133
|
+
}, [objectName, pureRecordId]);
|
|
108
134
|
// Record-scoped presence ("who else is viewing this record"). The default
|
|
109
135
|
// PresenceProvider source is a no-op, so this resolves to `[]` until a
|
|
110
136
|
// realtime transport (WebSocket-backed source) is wired in by the host
|
|
@@ -231,7 +257,9 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
231
257
|
cancelled = true;
|
|
232
258
|
window.removeEventListener('objectui:record-changed', onChanged);
|
|
233
259
|
};
|
|
234
|
-
|
|
260
|
+
// #2269: recordInvalidationNonce re-runs this fetch in place whenever the
|
|
261
|
+
// record (or its object) is invalidated on the bus.
|
|
262
|
+
}, [effectivePage, objectName, pureRecordId, dataSource, objectDef, recordInvalidationNonce]);
|
|
235
263
|
// Schema-driven path: derive a human-readable record title from the
|
|
236
264
|
// loaded `pageRecord` so favourites (record:*) and the breadcrumb show
|
|
237
265
|
// e.g. "Acme Corporation" instead of the raw record id. The legacy
|
|
@@ -308,7 +336,13 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
308
336
|
// "Undo" button (for `undoable` actions) restores the record's prior values.
|
|
309
337
|
const undoCtl = useGlobalUndo({
|
|
310
338
|
dataSource,
|
|
311
|
-
onUndo: () => {
|
|
339
|
+
onUndo: (op) => {
|
|
340
|
+
if (op?.objectName)
|
|
341
|
+
notifyDataChanged({ objectName: op.objectName, recordId: op.recordId });
|
|
342
|
+
else
|
|
343
|
+
notifyRecordChanged();
|
|
344
|
+
toast.success('Change undone');
|
|
345
|
+
},
|
|
312
346
|
});
|
|
313
347
|
const toastHandler = useCallback((message, options) => {
|
|
314
348
|
if (options?.type === 'error') {
|
|
@@ -393,12 +427,12 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
393
427
|
}
|
|
394
428
|
const shouldRefresh = action.refreshAfter === true;
|
|
395
429
|
if (shouldRefresh) {
|
|
396
|
-
|
|
430
|
+
notifyRecordChanged();
|
|
397
431
|
}
|
|
398
432
|
else if (undo) {
|
|
399
433
|
// Even when refreshAfter isn't set, reflect the change so the user sees
|
|
400
434
|
// it (and the subsequent Undo) on the open record.
|
|
401
|
-
|
|
435
|
+
notifyRecordChanged();
|
|
402
436
|
}
|
|
403
437
|
return { success: true, reload: shouldRefresh, undo };
|
|
404
438
|
}
|
|
@@ -451,7 +485,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
451
485
|
}
|
|
452
486
|
const shouldRefresh = action.refreshAfter !== false;
|
|
453
487
|
if (shouldRefresh) {
|
|
454
|
-
|
|
488
|
+
notifyRecordChanged();
|
|
455
489
|
}
|
|
456
490
|
return { success: true, data: json?.data, reload: shouldRefresh };
|
|
457
491
|
}
|
|
@@ -565,7 +599,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
565
599
|
}
|
|
566
600
|
}
|
|
567
601
|
if (action.refreshAfter === true)
|
|
568
|
-
|
|
602
|
+
notifyRecordChanged();
|
|
569
603
|
return { success: true };
|
|
570
604
|
}
|
|
571
605
|
const obj = action.objectName || objectName || 'global';
|
|
@@ -593,14 +627,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
593
627
|
}
|
|
594
628
|
catch { /* ignore */ }
|
|
595
629
|
}
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
|
|
630
|
+
// Don't toast here. This handler always runs through the ActionRunner
|
|
631
|
+
// (registered as the `script` handler on the ActionProvider below, which
|
|
632
|
+
// wires `onToast`), whose post-execution hook surfaces the returned
|
|
633
|
+
// `error` as one toast. Toasting again double-fired the message
|
|
634
|
+
// (e.g. RECORD_LOCKED appeared twice). Mirrors useConsoleActionRuntime.
|
|
599
635
|
return { success: false, error: errMsg };
|
|
600
636
|
}
|
|
601
637
|
const shouldRefresh = action.refreshAfter !== false;
|
|
602
638
|
if (shouldRefresh)
|
|
603
|
-
|
|
639
|
+
notifyRecordChanged();
|
|
604
640
|
const result = json?.data;
|
|
605
641
|
// ── redirectUrl convention ────────────────────────────────────────
|
|
606
642
|
// A script-action handler can return `{ redirectUrl: 'https://…' }`
|
|
@@ -666,7 +702,8 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
666
702
|
catch { /* ignore */ }
|
|
667
703
|
}
|
|
668
704
|
const msg = error.message;
|
|
669
|
-
toast
|
|
705
|
+
// Don't toast here — the ActionRunner post-execution hook toasts the
|
|
706
|
+
// returned `error` once (see the failure branch above).
|
|
670
707
|
return { success: false, error: msg };
|
|
671
708
|
}
|
|
672
709
|
finally {
|
|
@@ -699,7 +736,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
699
736
|
else {
|
|
700
737
|
return { success: false, error: `Unknown approval target: ${target}` };
|
|
701
738
|
}
|
|
702
|
-
|
|
739
|
+
notifyRecordChanged();
|
|
703
740
|
return { success: true, reload: true };
|
|
704
741
|
}
|
|
705
742
|
catch (err) {
|
|
@@ -1519,6 +1556,10 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1519
1556
|
primaryField,
|
|
1520
1557
|
sections,
|
|
1521
1558
|
autoTabs: true,
|
|
1559
|
+
// objectui#2257 — URL-driven active tab (same `?tab=` contract as the
|
|
1560
|
+
// schema path's page:tabs node).
|
|
1561
|
+
defaultTab: activeTabParam,
|
|
1562
|
+
onTabChange: handleTabChange,
|
|
1522
1563
|
autoDiscoverRelated: true,
|
|
1523
1564
|
...(historyEnabled && {
|
|
1524
1565
|
history: {
|
|
@@ -1537,7 +1578,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1537
1578
|
}),
|
|
1538
1579
|
};
|
|
1539
1580
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1540
|
-
}, [objectDef?.name, pureRecordId, childRelatedData,
|
|
1581
|
+
}, [objectDef?.name, pureRecordId, childRelatedData, appName, navigate, dataSource, t, objectLabel, objects, historyEnabled, historyEntries, historyLoading, approvals.available, approvals.canDecide, approvals.pendingRequest, approvals.latestRequest, embedded, activeTabParam, handleTabChange]);
|
|
1541
1582
|
if (isLoading) {
|
|
1542
1583
|
return _jsx(SkeletonDetail, {});
|
|
1543
1584
|
}
|
|
@@ -1730,7 +1771,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1730
1771
|
// there as a renderer capability).
|
|
1731
1772
|
...(assignedSlots ? { slots: assignedSlots } : {}),
|
|
1732
1773
|
});
|
|
1733
|
-
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [recordPresence.length > 0 && (_jsx(PresenceAvatars, { users: recordPresence, size: "sm", maxVisible: 3, showStatus: true })), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), _jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, headerSystemActions: synthSystemActions, isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, children: _jsx(HighlightFieldsProvider, { children: _jsx(DiscussionContextProvider, { items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, mentionSuggestions: mentionSuggestions, children: _jsxs(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, onResultDialog: resultDialogHandler, onModal: modalHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, approval: approvalHandler }, children: [_jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsxs("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: [originFrom?.pathname && originFrom?.label && (_jsxs(Link, { to: originFrom.pathname, className: "inline-flex items-center gap-1 mb-3 text-sm text-muted-foreground hover:text-foreground transition-colors", children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), _jsx("span", { children: originFrom.label })] })), _jsx(RelatedRecordActionsBridge, { appName: appName, objects: objects, dataSource: dataSource, actionLabel: actionLabel, children: _jsx(SchemaRenderer, { schema: renderedPage }) }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
|
|
1774
|
+
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [recordPresence.length > 0 && (_jsx(PresenceAvatars, { users: recordPresence, size: "sm", maxVisible: 3, showStatus: true })), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), _jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, headerSystemActions: synthSystemActions, isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, children: _jsx(HighlightFieldsProvider, { children: _jsx(DiscussionContextProvider, { items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction, mentionSuggestions: mentionSuggestions, children: _jsxs(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, onResultDialog: resultDialogHandler, onModal: modalHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, approval: approvalHandler }, children: [_jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsxs("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: [originFrom?.pathname && originFrom?.label && (_jsxs(Link, { to: originFrom.pathname, className: "inline-flex items-center gap-1 mb-3 text-sm text-muted-foreground hover:text-foreground transition-colors", children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), _jsx("span", { children: originFrom.label })] })), _jsx(RelatedRecordActionsBridge, { appName: appName, objects: objects, dataSource: dataSource, actionLabel: actionLabel, children: _jsx(SchemaRenderer, { schema: withPageTabsUrlSync(renderedPage, { defaultTab: activeTabParam, onTabChange: handleTabChange }) }) }), showAutoDiscussion && (_jsx("div", { className: "mt-6", children: _jsx(RecordChatterPanel, { config: {
|
|
1734
1775
|
position: 'bottom',
|
|
1735
1776
|
collapsible: false,
|
|
1736
1777
|
feed: {
|
|
@@ -1747,7 +1788,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1747
1788
|
} }), _jsx(ActionResultDialog, { state: resultDialogState, onAcknowledge: () => {
|
|
1748
1789
|
resultDialogState.resolve?.();
|
|
1749
1790
|
setResultDialogState({ open: false });
|
|
1750
|
-
} }), _jsx(FlowRunner, { state: screenFlow, authFetch: authFetch, baseUrl: import.meta.env.VITE_SERVER_URL || '', dataSource: dataSource, objects: objects, onClose: () => setScreenFlow(null), onComplete: () => { setScreenFlow(null);
|
|
1791
|
+
} }), _jsx(FlowRunner, { state: screenFlow, authFetch: authFetch, baseUrl: import.meta.env.VITE_SERVER_URL || '', dataSource: dataSource, objects: objects, onClose: () => setScreenFlow(null), onComplete: () => { setScreenFlow(null); notifyRecordChanged(); } })] }));
|
|
1751
1792
|
}
|
|
1752
1793
|
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [recordPresence.length > 0 && (_jsx(PresenceAvatars, { users: recordPresence, size: "sm", maxVisible: 3, showStatus: true })), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx("div", { className: "mx-auto w-full max-w-[1400px]", children: _jsxs(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, onResultDialog: resultDialogHandler, onModal: modalHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, approval: approvalHandler }, children: [_jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), isFavorite: isRecordFavorite, onToggleFavorite: favoriteRecord ? handleToggleRecordFavorite : undefined, onDataLoaded: (record) => {
|
|
1753
1794
|
if (!record || typeof record !== 'object')
|
|
@@ -1769,7 +1810,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1769
1810
|
enableThreading: true,
|
|
1770
1811
|
showCommentInput: true,
|
|
1771
1812
|
},
|
|
1772
|
-
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) }
|
|
1813
|
+
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) }), modalElement] }) }) }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'View Schema', data: detailSchema }] })] }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
|
|
1773
1814
|
if (!open)
|
|
1774
1815
|
setConfirmState(s => ({ ...s, open: false }));
|
|
1775
1816
|
} }), _jsx(ActionParamDialog, { state: paramState, onOpenChange: (open) => {
|
|
@@ -1778,5 +1819,5 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1778
1819
|
} }), _jsx(ActionResultDialog, { state: resultDialogState, onAcknowledge: () => {
|
|
1779
1820
|
resultDialogState.resolve?.();
|
|
1780
1821
|
setResultDialogState({ open: false });
|
|
1781
|
-
} }), _jsx(FlowRunner, { state: screenFlow, authFetch: authFetch, baseUrl: import.meta.env.VITE_SERVER_URL || '', dataSource: dataSource, objects: objects, onClose: () => setScreenFlow(null), onComplete: () => { setScreenFlow(null);
|
|
1822
|
+
} }), _jsx(FlowRunner, { state: screenFlow, authFetch: authFetch, baseUrl: import.meta.env.VITE_SERVER_URL || '', dataSource: dataSource, objects: objects, onClose: () => setScreenFlow(null), onComplete: () => { setScreenFlow(null); notifyRecordChanged(); } })] }));
|
|
1782
1823
|
}
|
|
@@ -5,7 +5,16 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Notify open related lists for `objectName` to refetch.
|
|
10
|
+
*
|
|
11
|
+
* Since #2269 this is a thin alias over the data-invalidation bus — the bus
|
|
12
|
+
* dispatches the legacy `objectui:related-changed` window event RelatedList
|
|
13
|
+
* listens for, plus every `useDataInvalidation` reader. Kept for callers
|
|
14
|
+
* whose writes BYPASS the dataSource (row actions over the ActionRunner);
|
|
15
|
+
* dataSource writes need no manual call (the MutationEvent bridge covers
|
|
16
|
+
* them).
|
|
17
|
+
*/
|
|
9
18
|
export declare function notifyRelatedChanged(objectName: string): void;
|
|
10
19
|
/** i18n label resolver signature (matches `useObjectLabel().actionLabel`). */
|
|
11
20
|
type ActionLabelFn = (objectName: string | undefined, actionName: string, fallback: string) => string;
|
|
@@ -16,10 +16,18 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
16
16
|
* (mounted inside the page's `ActionProvider`) closes that gap:
|
|
17
17
|
*
|
|
18
18
|
* - 查看详情 → navigate to the child record's detail route
|
|
19
|
-
* - 增
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* - 增 / 改 → open the child form as an OVERLAY on the parent detail
|
|
20
|
+
* (#2604 D3: a child task's return target is ALWAYS the parent
|
|
21
|
+
* detail with the subtable refreshed — never a route, which
|
|
22
|
+
* would drop the parent's scroll/tab context and refetch it).
|
|
23
|
+
* Implemented by pushing the console's record-form URL params
|
|
24
|
+
* (`?form=…&formObject=…&formLink=…`) — the ONE global record
|
|
25
|
+
* form overlay in `AppContent` picks them up, pre-links the
|
|
26
|
+
* parent from `formLink`, sizes to the CHILD object, and on
|
|
27
|
+
* save stays put + refetches the child's related lists
|
|
28
|
+
* (`notifyRelatedChanged`). URL-driven means browser Back
|
|
29
|
+
* closes the overlay and a refresh reopens it STILL correctly
|
|
30
|
+
* parent-linked.
|
|
23
31
|
* - 删 → `dataSource.delete(child, id)` (RelatedList shows the confirm
|
|
24
32
|
* dialog and refreshes afterwards)
|
|
25
33
|
* - 子对象 action → the child object's `list_item` actions, executed against
|
|
@@ -30,14 +38,22 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
30
38
|
* absent (e.g. the Studio designer) the related list stays read-only.
|
|
31
39
|
*/
|
|
32
40
|
import { useCallback, useMemo } from 'react';
|
|
33
|
-
import { useNavigate } from 'react-router-dom';
|
|
34
|
-
import { RelatedRecordActionsProvider, useAction, } from '@object-ui/react';
|
|
41
|
+
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
42
|
+
import { RelatedRecordActionsProvider, notifyDataChanged, useAction, } from '@object-ui/react';
|
|
35
43
|
import { resolveCrudAffordances } from '../utils/crudAffordances';
|
|
36
|
-
|
|
44
|
+
import { RECORD_FORM_PARAM, RECORD_FORM_OBJECT_PARAM, RECORD_FORM_LINK_PARAM } from '../urlParams';
|
|
45
|
+
/**
|
|
46
|
+
* Notify open related lists for `objectName` to refetch.
|
|
47
|
+
*
|
|
48
|
+
* Since #2269 this is a thin alias over the data-invalidation bus — the bus
|
|
49
|
+
* dispatches the legacy `objectui:related-changed` window event RelatedList
|
|
50
|
+
* listens for, plus every `useDataInvalidation` reader. Kept for callers
|
|
51
|
+
* whose writes BYPASS the dataSource (row actions over the ActionRunner);
|
|
52
|
+
* dataSource writes need no manual call (the MutationEvent bridge covers
|
|
53
|
+
* them).
|
|
54
|
+
*/
|
|
37
55
|
export function notifyRelatedChanged(objectName) {
|
|
38
|
-
|
|
39
|
-
return;
|
|
40
|
-
window.dispatchEvent(new CustomEvent('objectui:related-changed', { detail: { objectName } }));
|
|
56
|
+
notifyDataChanged({ objectName });
|
|
41
57
|
}
|
|
42
58
|
/**
|
|
43
59
|
* Derive the child object's row actions (metadata `actions` filtered to the
|
|
@@ -55,7 +71,22 @@ function deriveRowActions(childDef, actionLabel) {
|
|
|
55
71
|
export function RelatedRecordActionsBridge({ appName, objects, dataSource, actionLabel, children, }) {
|
|
56
72
|
const navigate = useNavigate();
|
|
57
73
|
const { execute } = useAction();
|
|
74
|
+
const [, setSearchParams] = useSearchParams();
|
|
58
75
|
const base = appName ? `/apps/${appName}` : '';
|
|
76
|
+
// #2604 D3 — open a child create/edit task as the console's global record
|
|
77
|
+
// form overlay, by URL params. Pushes ONE history entry (Back = close, the
|
|
78
|
+
// parent detail stays mounted underneath). The read side lives in
|
|
79
|
+
// `AppContent` (see its record-form URL contract).
|
|
80
|
+
const openChildForm = useCallback((opts) => {
|
|
81
|
+
const sp = new URLSearchParams(window.location.search);
|
|
82
|
+
sp.set(RECORD_FORM_PARAM, opts.recordId ?? 'new');
|
|
83
|
+
sp.set(RECORD_FORM_OBJECT_PARAM, opts.objectName);
|
|
84
|
+
if (opts.link)
|
|
85
|
+
sp.set(RECORD_FORM_LINK_PARAM, `${opts.link.field}:${opts.link.parentId}`);
|
|
86
|
+
else
|
|
87
|
+
sp.delete(RECORD_FORM_LINK_PARAM);
|
|
88
|
+
setSearchParams(sp); // push → Back closes the overlay
|
|
89
|
+
}, [setSearchParams]);
|
|
59
90
|
// Execute a child object's row action against the clicked record. Reuses the
|
|
60
91
|
// page's ActionRunner (confirm dialog, toast, param collection are handled by
|
|
61
92
|
// it) but retargets it at the CHILD object + row via the action's
|
|
@@ -88,14 +119,16 @@ export function RelatedRecordActionsBridge({ appName, objects, dataSource, actio
|
|
|
88
119
|
if (aff.create) {
|
|
89
120
|
handlers.onCreate = () => {
|
|
90
121
|
const canLink = relationshipField && parentId != null && parentId !== '';
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
:
|
|
94
|
-
|
|
122
|
+
openChildForm({
|
|
123
|
+
objectName,
|
|
124
|
+
link: canLink
|
|
125
|
+
? { field: relationshipField, parentId: parentId }
|
|
126
|
+
: undefined,
|
|
127
|
+
});
|
|
95
128
|
};
|
|
96
129
|
}
|
|
97
130
|
if (aff.edit) {
|
|
98
|
-
handlers.onEdit = (id) =>
|
|
131
|
+
handlers.onEdit = (id) => openChildForm({ objectName, recordId: String(id) });
|
|
99
132
|
}
|
|
100
133
|
if (aff.delete) {
|
|
101
134
|
handlers.onDelete = async (id) => {
|
|
@@ -109,6 +142,6 @@ export function RelatedRecordActionsBridge({ appName, objects, dataSource, actio
|
|
|
109
142
|
}
|
|
110
143
|
return handlers;
|
|
111
144
|
},
|
|
112
|
-
}), [objects, base, navigate, dataSource, actionLabel, runRowAction]);
|
|
145
|
+
}), [objects, base, navigate, dataSource, actionLabel, runRowAction, openChildForm]);
|
|
113
146
|
return (_jsx(RelatedRecordActionsProvider, { value: value, children: children }));
|
|
114
147
|
}
|
|
@@ -21,6 +21,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
21
21
|
*/
|
|
22
22
|
import * as React from 'react';
|
|
23
23
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
24
|
+
import { DESIGNER_SEL_PARAM, parseNavSelParam, formatNavSelParam, findNavPositionById, navIdAtPosition, } from './nav-selection';
|
|
24
25
|
import { Save, RotateCcw, Trash2, History, Link2, Loader2, AlertTriangle, Layers3, GitCompareArrows, Boxes, Eye, Pencil, X, PanelRightClose, PanelRightOpen, Maximize2, Minimize2, MousePointer2, SlidersHorizontal, FileCode2, Zap, ZapOff, Send, Undo2, Lock, ShieldCheck, } from 'lucide-react';
|
|
25
26
|
import { Button } from '@object-ui/components';
|
|
26
27
|
import { Badge } from '@object-ui/components';
|
|
@@ -357,6 +358,44 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
357
358
|
if (!editing)
|
|
358
359
|
setSelection(null);
|
|
359
360
|
}, [editing]);
|
|
361
|
+
// #2272 — designer deep-link: `?sel=nav:<id>` selects the nav item with
|
|
362
|
+
// that spec `id` (stable across reorders, unlike the positional selection
|
|
363
|
+
// ids the canvas/inspector exchange internally). Applied once per
|
|
364
|
+
// param/item; entering edit mode is implied — a selection is meaningless
|
|
365
|
+
// in the read-only state (the effect above would clear it).
|
|
366
|
+
const navSelParam = parseNavSelParam(searchParams.get(DESIGNER_SEL_PARAM));
|
|
367
|
+
const appliedNavSelRef = React.useRef(null);
|
|
368
|
+
React.useEffect(() => {
|
|
369
|
+
if (type !== 'app' || !navSelParam)
|
|
370
|
+
return;
|
|
371
|
+
if (appliedNavSelRef.current === `${name}:${navSelParam}`)
|
|
372
|
+
return;
|
|
373
|
+
if (!draft || Object.keys(draft).length === 0)
|
|
374
|
+
return;
|
|
375
|
+
const hit = findNavPositionById(draft, navSelParam);
|
|
376
|
+
if (!hit)
|
|
377
|
+
return;
|
|
378
|
+
appliedNavSelRef.current = `${name}:${navSelParam}`;
|
|
379
|
+
setEditing(true);
|
|
380
|
+
setSelection({ kind: 'nav', id: hit.selectionId, label: hit.label });
|
|
381
|
+
}, [type, name, navSelParam, draft]);
|
|
382
|
+
// Mirror nav selections back to the URL (replace — no history spam, same
|
|
383
|
+
// convention as ADR-0047 `uf_*`) so the designer's selected menu is
|
|
384
|
+
// shareable and survives reload. Non-nav selections clear the param.
|
|
385
|
+
React.useEffect(() => {
|
|
386
|
+
if (type !== 'app')
|
|
387
|
+
return;
|
|
388
|
+
const navId = selection?.kind === 'nav' ? navIdAtPosition(draft, selection.id) : null;
|
|
389
|
+
setSearchParams((prev) => {
|
|
390
|
+
const next = new URLSearchParams(prev);
|
|
391
|
+
if (navId)
|
|
392
|
+
next.set(DESIGNER_SEL_PARAM, formatNavSelParam(navId));
|
|
393
|
+
else
|
|
394
|
+
next.delete(DESIGNER_SEL_PARAM);
|
|
395
|
+
return next;
|
|
396
|
+
}, { replace: true });
|
|
397
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
398
|
+
}, [type, selection]);
|
|
360
399
|
// Snapshot of the last saved draft. Used by Cancel to revert in-flight
|
|
361
400
|
// edits, and as the source-of-truth when entering edit mode.
|
|
362
401
|
const draftSnapshotRef = React.useRef(null);
|