@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* ObjectUI
|
|
4
4
|
* Copyright (c) 2024-present ObjectStack Inc.
|
|
@@ -11,19 +11,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
* user commits. Lists every pending ADR-0033 draft grouped by metadata type,
|
|
12
12
|
* and classifies each as NEW (no published version exists — publishing adds
|
|
13
13
|
* it) or UPDATE (a published version exists — publishing overwrites it).
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Each entry expands into a field-level diff (objects) / changed-key summary
|
|
15
|
+
* (everything else), lazily fetched on first expand. This is the review
|
|
16
|
+
* surface that turns Publish from a leap of faith into an informed click.
|
|
16
17
|
*
|
|
17
|
-
* Read-only: fetches `_drafts` +
|
|
18
|
-
* never writes.
|
|
18
|
+
* Read-only by default: fetches `_drafts` + published lists on open, and
|
|
19
|
+
* never writes. When the caller passes `onPublish`, the panel additionally
|
|
20
|
+
* renders a confirm footer — review-then-publish in one surface — but the
|
|
21
|
+
* publish action itself still belongs to the caller.
|
|
19
22
|
*/
|
|
20
23
|
import { useCallback, useEffect, useState } from 'react';
|
|
21
|
-
import { FilePlus2, FilePen, Loader2 } from 'lucide-react';
|
|
22
|
-
import { Badge, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from '@object-ui/components';
|
|
24
|
+
import { ChevronDown, ChevronRight, FilePlus2, FilePen, Loader2, Rocket } from 'lucide-react';
|
|
25
|
+
import { Badge, Button, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from '@object-ui/components';
|
|
23
26
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
27
|
+
import { diffFields } from '../views/metadata-admin/previews/object-fields-io';
|
|
24
28
|
/** Pending drafts straight from the ADR-0033 `_drafts` endpoint. */
|
|
25
|
-
async function listPendingDrafts() {
|
|
26
|
-
const
|
|
29
|
+
async function listPendingDrafts(packageId) {
|
|
30
|
+
const qs = packageId ? `?packageId=${encodeURIComponent(packageId)}` : '';
|
|
31
|
+
const res = await fetch(`/api/v1/meta/_drafts${qs}`, {
|
|
27
32
|
credentials: 'include',
|
|
28
33
|
headers: { Accept: 'application/json' },
|
|
29
34
|
cache: 'no-store',
|
|
@@ -61,15 +66,131 @@ async function publishedNamesOf(type) {
|
|
|
61
66
|
.map((it) => (typeof it?.name === 'string' ? it.name : null))
|
|
62
67
|
.filter((n) => n !== null));
|
|
63
68
|
}
|
|
64
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Some framework reads wrap the body in a `{ type, name, item }` envelope
|
|
71
|
+
* (draft reads do; published reads return the bare body). Unwrap defensively.
|
|
72
|
+
*/
|
|
73
|
+
function unwrapItem(payload) {
|
|
74
|
+
if (!payload || typeof payload !== 'object')
|
|
75
|
+
return null;
|
|
76
|
+
const p = payload;
|
|
77
|
+
if (p.item && typeof p.item === 'object')
|
|
78
|
+
return p.item;
|
|
79
|
+
return p;
|
|
80
|
+
}
|
|
81
|
+
async function fetchItemBody(type, name, opts = {}) {
|
|
82
|
+
const params = [];
|
|
83
|
+
if (opts.draft)
|
|
84
|
+
params.push('state=draft');
|
|
85
|
+
if (opts.packageId)
|
|
86
|
+
params.push(`package=${encodeURIComponent(opts.packageId)}`);
|
|
87
|
+
const qs = params.length ? `?${params.join('&')}` : '';
|
|
88
|
+
const res = await fetch(`/api/v1/meta/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`, { credentials: 'include', headers: { Accept: 'application/json' }, cache: 'no-store' });
|
|
89
|
+
if (res.status === 404)
|
|
90
|
+
return null;
|
|
91
|
+
if (!res.ok)
|
|
92
|
+
throw new Error(`HTTP ${res.status}`);
|
|
93
|
+
return unwrapItem(await res.json());
|
|
94
|
+
}
|
|
95
|
+
/** Stable equality for metadata values (small JSON — order-sensitive is fine). */
|
|
96
|
+
function valueEqual(a, b) {
|
|
97
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* What publishing this draft actually changes, computed client-side from the
|
|
101
|
+
* published body (null when the item is NEW) and the pending draft body.
|
|
102
|
+
* `fields` gets the dedicated designer diff; every other top-level key is
|
|
103
|
+
* compared wholesale — enough to answer "which parts of this item move".
|
|
104
|
+
*/
|
|
105
|
+
export function computeChangeDetail(published, draft) {
|
|
106
|
+
const pub = published ?? {};
|
|
107
|
+
const cur = draft ?? {};
|
|
108
|
+
let fields = null;
|
|
109
|
+
if (pub.fields != null || cur.fields != null) {
|
|
110
|
+
const d = diffFields(pub.fields, cur.fields);
|
|
111
|
+
fields = {
|
|
112
|
+
added: Object.values(d.byName)
|
|
113
|
+
.filter((e) => e.status === 'added')
|
|
114
|
+
.map((e) => e.name)
|
|
115
|
+
.sort(),
|
|
116
|
+
changed: Object.values(d.byName)
|
|
117
|
+
.filter((e) => e.status === 'changed')
|
|
118
|
+
.map((e) => ({ name: e.name, keys: e.changedKeys }))
|
|
119
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
120
|
+
removed: d.removed.map((e) => e.name).sort(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const keys = new Set([...Object.keys(pub), ...Object.keys(cur)]);
|
|
124
|
+
keys.delete('fields');
|
|
125
|
+
const changedKeys = [...keys]
|
|
126
|
+
.filter((k) => !valueEqual(pub[k], cur[k]))
|
|
127
|
+
.sort();
|
|
128
|
+
return { fields, changedKeys };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Lazily-loaded drill-in for one draft entry: published vs draft, rendered as
|
|
132
|
+
* added / changed / removed field rows plus a changed-top-level-keys strip.
|
|
133
|
+
*/
|
|
134
|
+
function EntryDetail({ entry }) {
|
|
135
|
+
const { t } = useObjectTranslation();
|
|
136
|
+
const [detail, setDetail] = useState(null);
|
|
137
|
+
const [error, setError] = useState(null);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
let cancelled = false;
|
|
140
|
+
(async () => {
|
|
141
|
+
try {
|
|
142
|
+
const [published, draft] = await Promise.all([
|
|
143
|
+
// A NEW item 404s on the published read — that's data, not an error.
|
|
144
|
+
fetchItemBody(entry.type, entry.name, { packageId: entry.packageId }),
|
|
145
|
+
fetchItemBody(entry.type, entry.name, { draft: true, packageId: entry.packageId }),
|
|
146
|
+
]);
|
|
147
|
+
if (!cancelled)
|
|
148
|
+
setDetail(computeChangeDetail(published, draft));
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (!cancelled)
|
|
152
|
+
setError(e.message);
|
|
153
|
+
}
|
|
154
|
+
})();
|
|
155
|
+
return () => {
|
|
156
|
+
cancelled = true;
|
|
157
|
+
};
|
|
158
|
+
}, [entry.type, entry.name, entry.packageId]);
|
|
159
|
+
if (error) {
|
|
160
|
+
return (_jsxs("p", { className: "px-2 py-1 text-xs text-destructive", children: [t('preview.changes.detailLoadFailed', { defaultValue: 'Could not load change detail:' }), ' ', error] }));
|
|
161
|
+
}
|
|
162
|
+
if (!detail) {
|
|
163
|
+
return (_jsxs("p", { className: "flex items-center gap-1.5 px-2 py-1 text-xs text-muted-foreground", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), t('preview.changes.detailLoading', { defaultValue: 'Loading detail…' })] }));
|
|
164
|
+
}
|
|
165
|
+
const { fields, changedKeys } = detail;
|
|
166
|
+
const hasFieldRows = !!fields && (fields.added.length > 0 || fields.changed.length > 0 || fields.removed.length > 0);
|
|
167
|
+
if (!hasFieldRows && changedKeys.length === 0) {
|
|
168
|
+
return (_jsx("p", { className: "px-2 py-1 text-xs text-muted-foreground", children: t('preview.changes.detailNone', {
|
|
169
|
+
defaultValue: 'No differences detected — the draft matches the published version.',
|
|
170
|
+
}) }));
|
|
171
|
+
}
|
|
172
|
+
return (_jsxs("div", { className: "flex flex-col gap-0.5 px-2 py-1", "data-testid": "draft-entry-detail", children: [fields?.added.map((name) => (_jsxs("p", { className: "font-mono text-xs text-emerald-700 dark:text-emerald-400", children: ["+ ", name] }, `+${name}`))), fields?.changed.map((f) => (_jsxs("p", { className: "font-mono text-xs text-amber-700 dark:text-amber-400", children: ["~ ", f.name, f.keys.length > 0 && _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", f.keys.join(', ')] })] }, `~${f.name}`))), fields?.removed.map((name) => (_jsxs("p", { className: "font-mono text-xs text-red-700 line-through dark:text-red-400", children: ["\u2212 ", name] }, `-${name}`))), changedKeys.length > 0 && (_jsxs("p", { className: "text-xs text-muted-foreground", children: [t('preview.changes.detailChangedKeys', { defaultValue: 'Also changed:' }), ' ', _jsx("span", { className: "font-mono", children: changedKeys.join(', ') })] }))] }));
|
|
173
|
+
}
|
|
174
|
+
export function DraftChangesPanel({ open, onOpenChange, packageId, onPublish, publishing = false, }) {
|
|
65
175
|
const { t } = useObjectTranslation();
|
|
66
176
|
const [entries, setEntries] = useState(null);
|
|
67
177
|
const [error, setError] = useState(null);
|
|
178
|
+
const [expanded, setExpanded] = useState(new Set());
|
|
179
|
+
const toggleExpanded = useCallback((key) => {
|
|
180
|
+
setExpanded((prev) => {
|
|
181
|
+
const next = new Set(prev);
|
|
182
|
+
if (next.has(key))
|
|
183
|
+
next.delete(key);
|
|
184
|
+
else
|
|
185
|
+
next.add(key);
|
|
186
|
+
return next;
|
|
187
|
+
});
|
|
188
|
+
}, []);
|
|
68
189
|
const load = useCallback(async () => {
|
|
69
190
|
setEntries(null);
|
|
70
191
|
setError(null);
|
|
71
192
|
try {
|
|
72
|
-
const drafts = await listPendingDrafts();
|
|
193
|
+
const drafts = await listPendingDrafts(packageId);
|
|
73
194
|
setEntries(drafts);
|
|
74
195
|
// Classify new-vs-update per TYPE: one published-list read covers every
|
|
75
196
|
// draft of that type. A type whose read fails stays unclassified
|
|
@@ -93,7 +214,7 @@ export function DraftChangesPanel({ open, onOpenChange }) {
|
|
|
93
214
|
catch (e) {
|
|
94
215
|
setError(e.message);
|
|
95
216
|
}
|
|
96
|
-
}, []);
|
|
217
|
+
}, [packageId]);
|
|
97
218
|
useEffect(() => {
|
|
98
219
|
if (open)
|
|
99
220
|
void load();
|
|
@@ -104,11 +225,18 @@ export function DraftChangesPanel({ open, onOpenChange }) {
|
|
|
104
225
|
bucket.push(entry);
|
|
105
226
|
byType.set(entry.type, bucket);
|
|
106
227
|
}
|
|
107
|
-
return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", className: "w-[420px] sm:max-w-[420px]", "data-testid": "draft-changes-panel", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: t('preview.changes.title', { defaultValue: 'Pending changes' }) }), _jsx(SheetDescription, { children: t('preview.changes.description', {
|
|
228
|
+
return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", className: "flex w-[420px] flex-col sm:max-w-[420px]", "data-testid": "draft-changes-panel", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: t('preview.changes.title', { defaultValue: 'Pending changes' }) }), _jsx(SheetDescription, { children: t('preview.changes.description', {
|
|
108
229
|
defaultValue: 'What publishing will change. New items are added; updates overwrite the live version.',
|
|
109
|
-
}) })] }), _jsx("div", { className: "mt-4 flex flex-col gap-4 overflow-y-auto px-4 pb-6", children: error ? (_jsxs("p", { className: "text-sm text-destructive", children: [t('preview.changes.loadFailed', { defaultValue: 'Could not load pending changes:' }), ' ', error] })) : entries === null ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), t('preview.changes.loading', { defaultValue: 'Loading pending changes…' })] })) : entries.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('preview.changes.empty', { defaultValue: 'Nothing pending — every draft has been published.' }) })) : ([...byType.entries()].map(([type, items]) => (_jsxs("div", { children: [_jsxs("h4", { className: "mb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: [type, " \u00B7 ", items.length] }), _jsx("ul", { className: "flex flex-col gap-1", children: items.map((entry) =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
230
|
+
}) })] }), _jsx("div", { className: "mt-4 flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-4 pb-6", children: error ? (_jsxs("p", { className: "text-sm text-destructive", children: [t('preview.changes.loadFailed', { defaultValue: 'Could not load pending changes:' }), ' ', error] })) : entries === null ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), t('preview.changes.loading', { defaultValue: 'Loading pending changes…' })] })) : entries.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('preview.changes.empty', { defaultValue: 'Nothing pending — every draft has been published.' }) })) : ([...byType.entries()].map(([type, items]) => (_jsxs("div", { children: [_jsxs("h4", { className: "mb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: [type, " \u00B7 ", items.length] }), _jsx("ul", { className: "flex flex-col gap-1", children: items.map((entry) => {
|
|
231
|
+
const key = `${entry.type}:${entry.name}`;
|
|
232
|
+
const isExpanded = expanded.has(key);
|
|
233
|
+
return (_jsxs("li", { className: "rounded-md border text-sm", children: [_jsxs("button", { type: "button", onClick: () => toggleExpanded(key), "aria-expanded": isExpanded, className: "flex w-full items-center gap-2 px-2.5 py-1.5 text-left hover:bg-muted/50", "data-testid": "draft-entry-toggle", children: [isExpanded ? (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-3 w-3 shrink-0 text-muted-foreground" })), entry.kind === 'new' ? (_jsx(FilePlus2, { className: "h-3.5 w-3.5 shrink-0 text-emerald-600" })) : entry.kind === 'update' ? (_jsx(FilePen, { className: "h-3.5 w-3.5 shrink-0 text-amber-600" })) : (_jsx(Loader2, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground" })), _jsx("span", { className: "min-w-0 flex-1 truncate font-mono text-xs", children: entry.name }), entry.kind ? (_jsx(Badge, { variant: "outline", className: entry.kind === 'new'
|
|
234
|
+
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
|
235
|
+
: 'border-amber-200 bg-amber-50 text-amber-700', children: entry.kind === 'new'
|
|
236
|
+
? t('preview.changes.kindNew', { defaultValue: 'New' })
|
|
237
|
+
: t('preview.changes.kindUpdate', { defaultValue: 'Update' }) })) : null] }), isExpanded && (_jsx("div", { className: "border-t bg-muted/20", children: _jsx(EntryDetail, { entry: entry }) }))] }, key));
|
|
238
|
+
}) })] }, type)))) }), onPublish && (entries?.length ?? 0) > 0 && !error && (_jsxs("div", { className: "mt-auto flex flex-col gap-2 border-t px-4 py-3", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: t('preview.changes.confirmNote', {
|
|
239
|
+
count: entries.length,
|
|
240
|
+
defaultValue: 'Publishing releases all {{count}} pending drafts of this package atomically.',
|
|
241
|
+
}) }), _jsxs(Button, { size: "sm", onClick: () => void onPublish(), disabled: publishing, "data-testid": "draft-changes-publish", children: [publishing ? (_jsx(Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" })) : (_jsx(Rocket, { className: "mr-1.5 h-3.5 w-3.5" })), t('preview.changes.publishConfirm', { defaultValue: 'Publish all' })] })] }))] }) }));
|
|
114
242
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
* urlParams — the single registry of RESERVED console URL query params
|
|
10
|
+
* (objectui#2269 P3; ADR-0054 C3 "URL-addressable state").
|
|
11
|
+
*
|
|
12
|
+
* These params form the console's cross-route URL contract: they are read
|
|
13
|
+
* and written by app-shell chrome (overlays, record surfaces, tabs) and must
|
|
14
|
+
* never be repurposed by a page/view for something else. Every reader/writer
|
|
15
|
+
* imports the constant from here — no string literals — so the contract has
|
|
16
|
+
* ONE definition, collisions are caught at review time, and an AI author
|
|
17
|
+
* (north star: all metadata is AI-authored) has a single place to learn it.
|
|
18
|
+
*
|
|
19
|
+
* | Param | Meaning | History |
|
|
20
|
+
* |-------------|------------------------------------------------|--------------|
|
|
21
|
+
* | `recordId` | Record detail DRAWER over a list (light | push (open) |
|
|
22
|
+
* | | objects; heavy ones use the `/record/:id` | |
|
|
23
|
+
* | | route instead). URL is the drawer's source of | |
|
|
24
|
+
* | | truth. | |
|
|
25
|
+
* | `form` | The global record-form overlay: `new` = create,| |
|
|
26
|
+
* | | a record id = edit (framework#2604 D1/D2). | push (open), |
|
|
27
|
+
* | | Back closes the overlay. | replace (close) |
|
|
28
|
+
* | `formObject`| Child-task override for `form`: the object the | |
|
|
29
|
+
* | | overlay edits when it is NOT the route's | with `form` |
|
|
30
|
+
* | | object (subtable child over a parent detail, | |
|
|
31
|
+
* | | framework#2604 D3). | |
|
|
32
|
+
* | `formLink` | `"<fkField>:<parentId>"` — create-mode parent | |
|
|
33
|
+
* | | pre-link for a child task; refresh-safe. | with `form` |
|
|
34
|
+
* | `tab` | Active record-detail tab (stable semantic | replace |
|
|
35
|
+
* | | values: `details` / `related:<child>` / | |
|
|
36
|
+
* | | `related` / `activity` / `history`, | |
|
|
37
|
+
* | | objectui#2257). Never stacks history. | |
|
|
38
|
+
* | `palette` | Command palette overlay (alias `cmdk`). | replace |
|
|
39
|
+
* | `shortcuts` | Keyboard-shortcuts dialog overlay. | replace |
|
|
40
|
+
*
|
|
41
|
+
* Push-vs-replace rule of thumb: an overlay the user OPENED (form, drawer)
|
|
42
|
+
* pushes one entry so browser Back closes it; passive state that tracks an
|
|
43
|
+
* in-page selection (tab) or transient chrome (palette) replaces, so Back
|
|
44
|
+
* never pages through it.
|
|
45
|
+
*
|
|
46
|
+
* Page-scoped params (`q`, `limit`, `offset`, `view`, `type`, `review`,
|
|
47
|
+
* `package`, …) belong to their page's own contract and are NOT reserved
|
|
48
|
+
* here — but they must not collide with the names above.
|
|
49
|
+
*/
|
|
50
|
+
/** Record detail drawer over a list (`?recordId=<id>`). */
|
|
51
|
+
export declare const RECORD_DRAWER_PARAM = "recordId";
|
|
52
|
+
/** Global record-form overlay: `new` | `<recordId>` (framework#2604). */
|
|
53
|
+
export declare const RECORD_FORM_PARAM = "form";
|
|
54
|
+
/** Child-task object override for the record-form overlay (#2604 D3). */
|
|
55
|
+
export declare const RECORD_FORM_OBJECT_PARAM = "formObject";
|
|
56
|
+
/** Child-task parent pre-link `"<fkField>:<parentId>"` (#2604 D3). */
|
|
57
|
+
export declare const RECORD_FORM_LINK_PARAM = "formLink";
|
|
58
|
+
/** Active record-detail tab (objectui#2257; stable semantic values). */
|
|
59
|
+
export declare const RECORD_DETAIL_TAB_PARAM = "tab";
|
|
60
|
+
/** Command palette overlay (ADR-0054 Phase 1; alias `cmdk`). */
|
|
61
|
+
export declare const COMMAND_PALETTE_PARAM = "palette";
|
|
62
|
+
/** Keyboard-shortcuts dialog overlay. */
|
|
63
|
+
export declare const KEYBOARD_SHORTCUTS_PARAM = "shortcuts";
|
|
64
|
+
/**
|
|
65
|
+
* All reserved params, for collision checks (e.g. a lint or a dev-time
|
|
66
|
+
* assertion that a page-scoped param doesn't shadow the console contract).
|
|
67
|
+
*/
|
|
68
|
+
export declare const RESERVED_URL_PARAMS: readonly string[];
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
* urlParams — the single registry of RESERVED console URL query params
|
|
10
|
+
* (objectui#2269 P3; ADR-0054 C3 "URL-addressable state").
|
|
11
|
+
*
|
|
12
|
+
* These params form the console's cross-route URL contract: they are read
|
|
13
|
+
* and written by app-shell chrome (overlays, record surfaces, tabs) and must
|
|
14
|
+
* never be repurposed by a page/view for something else. Every reader/writer
|
|
15
|
+
* imports the constant from here — no string literals — so the contract has
|
|
16
|
+
* ONE definition, collisions are caught at review time, and an AI author
|
|
17
|
+
* (north star: all metadata is AI-authored) has a single place to learn it.
|
|
18
|
+
*
|
|
19
|
+
* | Param | Meaning | History |
|
|
20
|
+
* |-------------|------------------------------------------------|--------------|
|
|
21
|
+
* | `recordId` | Record detail DRAWER over a list (light | push (open) |
|
|
22
|
+
* | | objects; heavy ones use the `/record/:id` | |
|
|
23
|
+
* | | route instead). URL is the drawer's source of | |
|
|
24
|
+
* | | truth. | |
|
|
25
|
+
* | `form` | The global record-form overlay: `new` = create,| |
|
|
26
|
+
* | | a record id = edit (framework#2604 D1/D2). | push (open), |
|
|
27
|
+
* | | Back closes the overlay. | replace (close) |
|
|
28
|
+
* | `formObject`| Child-task override for `form`: the object the | |
|
|
29
|
+
* | | overlay edits when it is NOT the route's | with `form` |
|
|
30
|
+
* | | object (subtable child over a parent detail, | |
|
|
31
|
+
* | | framework#2604 D3). | |
|
|
32
|
+
* | `formLink` | `"<fkField>:<parentId>"` — create-mode parent | |
|
|
33
|
+
* | | pre-link for a child task; refresh-safe. | with `form` |
|
|
34
|
+
* | `tab` | Active record-detail tab (stable semantic | replace |
|
|
35
|
+
* | | values: `details` / `related:<child>` / | |
|
|
36
|
+
* | | `related` / `activity` / `history`, | |
|
|
37
|
+
* | | objectui#2257). Never stacks history. | |
|
|
38
|
+
* | `palette` | Command palette overlay (alias `cmdk`). | replace |
|
|
39
|
+
* | `shortcuts` | Keyboard-shortcuts dialog overlay. | replace |
|
|
40
|
+
*
|
|
41
|
+
* Push-vs-replace rule of thumb: an overlay the user OPENED (form, drawer)
|
|
42
|
+
* pushes one entry so browser Back closes it; passive state that tracks an
|
|
43
|
+
* in-page selection (tab) or transient chrome (palette) replaces, so Back
|
|
44
|
+
* never pages through it.
|
|
45
|
+
*
|
|
46
|
+
* Page-scoped params (`q`, `limit`, `offset`, `view`, `type`, `review`,
|
|
47
|
+
* `package`, …) belong to their page's own contract and are NOT reserved
|
|
48
|
+
* here — but they must not collide with the names above.
|
|
49
|
+
*/
|
|
50
|
+
/** Record detail drawer over a list (`?recordId=<id>`). */
|
|
51
|
+
export const RECORD_DRAWER_PARAM = 'recordId';
|
|
52
|
+
/** Global record-form overlay: `new` | `<recordId>` (framework#2604). */
|
|
53
|
+
export const RECORD_FORM_PARAM = 'form';
|
|
54
|
+
/** Child-task object override for the record-form overlay (#2604 D3). */
|
|
55
|
+
export const RECORD_FORM_OBJECT_PARAM = 'formObject';
|
|
56
|
+
/** Child-task parent pre-link `"<fkField>:<parentId>"` (#2604 D3). */
|
|
57
|
+
export const RECORD_FORM_LINK_PARAM = 'formLink';
|
|
58
|
+
/** Active record-detail tab (objectui#2257; stable semantic values). */
|
|
59
|
+
export const RECORD_DETAIL_TAB_PARAM = 'tab';
|
|
60
|
+
/** Command palette overlay (ADR-0054 Phase 1; alias `cmdk`). */
|
|
61
|
+
export const COMMAND_PALETTE_PARAM = 'palette';
|
|
62
|
+
/** Keyboard-shortcuts dialog overlay. */
|
|
63
|
+
export const KEYBOARD_SHORTCUTS_PARAM = 'shortcuts';
|
|
64
|
+
/**
|
|
65
|
+
* All reserved params, for collision checks (e.g. a lint or a dev-time
|
|
66
|
+
* assertion that a page-scoped param doesn't shadow the console contract).
|
|
67
|
+
*/
|
|
68
|
+
export const RESERVED_URL_PARAMS = [
|
|
69
|
+
RECORD_DRAWER_PARAM,
|
|
70
|
+
RECORD_FORM_PARAM,
|
|
71
|
+
RECORD_FORM_OBJECT_PARAM,
|
|
72
|
+
RECORD_FORM_LINK_PARAM,
|
|
73
|
+
RECORD_DETAIL_TAB_PARAM,
|
|
74
|
+
COMMAND_PALETTE_PARAM,
|
|
75
|
+
KEYBOARD_SHORTCUTS_PARAM,
|
|
76
|
+
];
|
package/dist/utils/appRoute.d.ts
CHANGED
|
@@ -18,4 +18,19 @@ type AppLike = {
|
|
|
18
18
|
} & Record<string, unknown>;
|
|
19
19
|
export declare function appRouteSegment(app: AppLike | null | undefined): string | undefined;
|
|
20
20
|
export declare function matchAppBySegment<T extends AppLike>(apps: readonly T[] | null | undefined, seg: string | null | undefined): T | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* App → Studio reverse bridge (ADR-0080). Resolves a running app to its owning
|
|
23
|
+
* package's design surface (`/studio/:packageId/data`), or `null` when there is
|
|
24
|
+
* nothing to open:
|
|
25
|
+
* - the viewer is not a workspace admin (designing mutates shared package
|
|
26
|
+
* metadata, so the entry point is admin-only — mirrors the runtime editors);
|
|
27
|
+
* - the app has no owning package (runtime/DB apps), or its container is the
|
|
28
|
+
* DB-authored `sys_metadata` pseudo-package, which is not a package the
|
|
29
|
+
* Studio design surface can open.
|
|
30
|
+
*
|
|
31
|
+
* Writability of the target package is NOT checked here — the ADR-0070 D4 gate
|
|
32
|
+
* stays the server-side authority, and the Studio surface itself renders
|
|
33
|
+
* read-only packages as browse-only.
|
|
34
|
+
*/
|
|
35
|
+
export declare function appStudioDesignPath(app: AppLike | null | undefined, isWorkspaceAdmin: boolean): string | null;
|
|
21
36
|
export {};
|
package/dist/utils/appRoute.js
CHANGED
|
@@ -23,3 +23,25 @@ export function matchAppBySegment(apps, seg) {
|
|
|
23
23
|
return undefined;
|
|
24
24
|
return apps.find((a) => a?._packageId === seg) ?? apps.find((a) => a?.name === seg);
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* App → Studio reverse bridge (ADR-0080). Resolves a running app to its owning
|
|
28
|
+
* package's design surface (`/studio/:packageId/data`), or `null` when there is
|
|
29
|
+
* nothing to open:
|
|
30
|
+
* - the viewer is not a workspace admin (designing mutates shared package
|
|
31
|
+
* metadata, so the entry point is admin-only — mirrors the runtime editors);
|
|
32
|
+
* - the app has no owning package (runtime/DB apps), or its container is the
|
|
33
|
+
* DB-authored `sys_metadata` pseudo-package, which is not a package the
|
|
34
|
+
* Studio design surface can open.
|
|
35
|
+
*
|
|
36
|
+
* Writability of the target package is NOT checked here — the ADR-0070 D4 gate
|
|
37
|
+
* stays the server-side authority, and the Studio surface itself renders
|
|
38
|
+
* read-only packages as browse-only.
|
|
39
|
+
*/
|
|
40
|
+
export function appStudioDesignPath(app, isWorkspaceAdmin) {
|
|
41
|
+
if (!isWorkspaceAdmin)
|
|
42
|
+
return null;
|
|
43
|
+
const packageId = app?._packageId;
|
|
44
|
+
if (typeof packageId !== 'string' || !packageId || packageId === 'sys_metadata')
|
|
45
|
+
return null;
|
|
46
|
+
return `/studio/${encodeURIComponent(packageId)}/data`;
|
|
47
|
+
}
|
|
@@ -9,20 +9,29 @@
|
|
|
9
9
|
*
|
|
10
10
|
* This helper scans every object for fields whose `reference`/`reference_to`
|
|
11
11
|
* points back at the parent object and produces one related-list descriptor per
|
|
12
|
-
*
|
|
12
|
+
* eligible FK. The detail page (`RecordDetailView`) feeds these into the
|
|
13
13
|
* `record:related_list` renderers (and the legacy `DetailView.related`).
|
|
14
14
|
*
|
|
15
15
|
* Rules (kept in lockstep with the relationship-level `relatedList` spec flag):
|
|
16
16
|
* - Owned children (`master_detail`) and `lookup` children are SHOWN by
|
|
17
17
|
* default. Set `relatedList: false` on the FK field to suppress a noisy
|
|
18
|
-
* association/audit link
|
|
18
|
+
* association/audit link; set `relatedList: 'primary'` to mark a CORE
|
|
19
|
+
* relationship — the detail page promotes it to its own tab (the tab-vs-
|
|
20
|
+
* Related split is decided downstream in `buildDefaultTabs`).
|
|
19
21
|
* - `relatedListTitle` / `relatedListColumns` on the FK field override the
|
|
20
|
-
* derived title / columns
|
|
22
|
+
* derived title / columns (columns default to the child object's own list
|
|
23
|
+
* columns when omitted — resolved by the renderer).
|
|
21
24
|
* - Audit FKs (`created_by` / `updated_by` / `owner_id`) are skipped — they
|
|
22
25
|
* exist on virtually every object and would balloon the detail page into
|
|
23
26
|
* dozens of duplicate cards.
|
|
24
|
-
* -
|
|
25
|
-
*
|
|
27
|
+
* - ONE related list per eligible FK. A child may point at the parent through
|
|
28
|
+
* MORE THAN ONE relationship (e.g. `opportunity.primary_account` +
|
|
29
|
+
* `opportunity.partner_account`); each surfaces as its own list. When a
|
|
30
|
+
* child appears more than once and gave no explicit `relatedListTitle`, the
|
|
31
|
+
* FK's label is suffixed to disambiguate ("Opportunity · Partner Account").
|
|
32
|
+
* - Self-references are allowed (e.g. `account.parent_account` → `account`):
|
|
33
|
+
* the parent record lists the records whose self-FK points back at it
|
|
34
|
+
* ("Child Accounts"). Suppress with `relatedList: false` if unwanted.
|
|
26
35
|
* - Owned (`master_detail`) children are ordered before plain `lookup`
|
|
27
36
|
* children, preserving discovery order within each group.
|
|
28
37
|
*/
|
|
@@ -39,6 +48,12 @@ export interface DerivedRelatedList {
|
|
|
39
48
|
columns?: any[];
|
|
40
49
|
/** True when the child→parent link is a `master_detail` (owned) relationship. */
|
|
41
50
|
isOwned: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* True when the FK declares `relatedList: 'primary'`. A prominence hint
|
|
53
|
+
* (ADR-0085): the detail page promotes this relationship to its OWN tab,
|
|
54
|
+
* while non-primary lists collapse into a single "Related" tab.
|
|
55
|
+
*/
|
|
56
|
+
isPrimary: boolean;
|
|
42
57
|
}
|
|
43
58
|
interface ObjectLike {
|
|
44
59
|
name?: string;
|
|
@@ -9,20 +9,29 @@
|
|
|
9
9
|
*
|
|
10
10
|
* This helper scans every object for fields whose `reference`/`reference_to`
|
|
11
11
|
* points back at the parent object and produces one related-list descriptor per
|
|
12
|
-
*
|
|
12
|
+
* eligible FK. The detail page (`RecordDetailView`) feeds these into the
|
|
13
13
|
* `record:related_list` renderers (and the legacy `DetailView.related`).
|
|
14
14
|
*
|
|
15
15
|
* Rules (kept in lockstep with the relationship-level `relatedList` spec flag):
|
|
16
16
|
* - Owned children (`master_detail`) and `lookup` children are SHOWN by
|
|
17
17
|
* default. Set `relatedList: false` on the FK field to suppress a noisy
|
|
18
|
-
* association/audit link
|
|
18
|
+
* association/audit link; set `relatedList: 'primary'` to mark a CORE
|
|
19
|
+
* relationship — the detail page promotes it to its own tab (the tab-vs-
|
|
20
|
+
* Related split is decided downstream in `buildDefaultTabs`).
|
|
19
21
|
* - `relatedListTitle` / `relatedListColumns` on the FK field override the
|
|
20
|
-
* derived title / columns
|
|
22
|
+
* derived title / columns (columns default to the child object's own list
|
|
23
|
+
* columns when omitted — resolved by the renderer).
|
|
21
24
|
* - Audit FKs (`created_by` / `updated_by` / `owner_id`) are skipped — they
|
|
22
25
|
* exist on virtually every object and would balloon the detail page into
|
|
23
26
|
* dozens of duplicate cards.
|
|
24
|
-
* -
|
|
25
|
-
*
|
|
27
|
+
* - ONE related list per eligible FK. A child may point at the parent through
|
|
28
|
+
* MORE THAN ONE relationship (e.g. `opportunity.primary_account` +
|
|
29
|
+
* `opportunity.partner_account`); each surfaces as its own list. When a
|
|
30
|
+
* child appears more than once and gave no explicit `relatedListTitle`, the
|
|
31
|
+
* FK's label is suffixed to disambiguate ("Opportunity · Partner Account").
|
|
32
|
+
* - Self-references are allowed (e.g. `account.parent_account` → `account`):
|
|
33
|
+
* the parent record lists the records whose self-FK points back at it
|
|
34
|
+
* ("Child Accounts"). Suppress with `relatedList: false` if unwanted.
|
|
26
35
|
* - Owned (`master_detail`) children are ordered before plain `lookup`
|
|
27
36
|
* children, preserving discovery order within each group.
|
|
28
37
|
*/
|
|
@@ -50,9 +59,8 @@ export function deriveRelatedLists(objectDef, objects) {
|
|
|
50
59
|
const parentName = objectDef.name;
|
|
51
60
|
const owned = [];
|
|
52
61
|
const referenced = [];
|
|
53
|
-
const seenChild = new Set();
|
|
54
62
|
for (const child of objects) {
|
|
55
|
-
if (!child?.name
|
|
63
|
+
if (!child?.name)
|
|
56
64
|
continue;
|
|
57
65
|
for (const [fieldName, fieldDef] of fieldEntries(child.fields)) {
|
|
58
66
|
if (!fieldDef)
|
|
@@ -67,15 +75,13 @@ export function deriveRelatedLists(objectDef, objects) {
|
|
|
67
75
|
// Explicit opt-out lives on the relationship.
|
|
68
76
|
if (fieldDef.relatedList === false)
|
|
69
77
|
continue;
|
|
70
|
-
// One related list per child object — the first eligible FK wins.
|
|
71
|
-
if (seenChild.has(child.name))
|
|
72
|
-
continue;
|
|
73
|
-
seenChild.add(child.name);
|
|
74
78
|
const entry = {
|
|
75
79
|
childObject: child.name,
|
|
76
80
|
childLabel: child.label || child.name,
|
|
77
81
|
referenceField: fieldName,
|
|
78
82
|
isOwned: type === 'master_detail',
|
|
83
|
+
isPrimary: fieldDef.relatedList === 'primary',
|
|
84
|
+
_fkLabel: (typeof fieldDef.label === 'string' && fieldDef.label) || fieldName,
|
|
79
85
|
...(typeof fieldDef.relatedListTitle === 'string' && fieldDef.relatedListTitle
|
|
80
86
|
? { title: fieldDef.relatedListTitle }
|
|
81
87
|
: {}),
|
|
@@ -83,9 +89,21 @@ export function deriveRelatedLists(objectDef, objects) {
|
|
|
83
89
|
? { columns: fieldDef.relatedListColumns }
|
|
84
90
|
: {}),
|
|
85
91
|
};
|
|
92
|
+
// NO `break`: a child object may reference this parent through several FKs.
|
|
86
93
|
(entry.isOwned ? owned : referenced).push(entry);
|
|
87
|
-
break; // move on to the next child object
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
|
-
|
|
96
|
+
const all = [...owned, ...referenced];
|
|
97
|
+
// Multi-FK disambiguation: when a child object points here through more than
|
|
98
|
+
// one relationship and gave no explicit title, suffix the FK label so the two
|
|
99
|
+
// lists are distinguishable (e.g. "Opportunity · Partner Account").
|
|
100
|
+
const counts = {};
|
|
101
|
+
for (const r of all)
|
|
102
|
+
counts[r.childObject] = (counts[r.childObject] || 0) + 1;
|
|
103
|
+
return all.map(({ _fkLabel, ...rest }) => {
|
|
104
|
+
if (!rest.title && counts[rest.childObject] > 1) {
|
|
105
|
+
return { ...rest, title: `${rest.childLabel} · ${_fkLabel}` };
|
|
106
|
+
}
|
|
107
|
+
return rest;
|
|
108
|
+
});
|
|
91
109
|
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { ObjectDefinitionForNavigation, RecordFormTarget, ObjectDefinitionF
|
|
|
8
8
|
export { deriveRelatedLists } from './deriveRelatedLists';
|
|
9
9
|
export type { DerivedRelatedList } from './deriveRelatedLists';
|
|
10
10
|
export { preferLocal } from './preferLocal';
|
|
11
|
-
export { appRouteSegment, matchAppBySegment } from './appRoute';
|
|
11
|
+
export { appRouteSegment, matchAppBySegment, appStudioDesignPath } from './appRoute';
|
|
12
12
|
/**
|
|
13
13
|
* Resolves an I18nLabel to a plain string.
|
|
14
14
|
* I18nLabel can be either a string or an object { key, defaultValue?, params? }.
|
package/dist/utils/index.js
CHANGED
|
@@ -12,7 +12,7 @@ formatTitleTemplate as formatRecordTitle, } from '@object-ui/core';
|
|
|
12
12
|
export { resolveRecordFormTarget, resolveFormViewLayout, } from './recordFormNavigation';
|
|
13
13
|
export { deriveRelatedLists } from './deriveRelatedLists';
|
|
14
14
|
export { preferLocal } from './preferLocal';
|
|
15
|
-
export { appRouteSegment, matchAppBySegment } from './appRoute';
|
|
15
|
+
export { appRouteSegment, matchAppBySegment, appStudioDesignPath } from './appRoute';
|
|
16
16
|
/**
|
|
17
17
|
* Resolves an I18nLabel to a plain string.
|
|
18
18
|
* I18nLabel can be either a string or an object { key, defaultValue?, params? }.
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
* pageTabsUrlSync — make a record page's `page:tabs` node URL-aware
|
|
10
|
+
* (objectui#2257, ADR-0054 C3 "URL-addressable state").
|
|
11
|
+
*
|
|
12
|
+
* The active detail tab is user state that must SURVIVE the page subtree
|
|
13
|
+
* remounting — which happens for real on every `refreshKey`-style save
|
|
14
|
+
* refresh (and, in dev StrictMode, on any URL search change). An
|
|
15
|
+
* uncontrolled Radix `defaultValue` loses it; the URL keeps it.
|
|
16
|
+
*
|
|
17
|
+
* This helper walks a page schema tree and returns a NEW tree in which every
|
|
18
|
+
* `page:tabs` node carries the host-provided `defaultTab` (the value read
|
|
19
|
+
* from `?tab=`) and `onTabChange` (writes `?tab=` back, with `replace`). It
|
|
20
|
+
* never mutates the input — authored/assigned page schemas may be shared or
|
|
21
|
+
* memoized objects.
|
|
22
|
+
*
|
|
23
|
+
* Only container keys are traversed (`regions` / `components` / `children`);
|
|
24
|
+
* `page:tabs`' own `items` are its content, not a container to recurse into.
|
|
25
|
+
*/
|
|
26
|
+
export interface PageTabsUrlSyncInject {
|
|
27
|
+
/** Initial active tab (from `?tab=`); ignored by the renderer if it names no tab. */
|
|
28
|
+
defaultTab?: string;
|
|
29
|
+
/** Called by the renderer on every tab switch (host writes `?tab=`). */
|
|
30
|
+
onTabChange?: (value: string) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare function withPageTabsUrlSync<T>(node: T, inject: PageTabsUrlSyncInject): T;
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
const CONTAINER_KEYS = ['regions', 'components', 'children'];
|
|
9
|
+
export function withPageTabsUrlSync(node, inject) {
|
|
10
|
+
if (Array.isArray(node)) {
|
|
11
|
+
let outArr = null;
|
|
12
|
+
node.forEach((n, i) => {
|
|
13
|
+
const next = withPageTabsUrlSync(n, inject);
|
|
14
|
+
if (next !== n) {
|
|
15
|
+
outArr = outArr ?? node.slice();
|
|
16
|
+
outArr[i] = next;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return (outArr ?? node);
|
|
20
|
+
}
|
|
21
|
+
if (!node || typeof node !== 'object')
|
|
22
|
+
return node;
|
|
23
|
+
const rec = node;
|
|
24
|
+
let out = null;
|
|
25
|
+
if (rec.type === 'page:tabs') {
|
|
26
|
+
out = { ...rec };
|
|
27
|
+
if (inject.defaultTab !== undefined)
|
|
28
|
+
out.defaultTab = inject.defaultTab;
|
|
29
|
+
if (inject.onTabChange)
|
|
30
|
+
out.onTabChange = inject.onTabChange;
|
|
31
|
+
}
|
|
32
|
+
for (const key of CONTAINER_KEYS) {
|
|
33
|
+
const child = (out ?? rec)[key];
|
|
34
|
+
if (child === undefined || child === null)
|
|
35
|
+
continue;
|
|
36
|
+
const next = withPageTabsUrlSync(child, inject);
|
|
37
|
+
if (next !== child) {
|
|
38
|
+
out = out ?? { ...rec };
|
|
39
|
+
out[key] = next;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return (out ?? node);
|
|
43
|
+
}
|