@object-ui/app-shell 11.3.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 +700 -0
- package/README.md +32 -0
- package/dist/chrome/KeyboardShortcutsDialog.js +2 -1
- package/dist/console/AppContent.js +145 -26
- package/dist/console/ConsoleShell.js +25 -3
- package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
- package/dist/console/home/CloudOnboardingNext.js +14 -4
- package/dist/console/home/HomePage.js +34 -7
- package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
- package/dist/console/organizations/OrganizationsPage.js +16 -7
- package/dist/context/CommandPaletteProvider.js +2 -1
- package/dist/hooks/useConsoleActionRuntime.js +32 -3
- package/dist/hooks/useObjectActions.js +16 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- 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 +31 -1
- package/dist/preview/DraftChangesPanel.js +146 -18
- 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/deriveRelatedLists.d.ts +20 -5
- package/dist/utils/deriveRelatedLists.js +31 -13
- 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/utils/resolveViewId.d.ts +23 -0
- package/dist/utils/resolveViewId.js +37 -0
- package/dist/utils/warnSuppressedListNav.d.ts +10 -0
- package/dist/utils/warnSuppressedListNav.js +40 -0
- package/dist/views/InterfaceListPage.d.ts +1 -0
- package/dist/views/InterfaceListPage.js +7 -5
- package/dist/views/ObjectDataPage.d.ts +29 -0
- package/dist/views/ObjectDataPage.js +227 -0
- package/dist/views/ObjectView.js +65 -13
- package/dist/views/RecordDetailView.js +191 -123
- package/dist/views/RecordFormPage.js +7 -1
- package/dist/views/RelatedRecordActionsBridge.d.ts +33 -0
- package/dist/views/RelatedRecordActionsBridge.js +147 -0
- package/dist/views/metadata-admin/PackagesPage.js +18 -7
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
- package/dist/views/metadata-admin/ResourceEditPage.js +39 -0
- package/dist/views/metadata-admin/i18n.d.ts +12 -21
- package/dist/views/metadata-admin/i18n.js +557 -6
- 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 +60 -30
- 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/permission-slice.d.ts +66 -0
- package/dist/views/metadata-admin/permission-slice.js +70 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.js +19 -7
- package/dist/views/metadata-admin/previews/AppPreview.js +4 -2
- package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
- package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
- package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
- package/dist/views/studio-design/BuilderLanding.js +126 -0
- package/dist/views/studio-design/ObjectFormDesigner.d.ts +33 -0
- package/dist/views/studio-design/ObjectFormDesigner.js +231 -0
- package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectSettingsPanel.js +46 -0
- package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectValidationsPanel.js +80 -0
- 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 +962 -145
- package/dist/views/studio-design/metadataError.d.ts +23 -0
- package/dist/views/studio-design/metadataError.js +44 -0
- 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 +38 -0
- package/dist/views/studio-design/packages-io.js +73 -0
- package/dist/views/studio-design/skeletons.d.ts +16 -0
- package/dist/views/studio-design/skeletons.js +51 -0
- package/package.json +42 -39
|
@@ -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';
|
|
11
|
-
import { DetailView, RecordChatterPanel, buildDefaultPageSchema, extractMentions } from '@object-ui/plugin-detail';
|
|
10
|
+
import { useParams, useNavigate, useLocation, useSearchParams, Link } from 'react-router-dom';
|
|
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';
|
|
@@ -26,6 +26,9 @@ import { ActionConfirmDialog } from './ActionConfirmDialog';
|
|
|
26
26
|
import { ActionParamDialog } from './ActionParamDialog';
|
|
27
27
|
import { ActionResultDialog } from './ActionResultDialog';
|
|
28
28
|
import { FlowRunner } from './FlowRunner';
|
|
29
|
+
import { RelatedRecordActionsBridge } from './RelatedRecordActionsBridge';
|
|
30
|
+
import { withPageTabsUrlSync } from '../utils/pageTabsUrlSync';
|
|
31
|
+
import { RECORD_DETAIL_TAB_PARAM } from '../urlParams';
|
|
29
32
|
import { resolveActionParams } from '../utils/resolveActionParams';
|
|
30
33
|
import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
|
|
31
34
|
import { useRecordApprovals } from '../hooks/useRecordApprovals';
|
|
@@ -47,9 +50,8 @@ const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'up
|
|
|
47
50
|
/**
|
|
48
51
|
* System/tenant fields that the framework auto-injects on every record but
|
|
49
52
|
* which carry no business value on a detail page. Hidden from the
|
|
50
|
-
* auto-generated
|
|
51
|
-
*
|
|
52
|
-
* `objectDef.views.form.sections`.
|
|
53
|
+
* auto-generated sections. Authors who really want to surface one can
|
|
54
|
+
* assign it to a `fieldGroups` group explicitly (explicit listing wins).
|
|
53
55
|
*/
|
|
54
56
|
const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
|
|
55
57
|
'organization_id', 'tenant_id', 'is_deleted', 'deleted_at',
|
|
@@ -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') {
|
|
@@ -359,38 +393,46 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
359
393
|
case 'opportunity_mark_lost':
|
|
360
394
|
await dataSource.update(objectName, pureRecordId, { stage: 'closed_lost', loss_reason: params.loss_reason });
|
|
361
395
|
break;
|
|
362
|
-
default:
|
|
363
|
-
// Generic: update record with collected params
|
|
364
|
-
|
|
396
|
+
default: {
|
|
397
|
+
// Generic: update record with collected params. Related-list row
|
|
398
|
+
// actions retarget a CHILD record via explicit `objectName`/`recordId`;
|
|
399
|
+
// otherwise the update falls back to this page's record.
|
|
400
|
+
const targetObject = action.objectName ?? objectName;
|
|
401
|
+
const targetId = action.recordId ?? pureRecordId;
|
|
402
|
+
const isThisRecord = targetObject === objectName && String(targetId) === String(pureRecordId);
|
|
403
|
+
if (Object.keys(params).length > 0 && targetObject && targetId != null) {
|
|
365
404
|
// Undoable single-record update: capture the changed fields' prior
|
|
366
405
|
// values from the loaded record so the success toast can offer Undo.
|
|
367
|
-
|
|
406
|
+
// Only this page's record has its prior values loaded, so child-row
|
|
407
|
+
// updates skip undo capture.
|
|
408
|
+
if (action.undoable && isThisRecord && pageRecord) {
|
|
368
409
|
const undoData = {};
|
|
369
410
|
for (const k of Object.keys(params))
|
|
370
411
|
undoData[k] = pageRecord[k] ?? null;
|
|
371
412
|
undo = {
|
|
372
|
-
id: `undo-${
|
|
413
|
+
id: `undo-${targetObject}-${targetId}-${Date.now()}`,
|
|
373
414
|
type: 'update',
|
|
374
|
-
objectName,
|
|
375
|
-
recordId: String(
|
|
415
|
+
objectName: targetObject,
|
|
416
|
+
recordId: String(targetId),
|
|
376
417
|
timestamp: Date.now(),
|
|
377
|
-
description: action.label || `Undo ${
|
|
418
|
+
description: action.label || `Undo ${targetObject}`,
|
|
378
419
|
undoData,
|
|
379
420
|
redoData: { ...params },
|
|
380
421
|
};
|
|
381
422
|
}
|
|
382
|
-
await dataSource.update(
|
|
423
|
+
await dataSource.update(targetObject, String(targetId), params);
|
|
383
424
|
}
|
|
384
425
|
break;
|
|
426
|
+
}
|
|
385
427
|
}
|
|
386
428
|
const shouldRefresh = action.refreshAfter === true;
|
|
387
429
|
if (shouldRefresh) {
|
|
388
|
-
|
|
430
|
+
notifyRecordChanged();
|
|
389
431
|
}
|
|
390
432
|
else if (undo) {
|
|
391
433
|
// Even when refreshAfter isn't set, reflect the change so the user sees
|
|
392
434
|
// it (and the subsequent Undo) on the open record.
|
|
393
|
-
|
|
435
|
+
notifyRecordChanged();
|
|
394
436
|
}
|
|
395
437
|
return { success: true, reload: shouldRefresh, undo };
|
|
396
438
|
}
|
|
@@ -419,8 +461,11 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
419
461
|
method: 'POST',
|
|
420
462
|
headers: { 'Content-Type': 'application/json' },
|
|
421
463
|
body: JSON.stringify({
|
|
422
|
-
|
|
423
|
-
objectName
|
|
464
|
+
// Related-list row actions retarget the flow at a CHILD record via
|
|
465
|
+
// an explicit `recordId` / `objectName`; fall back to this page's
|
|
466
|
+
// record when the action carries none (header/more actions).
|
|
467
|
+
recordId: action.recordId ?? pureRecordId,
|
|
468
|
+
objectName: action.objectName ?? objectName,
|
|
424
469
|
params: action.params ?? {},
|
|
425
470
|
}),
|
|
426
471
|
});
|
|
@@ -440,7 +485,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
440
485
|
}
|
|
441
486
|
const shouldRefresh = action.refreshAfter !== false;
|
|
442
487
|
if (shouldRefresh) {
|
|
443
|
-
|
|
488
|
+
notifyRecordChanged();
|
|
444
489
|
}
|
|
445
490
|
return { success: true, data: json?.data, reload: shouldRefresh };
|
|
446
491
|
}
|
|
@@ -554,14 +599,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
554
599
|
}
|
|
555
600
|
}
|
|
556
601
|
if (action.refreshAfter === true)
|
|
557
|
-
|
|
602
|
+
notifyRecordChanged();
|
|
558
603
|
return { success: true };
|
|
559
604
|
}
|
|
560
605
|
const obj = action.objectName || objectName || 'global';
|
|
561
606
|
const res = await authFetch(`${baseUrl}/api/v1/actions/${encodeURIComponent(obj)}/${encodeURIComponent(targetName)}`, {
|
|
562
607
|
method: 'POST',
|
|
563
608
|
headers: { 'Content-Type': 'application/json' },
|
|
564
|
-
|
|
609
|
+
// Related-list row actions retarget a CHILD record via explicit
|
|
610
|
+
// `recordId`; header/more actions carry none and use this page's id.
|
|
611
|
+
body: JSON.stringify({ recordId: action.recordId ?? pureRecordId, params }),
|
|
565
612
|
});
|
|
566
613
|
const json = await res.json().catch(() => null);
|
|
567
614
|
// The action route wraps the handler's return value in a {success, data}
|
|
@@ -580,14 +627,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
580
627
|
}
|
|
581
628
|
catch { /* ignore */ }
|
|
582
629
|
}
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
|
|
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.
|
|
586
635
|
return { success: false, error: errMsg };
|
|
587
636
|
}
|
|
588
637
|
const shouldRefresh = action.refreshAfter !== false;
|
|
589
638
|
if (shouldRefresh)
|
|
590
|
-
|
|
639
|
+
notifyRecordChanged();
|
|
591
640
|
const result = json?.data;
|
|
592
641
|
// ── redirectUrl convention ────────────────────────────────────────
|
|
593
642
|
// A script-action handler can return `{ redirectUrl: 'https://…' }`
|
|
@@ -653,7 +702,8 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
653
702
|
catch { /* ignore */ }
|
|
654
703
|
}
|
|
655
704
|
const msg = error.message;
|
|
656
|
-
toast
|
|
705
|
+
// Don't toast here — the ActionRunner post-execution hook toasts the
|
|
706
|
+
// returned `error` once (see the failure branch above).
|
|
657
707
|
return { success: false, error: msg };
|
|
658
708
|
}
|
|
659
709
|
finally {
|
|
@@ -686,7 +736,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
686
736
|
else {
|
|
687
737
|
return { success: false, error: `Unknown approval target: ${target}` };
|
|
688
738
|
}
|
|
689
|
-
|
|
739
|
+
notifyRecordChanged();
|
|
690
740
|
return { success: true, reload: true };
|
|
691
741
|
}
|
|
692
742
|
catch (err) {
|
|
@@ -1215,76 +1265,49 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1215
1265
|
if (!objectDef) {
|
|
1216
1266
|
return { type: 'detail-view' };
|
|
1217
1267
|
}
|
|
1218
|
-
// Auto-detect primary field: prefer objectDef metadata
|
|
1268
|
+
// Auto-detect primary field: prefer objectDef metadata — `primaryField`
|
|
1269
|
+
// (objectui-local override), then the spec-canonical `nameField` and its
|
|
1270
|
+
// deprecated `displayNameField` alias (ADR-0079) — then the 'name'/'title'
|
|
1271
|
+
// heuristic.
|
|
1219
1272
|
const primaryField = objectDef.primaryField
|
|
1273
|
+
|| objectDef.nameField
|
|
1274
|
+
|| objectDef.displayNameField
|
|
1220
1275
|
|| Object.keys(objectDef.fields || {}).find((key) => key === 'name' || key === 'title');
|
|
1221
|
-
// Build sections:
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
.map((f) => {
|
|
1239
|
-
const fieldName = typeof f === 'string' ? f : f.name;
|
|
1240
|
-
const fieldDef = objectDef.fields[fieldName];
|
|
1241
|
-
if (!fieldDef) {
|
|
1242
|
-
console.warn(`[RecordDetailView] Field "${fieldName}" not found in ${objectDef.name} definition`);
|
|
1243
|
-
return { name: fieldName, label: fieldName };
|
|
1244
|
-
}
|
|
1245
|
-
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
1246
|
-
return {
|
|
1247
|
-
name: fieldName,
|
|
1248
|
-
label: fieldDef.label || fieldName,
|
|
1249
|
-
type: fieldDef.type || 'text',
|
|
1250
|
-
...(fieldDef.options && { options: fieldDef.options }),
|
|
1251
|
-
...(refTarget && { reference_to: refTarget }),
|
|
1252
|
-
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
|
|
1253
|
-
...(fieldDef.currency && { currency: fieldDef.currency }),
|
|
1254
|
-
};
|
|
1255
|
-
}),
|
|
1256
|
-
}))
|
|
1257
|
-
: (() => {
|
|
1258
|
-
// Auto-grouping (platform B): when no form sections are authored,
|
|
1259
|
-
// split fields into a primary section and a collapsible
|
|
1260
|
-
// "More details" section so long-form/secondary fields don't
|
|
1261
|
-
// dilute the main grid. The primary section stays untitled so
|
|
1262
|
-
// DetailSection still flattens its chrome when alone.
|
|
1263
|
-
const allFields = Object.keys(objectDef.fields || {})
|
|
1264
|
-
.filter((key) => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden);
|
|
1265
|
-
const toField = (key) => {
|
|
1266
|
-
const fieldDef = objectDef.fields[key];
|
|
1267
|
-
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
1268
|
-
return {
|
|
1269
|
-
name: key,
|
|
1270
|
-
label: fieldDef.label || key,
|
|
1271
|
-
type: fieldDef.type || 'text',
|
|
1272
|
-
...(fieldDef.options && { options: fieldDef.options }),
|
|
1273
|
-
...(refTarget && { reference_to: refTarget }),
|
|
1274
|
-
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
|
|
1275
|
-
...(fieldDef.currency && { currency: fieldDef.currency }),
|
|
1276
|
-
};
|
|
1276
|
+
// Build sections (ADR-0085: grouping is the `fieldGroups` semantic
|
|
1277
|
+
// role — there is no per-surface sections override; per-page
|
|
1278
|
+
// customization goes through an assigned Page schema):
|
|
1279
|
+
// 1) sections derived from the object's `fieldGroups`;
|
|
1280
|
+
// 2) auto-grouping (primary + collapsible "More details").
|
|
1281
|
+
const sections = (() => {
|
|
1282
|
+
const toField = (key) => {
|
|
1283
|
+
const fieldDef = objectDef.fields[key];
|
|
1284
|
+
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
1285
|
+
return {
|
|
1286
|
+
name: key,
|
|
1287
|
+
label: fieldDef.label || key,
|
|
1288
|
+
type: fieldDef.type || 'text',
|
|
1289
|
+
...(fieldDef.options && { options: fieldDef.options }),
|
|
1290
|
+
...(refTarget && { reference_to: refTarget }),
|
|
1291
|
+
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
|
|
1292
|
+
...(fieldDef.currency && { currency: fieldDef.currency }),
|
|
1277
1293
|
};
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1294
|
+
};
|
|
1295
|
+
// Auto-grouping (platform B): split fields into a primary section
|
|
1296
|
+
// and a collapsible "More details" section so long-form/secondary
|
|
1297
|
+
// fields don't dilute the main grid. The primary section stays
|
|
1298
|
+
// untitled so DetailSection still flattens its chrome when alone.
|
|
1299
|
+
// Shared by the pure-fallback path and the ungrouped remainder of
|
|
1300
|
+
// the fieldGroups path below.
|
|
1301
|
+
const splitPrimarySecondary = (keys) => {
|
|
1302
|
+
const primaryKeys = keys.filter((k) => !isSecondaryField(k, objectDef.fields[k]));
|
|
1303
|
+
const secondaryKeys = keys.filter((k) => isSecondaryField(k, objectDef.fields[k]));
|
|
1304
|
+
// Keep the legacy single-untitled-section behaviour when the
|
|
1305
|
+
// split would leave one side empty.
|
|
1283
1306
|
if (secondaryKeys.length === 0 || primaryKeys.length === 0) {
|
|
1284
1307
|
return [
|
|
1285
1308
|
{
|
|
1286
1309
|
showBorder: false,
|
|
1287
|
-
fields:
|
|
1310
|
+
fields: keys.map(toField),
|
|
1288
1311
|
},
|
|
1289
1312
|
];
|
|
1290
1313
|
}
|
|
@@ -1302,7 +1325,31 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1302
1325
|
fields: secondaryKeys.map(toField),
|
|
1303
1326
|
},
|
|
1304
1327
|
];
|
|
1305
|
-
}
|
|
1328
|
+
};
|
|
1329
|
+
// 1) fieldGroups-derived sections (the ADR-0085 semantic role,
|
|
1330
|
+
// same shared derivation the runtime form honours). Declared
|
|
1331
|
+
// groups render as titled cards in declared order; the
|
|
1332
|
+
// trailing untitled bucket (ungrouped fields) still goes
|
|
1333
|
+
// through the primary/"More details" split so long-form
|
|
1334
|
+
// fields stay tucked away.
|
|
1335
|
+
const grouped = deriveFieldGroupDetailSections(objectDef);
|
|
1336
|
+
if (grouped) {
|
|
1337
|
+
return grouped.flatMap((sec) => {
|
|
1338
|
+
if (!sec.name) {
|
|
1339
|
+
return splitPrimarySecondary(sec.fields.map((f) => f.name));
|
|
1340
|
+
}
|
|
1341
|
+
return [{
|
|
1342
|
+
...sec,
|
|
1343
|
+
title: sectionLabel(objectDef.name, sec.name, sec.title),
|
|
1344
|
+
showBorder: true,
|
|
1345
|
+
}];
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
// 3) Pure auto-grouping fallback.
|
|
1349
|
+
const allFields = Object.keys(objectDef.fields || {})
|
|
1350
|
+
.filter((key) => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden);
|
|
1351
|
+
return splitPrimarySecondary(allFields);
|
|
1352
|
+
})();
|
|
1306
1353
|
// Audit fields (created_at/created_by/updated_at/updated_by) are NOT
|
|
1307
1354
|
// appended as a section here — they are surfaced by `<RecordMetaFooter>`
|
|
1308
1355
|
// (rendered by DetailView) as a single subtle line below the content,
|
|
@@ -1372,10 +1419,22 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1372
1419
|
}
|
|
1373
1420
|
return base;
|
|
1374
1421
|
})();
|
|
1375
|
-
// Build highlightFields
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
const
|
|
1422
|
+
// Build highlightFields from the object's semantic role (ADR-0085).
|
|
1423
|
+
// Bare field names resolve label/type from the field def.
|
|
1424
|
+
const rawHighlightFields = objectDef.highlightFields ?? [];
|
|
1425
|
+
const highlightFields = (Array.isArray(rawHighlightFields) ? rawHighlightFields : [])
|
|
1426
|
+
.map((f) => {
|
|
1427
|
+
const name = typeof f === 'string' ? f : f?.name;
|
|
1428
|
+
if (!name)
|
|
1429
|
+
return null;
|
|
1430
|
+
const fieldDef = objectDef.fields?.[name];
|
|
1431
|
+
return {
|
|
1432
|
+
name,
|
|
1433
|
+
label: fieldDef?.label || name,
|
|
1434
|
+
...(fieldDef?.type ? { type: fieldDef.type } : {}),
|
|
1435
|
+
};
|
|
1436
|
+
})
|
|
1437
|
+
.filter((f) => !!f);
|
|
1379
1438
|
// Build related entries from reverse-reference child objects.
|
|
1380
1439
|
// `referenceField` is the FK field on the child pointing back to this
|
|
1381
1440
|
// record — passed so the related-list renderer can hide the redundant
|
|
@@ -1468,9 +1527,10 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1468
1527
|
// Surface the child object's canonical display field so the
|
|
1469
1528
|
// right-rail can show meaningful labels (`user_agent`, `email`,
|
|
1470
1529
|
// …) instead of opaque IDs like `kCc8mhJr0bRs0r9Ykd09…`.
|
|
1471
|
-
displayField: childObjectDef?.
|
|
1472
|
-
|
|
1473
|
-
|
|
1530
|
+
displayField: childObjectDef?.nameField ||
|
|
1531
|
+
childObjectDef?.displayNameField ||
|
|
1532
|
+
(Array.isArray(childObjectDef?.highlightFields)
|
|
1533
|
+
? childObjectDef.highlightFields[0]
|
|
1474
1534
|
: undefined),
|
|
1475
1535
|
onNew,
|
|
1476
1536
|
onViewAll,
|
|
@@ -1496,6 +1556,10 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1496
1556
|
primaryField,
|
|
1497
1557
|
sections,
|
|
1498
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,
|
|
1499
1563
|
autoDiscoverRelated: true,
|
|
1500
1564
|
...(historyEnabled && {
|
|
1501
1565
|
history: {
|
|
@@ -1505,7 +1569,6 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1505
1569
|
}),
|
|
1506
1570
|
...(related.length > 0 && { related }),
|
|
1507
1571
|
...(highlightFields.length > 0 && { highlightFields }),
|
|
1508
|
-
...(sectionGroups && sectionGroups.length > 0 && { sectionGroups }),
|
|
1509
1572
|
...(recordHeaderActions.length > 0 && {
|
|
1510
1573
|
actions: [{
|
|
1511
1574
|
type: 'action:bar',
|
|
@@ -1515,7 +1578,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1515
1578
|
}),
|
|
1516
1579
|
};
|
|
1517
1580
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1518
|
-
}, [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]);
|
|
1519
1582
|
if (isLoading) {
|
|
1520
1583
|
return _jsx(SkeletonDetail, {});
|
|
1521
1584
|
}
|
|
@@ -1666,14 +1729,21 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1666
1729
|
const synthRelated = Array.isArray(detailSchema.related)
|
|
1667
1730
|
? detailSchema.related
|
|
1668
1731
|
.filter((r) => r?.api && r?.referenceField)
|
|
1669
|
-
.map((r) =>
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1732
|
+
.map((r) => {
|
|
1733
|
+
// Carry the `relatedList: 'primary'` prominence flag from the derived
|
|
1734
|
+
// relationship graph. Matched by (childObject, referenceField) — the
|
|
1735
|
+
// unique key of a related list — so it is robust to ordering/filtering.
|
|
1736
|
+
const derived = childRelations.find((c) => c.childObject === r.api && c.referenceField === r.referenceField);
|
|
1737
|
+
return {
|
|
1738
|
+
title: r.title,
|
|
1739
|
+
objectName: r.api,
|
|
1740
|
+
relationshipField: r.referenceField,
|
|
1741
|
+
...(Array.isArray(r.columns) ? { columns: r.columns } : {}),
|
|
1742
|
+
...(typeof r.pageSize === 'number' ? { limit: r.pageSize } : {}),
|
|
1743
|
+
...(r.icon ? { icon: r.icon } : {}),
|
|
1744
|
+
...(derived?.isPrimary ? { isPrimary: true } : {}),
|
|
1745
|
+
};
|
|
1746
|
+
})
|
|
1677
1747
|
: undefined;
|
|
1678
1748
|
const synthHistory = detailSchema.history
|
|
1679
1749
|
? {
|
|
@@ -1694,16 +1764,14 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1694
1764
|
headerActions: synthHeaderActions,
|
|
1695
1765
|
related: synthRelated,
|
|
1696
1766
|
history: synthHistory,
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
//
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
hideRelatedTab: objectDef?.detail?.hideRelatedTab === true || undefined,
|
|
1703
|
-
relatedLayout: objectDef?.detail?.relatedLayout === 'tabs' ? 'tabs' : undefined,
|
|
1767
|
+
// ADR-0085 removed the per-object `detail.*` presentation
|
|
1768
|
+
// toggles (show/hideReferenceRail, hideRelatedTab, relatedLayout)
|
|
1769
|
+
// — the synth defaults apply; per-page layout goes through an
|
|
1770
|
+
// assigned Page schema (`record:reference_rail` stays available
|
|
1771
|
+
// there as a renderer capability).
|
|
1704
1772
|
...(assignedSlots ? { slots: assignedSlots } : {}),
|
|
1705
1773
|
});
|
|
1706
|
-
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(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: {
|
|
1707
1775
|
position: 'bottom',
|
|
1708
1776
|
collapsible: false,
|
|
1709
1777
|
feed: {
|
|
@@ -1720,7 +1788,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1720
1788
|
} }), _jsx(ActionResultDialog, { state: resultDialogState, onAcknowledge: () => {
|
|
1721
1789
|
resultDialogState.resolve?.();
|
|
1722
1790
|
setResultDialogState({ open: false });
|
|
1723
|
-
} }), _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(); } })] }));
|
|
1724
1792
|
}
|
|
1725
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) => {
|
|
1726
1794
|
if (!record || typeof record !== 'object')
|
|
@@ -1742,7 +1810,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1742
1810
|
enableThreading: true,
|
|
1743
1811
|
showCommentInput: true,
|
|
1744
1812
|
},
|
|
1745
|
-
}, 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) => {
|
|
1746
1814
|
if (!open)
|
|
1747
1815
|
setConfirmState(s => ({ ...s, open: false }));
|
|
1748
1816
|
} }), _jsx(ActionParamDialog, { state: paramState, onOpenChange: (open) => {
|
|
@@ -1751,5 +1819,5 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1751
1819
|
} }), _jsx(ActionResultDialog, { state: resultDialogState, onAcknowledge: () => {
|
|
1752
1820
|
resultDialogState.resolve?.();
|
|
1753
1821
|
setResultDialogState({ open: false });
|
|
1754
|
-
} }), _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(); } })] }));
|
|
1755
1823
|
}
|
|
@@ -181,7 +181,13 @@ export function RecordFormPage({ mode }) {
|
|
|
181
181
|
// `simple` here (see the form-layout-vs-presentation modelling note in #1890).
|
|
182
182
|
const formDef = objectDef.form ?? objectDef.formViews?.default ?? {};
|
|
183
183
|
const pageFormType = ['tabbed', 'wizard', 'split'].includes(formDef.type) ? formDef.type : 'simple';
|
|
184
|
-
|
|
184
|
+
// Curated `sections` are wired through for EVERY layout family — a `simple`
|
|
185
|
+
// form view's sections still carry the authored field selection, order,
|
|
186
|
+
// grouping and per-field `visibleOn` predicates (#2212). Dropping them for
|
|
187
|
+
// `simple` made the page-mode form fall back to the raw schema (every field,
|
|
188
|
+
// no conditional visibility) while the New/Edit modal honored the same view
|
|
189
|
+
// via resolveFormViewLayout.
|
|
190
|
+
const formLayoutProps = Array.isArray(formDef.sections) && formDef.sections.length > 0
|
|
185
191
|
? {
|
|
186
192
|
sections: formDef.sections,
|
|
187
193
|
defaultTab: formDef.defaultTab,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
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
|
+
*/
|
|
18
|
+
export declare function notifyRelatedChanged(objectName: string): void;
|
|
19
|
+
/** i18n label resolver signature (matches `useObjectLabel().actionLabel`). */
|
|
20
|
+
type ActionLabelFn = (objectName: string | undefined, actionName: string, fallback: string) => string;
|
|
21
|
+
export interface RelatedRecordActionsBridgeProps {
|
|
22
|
+
/** Current app segment used to build `/apps/:appName/...` routes. */
|
|
23
|
+
appName?: string;
|
|
24
|
+
/** All object definitions (to resolve the child object + its actions). */
|
|
25
|
+
objects: any[];
|
|
26
|
+
/** Data source for delete + action dispatch. */
|
|
27
|
+
dataSource: any;
|
|
28
|
+
/** Localizes a child action's label (falls back to the raw label). */
|
|
29
|
+
actionLabel: ActionLabelFn;
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
export declare function RelatedRecordActionsBridge({ appName, objects, dataSource, actionLabel, children, }: RelatedRecordActionsBridgeProps): import("react").JSX.Element;
|
|
33
|
+
export {};
|