@object-ui/app-shell 11.0.0 → 11.3.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 +144 -0
- package/dist/chrome/LoadingScreen.js +1 -1
- package/dist/console/ConsoleShell.js +2 -1
- package/dist/console/RemediationOverlay.d.ts +17 -0
- package/dist/console/RemediationOverlay.js +113 -0
- package/dist/console/ai/AiChatPage.d.ts +36 -0
- package/dist/console/ai/AiChatPage.js +104 -13
- package/dist/console/home/CloudOnboardingNext.d.ts +10 -0
- package/dist/console/home/CloudOnboardingNext.js +117 -0
- package/dist/console/home/HomePage.js +2 -1
- package/dist/console/organizations/OrganizationsPage.js +6 -2
- package/dist/console/organizations/resolveHomeUrl.d.ts +7 -0
- package/dist/console/organizations/resolveHomeUrl.js +19 -2
- package/dist/hooks/useChatConversation.d.ts +22 -0
- package/dist/hooks/useChatConversation.js +57 -10
- package/dist/hooks/useReconcileOnError.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/layout/ConsoleFloatingChatbot.js +54 -7
- package/dist/layout/WorkspaceSwitcher.d.ts +4 -2
- package/dist/layout/WorkspaceSwitcher.js +15 -10
- package/dist/preview/DraftPreviewBar.js +15 -11
- package/dist/utils/index.d.ts +2 -24
- package/dist/utils/index.js +14 -101
- package/dist/views/DashboardView.js +2 -3
- package/dist/views/InterfaceListPage.js +4 -1
- package/dist/views/ObjectView.js +4 -2
- package/dist/views/PageView.js +14 -5
- package/dist/views/RecordDetailView.js +1 -0
- package/dist/views/ReportView.js +2 -3
- package/dist/views/metadata-admin/clientValidation.js +8 -2
- package/dist/views/metadata-admin/color-variant-field.d.ts +1 -12
- package/dist/views/metadata-admin/color-variant-field.js +11 -0
- package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +1 -13
- package/dist/views/metadata-admin/previews/OutlineStrip.js +12 -0
- package/dist/views/metadata-admin/previews/PagePreview.js +9 -0
- package/dist/views/metadata-admin/previews/SourcePageEditor.d.ts +28 -0
- package/dist/views/metadata-admin/previews/SourcePageEditor.js +83 -0
- package/dist/views/studio-design/StudioDesignSurface.d.ts +20 -0
- package/dist/views/studio-design/StudioDesignSurface.js +659 -0
- package/package.json +43 -43
|
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
15
15
|
*/
|
|
16
16
|
import { useEffect, useState } from 'react';
|
|
17
17
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
18
|
-
import { Eye, X, Rocket, GitCompareArrows } from 'lucide-react';
|
|
18
|
+
import { Eye, X, Rocket, GitCompareArrows, Sparkles } from 'lucide-react';
|
|
19
19
|
import { Button, cn } from '@object-ui/components';
|
|
20
20
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
21
21
|
import { usePreviewDrafts, markPreviewExit, PREVIEW_QUERY_FLAG } from './PreviewModeContext';
|
|
@@ -85,15 +85,19 @@ export function DraftPreviewBar() {
|
|
|
85
85
|
// preview indicator. An UNKNOWN count (null — still loading or the fetch
|
|
86
86
|
// failed) keeps the publish path: we only relax when we KNOW the count is zero.
|
|
87
87
|
const noChanges = pendingCount === 0;
|
|
88
|
-
return (_jsxs("div", { className: cn('sticky top-0 z-40
|
|
88
|
+
return (_jsxs("div", { className: cn('sticky top-0 z-40 border-b', noChanges
|
|
89
89
|
? 'border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-700/60 dark:bg-slate-900/40 dark:text-slate-300'
|
|
90
|
-
: 'border-amber-300/70 bg-amber-50 text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-200'), "data-testid": "draft-preview-bar", children: [_jsx(Eye, { className: "h-4 w-4 shrink-0" }), _jsx("p", { className: "min-w-0 flex-1 truncate", children: noChanges
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
: 'border-amber-300/70 bg-amber-50 text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-200'), "data-testid": "draft-preview-bar", children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-2 text-sm", children: [_jsx(Eye, { className: "h-4 w-4 shrink-0" }), _jsx("p", { className: "min-w-0 flex-1 truncate", children: noChanges
|
|
91
|
+
? t('preview.draftBar.messageClean', {
|
|
92
|
+
defaultValue: 'Draft preview — no unpublished changes; everything here is already live.',
|
|
93
|
+
})
|
|
94
|
+
: t('preview.draftBar.message', {
|
|
95
|
+
defaultValue: 'Draft preview — you are seeing unpublished changes. Nothing here is live until you publish.',
|
|
96
|
+
}) }), !noChanges && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setChangesOpen(true), "data-testid": "draft-preview-changes", children: [_jsx(GitCompareArrows, { className: "mr-1 h-3.5 w-3.5" }), t('preview.draftBar.changes', { defaultValue: 'Changes' }), typeof pendingCount === 'number' ? ` (${pendingCount})` : ''] })), _jsxs(Button, { size: "sm", variant: "outline", onClick: exit, "data-testid": "draft-preview-exit", children: [_jsx(X, { className: "mr-1 h-3.5 w-3.5" }), t('preview.draftBar.exit', { defaultValue: 'Exit preview' })] })] }), !noChanges && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center gap-3 border-t border-amber-300/50 px-4 py-2.5 dark:border-amber-700/40", "data-testid": "draft-preview-sample-hint", children: [_jsx(Sparkles, { className: "h-5 w-5 shrink-0 text-amber-600 dark:text-amber-300", "aria-hidden": "true" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-sm font-semibold leading-tight", children: t('preview.draftBar.sampleDataTitle', {
|
|
97
|
+
defaultValue: 'Sample data appears once you publish',
|
|
98
|
+
}) }), _jsx("p", { className: "text-xs leading-snug text-amber-800/80 dark:text-amber-200/70", children: t('preview.draftBar.sampleDataBody', {
|
|
99
|
+
defaultValue: 'You’re previewing your app’s structure. Publish to load example records and make it live.',
|
|
100
|
+
}) })] }), _jsxs(Button, { onClick: publish, disabled: publishing, "data-testid": "draft-preview-publish", className: "shrink-0 shadow-sm", children: [_jsx(Rocket, { className: "mr-1.5 h-4 w-4" }), publishing
|
|
101
|
+
? t('preview.draftBar.publishing', { defaultValue: 'Publishing…' })
|
|
102
|
+
: t('preview.draftBar.publishCta', { defaultValue: 'Publish to see it live' })] })] }), _jsx(DraftChangesPanel, { open: changesOpen, onOpenChange: setChangesOpen })] }))] }));
|
|
99
103
|
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility functions for ObjectStack Console
|
|
3
3
|
*/
|
|
4
|
+
export { getRecordDisplayName, deriveTitleField, isTitleEligibleField, formatTitleTemplate as formatRecordTitle, } from '@object-ui/core';
|
|
5
|
+
export type { RecordDisplayNameOptions } from '@object-ui/core';
|
|
4
6
|
export { resolveRecordFormTarget, resolveFormViewLayout, } from './recordFormNavigation';
|
|
5
7
|
export type { ObjectDefinitionForNavigation, RecordFormTarget, ObjectDefinitionForFormView, FormViewDefinition, FormViewModalLayout, } from './recordFormNavigation';
|
|
6
8
|
export { deriveRelatedLists } from './deriveRelatedLists';
|
|
@@ -23,27 +25,3 @@ export declare function resolveI18nLabel(label: string | {
|
|
|
23
25
|
* Preferred over CSS `capitalize` for i18n compatibility.
|
|
24
26
|
*/
|
|
25
27
|
export declare function capitalizeFirst(str: string): string;
|
|
26
|
-
/**
|
|
27
|
-
* Format a record title using the titleFormat pattern.
|
|
28
|
-
*
|
|
29
|
-
* Accepts either a legacy string template or an Expression envelope
|
|
30
|
-
* (`{ dialect: 'template', source: string }`) emitted by `@objectstack/spec`'s
|
|
31
|
-
* normalized templates. The placeholder syntax (`{field}`) is identical in both
|
|
32
|
-
* shapes; only the wrapping object is new.
|
|
33
|
-
*
|
|
34
|
-
* Empty placeholders (missing or null/empty fields) are stripped along with
|
|
35
|
-
* any orphan separator they leave behind, so a template like
|
|
36
|
-
* "{full_name} - {company}"
|
|
37
|
-
* evaluated against `{ company: "Acme" }` resolves to `"Acme"` rather than
|
|
38
|
-
* `" - Acme"`. Returns an empty string when no placeholder resolved.
|
|
39
|
-
*/
|
|
40
|
-
export declare function formatRecordTitle(titleFormat: string | {
|
|
41
|
-
source?: string;
|
|
42
|
-
} | undefined, record: any): string;
|
|
43
|
-
/**
|
|
44
|
-
* Get display name for a record using titleFormat or fallback
|
|
45
|
-
* @param objectDef Object definition with optional titleFormat
|
|
46
|
-
* @param record The record data
|
|
47
|
-
* @returns Display name for the record
|
|
48
|
-
*/
|
|
49
|
-
export declare function getRecordDisplayName(objectDef: any, record: any): string;
|
package/dist/utils/index.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility functions for ObjectStack Console
|
|
3
3
|
*/
|
|
4
|
+
// Re-export the unified record display-name resolver (ADR-0079) so existing
|
|
5
|
+
// importers of `@object-ui/app-shell`'s `getRecordDisplayName` /
|
|
6
|
+
// `formatRecordTitle` keep working unchanged. The implementation now lives in
|
|
7
|
+
// `@object-ui/core` (pure util, shared by every view plugin and field widget).
|
|
8
|
+
export { getRecordDisplayName, deriveTitleField, isTitleEligibleField,
|
|
9
|
+
// `formatTitleTemplate` is the new canonical name; alias it to the legacy
|
|
10
|
+
// `formatRecordTitle` export this module has always provided.
|
|
11
|
+
formatTitleTemplate as formatRecordTitle, } from '@object-ui/core';
|
|
4
12
|
export { resolveRecordFormTarget, resolveFormViewLayout, } from './recordFormNavigation';
|
|
5
13
|
export { deriveRelatedLists } from './deriveRelatedLists';
|
|
6
14
|
export { preferLocal } from './preferLocal';
|
|
@@ -32,104 +40,9 @@ export function capitalizeFirst(str) {
|
|
|
32
40
|
return str;
|
|
33
41
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
34
42
|
}
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Format a record title using the titleFormat pattern.
|
|
43
|
-
*
|
|
44
|
-
* Accepts either a legacy string template or an Expression envelope
|
|
45
|
-
* (`{ dialect: 'template', source: string }`) emitted by `@objectstack/spec`'s
|
|
46
|
-
* normalized templates. The placeholder syntax (`{field}`) is identical in both
|
|
47
|
-
* shapes; only the wrapping object is new.
|
|
48
|
-
*
|
|
49
|
-
* Empty placeholders (missing or null/empty fields) are stripped along with
|
|
50
|
-
* any orphan separator they leave behind, so a template like
|
|
51
|
-
* "{full_name} - {company}"
|
|
52
|
-
* evaluated against `{ company: "Acme" }` resolves to `"Acme"` rather than
|
|
53
|
-
* `" - Acme"`. Returns an empty string when no placeholder resolved.
|
|
54
|
-
*/
|
|
55
|
-
export function formatRecordTitle(titleFormat, record) {
|
|
56
|
-
// Normalize Expression envelope ({ dialect, source }) → raw template string.
|
|
57
|
-
const template = typeof titleFormat === 'string'
|
|
58
|
-
? titleFormat
|
|
59
|
-
: (titleFormat && typeof titleFormat === 'object' && typeof titleFormat.source === 'string')
|
|
60
|
-
? titleFormat.source
|
|
61
|
-
: undefined;
|
|
62
|
-
if (!template || !record) {
|
|
63
|
-
return record?.id || record?._id || 'Record';
|
|
64
|
-
}
|
|
65
|
-
let anyResolved = false;
|
|
66
|
-
let out = template.replace(/\{([^{}]+)\}/g, (_match, fieldName) => {
|
|
67
|
-
// Support dotted paths (e.g. `{account.name}`) for $expanded lookups.
|
|
68
|
-
const parts = String(fieldName).trim().split('.');
|
|
69
|
-
let value = record;
|
|
70
|
-
for (const p of parts) {
|
|
71
|
-
if (value == null)
|
|
72
|
-
break;
|
|
73
|
-
value = value[p];
|
|
74
|
-
}
|
|
75
|
-
// Auto-extract display name from expanded reference objects, with a
|
|
76
|
-
// Salesforce-style fallback chain.
|
|
77
|
-
if (value && typeof value === 'object') {
|
|
78
|
-
const o = value;
|
|
79
|
-
let display = o.name ?? o.full_name ?? o.display_name ?? o.label ?? o.title ?? o.subject ?? null;
|
|
80
|
-
if (display == null || (typeof display === 'string' && !display.trim())) {
|
|
81
|
-
const composite = [o.salutation, o.first_name, o.last_name]
|
|
82
|
-
.filter((p) => typeof p === 'string' && p.trim())
|
|
83
|
-
.map((p) => p.trim())
|
|
84
|
-
.join(' ');
|
|
85
|
-
if (composite)
|
|
86
|
-
display = composite;
|
|
87
|
-
else if (typeof o.email === 'string' && o.email.trim())
|
|
88
|
-
display = o.email.trim();
|
|
89
|
-
else
|
|
90
|
-
display = null;
|
|
91
|
-
}
|
|
92
|
-
value = display;
|
|
93
|
-
}
|
|
94
|
-
if (value === null || value === undefined || value === '') {
|
|
95
|
-
return EMPTY_TOKEN;
|
|
96
|
-
}
|
|
97
|
-
anyResolved = true;
|
|
98
|
-
return String(value);
|
|
99
|
-
});
|
|
100
|
-
if (!anyResolved)
|
|
101
|
-
return '';
|
|
102
|
-
// Drop separators on either side of an empty token, then any leftover
|
|
103
|
-
// tokens, then collapse runs of whitespace.
|
|
104
|
-
const sepBefore = new RegExp(`\\s*${SEPARATOR_CLASS}\\s*${EMPTY_TOKEN}`, 'g');
|
|
105
|
-
const sepAfter = new RegExp(`${EMPTY_TOKEN}\\s*${SEPARATOR_CLASS}\\s*`, 'g');
|
|
106
|
-
out = out
|
|
107
|
-
.replace(sepBefore, '')
|
|
108
|
-
.replace(sepAfter, '')
|
|
109
|
-
.replace(new RegExp(EMPTY_TOKEN, 'g'), '')
|
|
110
|
-
.replace(/\s+/g, ' ')
|
|
111
|
-
.trim();
|
|
112
|
-
return out;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Get display name for a record using titleFormat or fallback
|
|
116
|
-
* @param objectDef Object definition with optional titleFormat
|
|
117
|
-
* @param record The record data
|
|
118
|
-
* @returns Display name for the record
|
|
119
|
-
*/
|
|
120
|
-
export function getRecordDisplayName(objectDef, record) {
|
|
121
|
-
if (objectDef?.titleFormat) {
|
|
122
|
-
const formatted = formatRecordTitle(objectDef.titleFormat, record);
|
|
123
|
-
if (formatted)
|
|
124
|
-
return formatted;
|
|
125
|
-
}
|
|
126
|
-
return (record?.name ||
|
|
127
|
-
record?.full_name ||
|
|
128
|
-
record?.fullName ||
|
|
129
|
-
record?.title ||
|
|
130
|
-
record?.label ||
|
|
131
|
-
record?.subject ||
|
|
132
|
-
record?.id ||
|
|
133
|
-
record?._id ||
|
|
134
|
-
'Untitled');
|
|
135
|
-
}
|
|
43
|
+
// NOTE (ADR-0079): `formatRecordTitle` (now canonically `formatTitleTemplate`)
|
|
44
|
+
// and `getRecordDisplayName` moved to `@object-ui/core` and are re-exported
|
|
45
|
+
// from the top of this module. The previous local copies — a titleFormat-only
|
|
46
|
+
// resolver that fell back to a hard-coded `name`/`title`/… list and bottomed
|
|
47
|
+
// out at the literal 'Untitled' — are gone, so every surface now also honors
|
|
48
|
+
// the object's `displayNameField` + type-aware derivation + `Record #<id>`.
|
|
@@ -13,7 +13,7 @@ import { DrillNavigationProvider } from '@object-ui/react';
|
|
|
13
13
|
import { useOpenRecordList } from './useOpenRecordList';
|
|
14
14
|
import { ModalForm } from '@object-ui/plugin-form';
|
|
15
15
|
import { DashboardConfigPanel } from './DashboardConfigPanel';
|
|
16
|
-
import {
|
|
16
|
+
import { useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
17
17
|
import { toast } from 'sonner';
|
|
18
18
|
import { Empty, EmptyTitle, EmptyDescription, Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@object-ui/components';
|
|
19
19
|
import { LayoutDashboard, Pencil, TrendingUp, BarChart3, LineChart, PieChart, Table2, LayoutGrid, Plus, } from 'lucide-react';
|
|
@@ -77,8 +77,7 @@ export function DashboardView({ dataSource }) {
|
|
|
77
77
|
const { dashboardLabel, dashboardDescription } = useObjectLabel();
|
|
78
78
|
// Editing a dashboard mutates the SHARED definition, so it is an admin-only
|
|
79
79
|
// quick-edit affordance (mirrors ObjectView's view-config gate).
|
|
80
|
-
const
|
|
81
|
-
const isAdmin = user?.role === 'admin';
|
|
80
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
82
81
|
const [isLoading, setIsLoading] = useState(true);
|
|
83
82
|
const [configPanelOpen, setConfigPanelOpen] = useState(false);
|
|
84
83
|
const [selectedWidgetId, setSelectedWidgetId] = useState(null);
|
|
@@ -319,7 +319,10 @@ export function InterfaceListPage({ page, className, onConfigChange, reserveEdit
|
|
|
319
319
|
showGroup: false,
|
|
320
320
|
showColor: false,
|
|
321
321
|
allowExport: false,
|
|
322
|
-
|
|
322
|
+
// Inline record editing is a page-authored property: a list block opts in
|
|
323
|
+
// via `userActions.editInline` (default off). When on, clicking a cell
|
|
324
|
+
// edits it with the dedicated field widgets, same as the object views.
|
|
325
|
+
inlineEdit: userActions.editInline === true,
|
|
323
326
|
};
|
|
324
327
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
325
328
|
}, [objectDefName, viewDefJson, cfg]);
|
package/dist/views/ObjectView.js
CHANGED
|
@@ -37,7 +37,7 @@ import { resolveManagedByEmptyState } from '../utils/managedByEmptyState';
|
|
|
37
37
|
import { useObjectActions } from '../hooks/useObjectActions';
|
|
38
38
|
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
39
39
|
import { usePermissions } from '@object-ui/permissions';
|
|
40
|
-
import { useAuth } from '@object-ui/auth';
|
|
40
|
+
import { useAuth, useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
41
41
|
import { useRealtimeSubscription, useConflictResolution } from '@object-ui/collaboration';
|
|
42
42
|
import { ActionProvider, useNavigationOverlay, SchemaRenderer } from '@object-ui/react';
|
|
43
43
|
import { toast } from 'sonner';
|
|
@@ -253,7 +253,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
253
253
|
const [recordCount, setRecordCount] = useState(undefined);
|
|
254
254
|
// Admin users automatically get design tools (no toggle needed)
|
|
255
255
|
const { user, activeOrganization } = useAuth();
|
|
256
|
-
const isAdmin =
|
|
256
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
257
257
|
const { can } = usePermissions();
|
|
258
258
|
// Get Object Definition. The outer ObjectView wrapper already guards the
|
|
259
259
|
// missing-object case, so this always resolves while this component is
|
|
@@ -1302,6 +1302,8 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
|
1302
1302
|
persistViewPatch(viewDef.id, viewDef, { filter });
|
|
1303
1303
|
}, onHiddenFieldsChange: (hidden) => {
|
|
1304
1304
|
persistViewPatch(viewDef.id, viewDef, { hiddenFields: hidden });
|
|
1305
|
+
}, onInlineEditChange: (next) => {
|
|
1306
|
+
persistViewPatch(viewDef.id, viewDef, { inlineEdit: next });
|
|
1305
1307
|
}, onColumnStateChange: (state) => {
|
|
1306
1308
|
persistViewPatch(viewDef.id, viewDef, { columnState: state });
|
|
1307
1309
|
}, userFilterSelections: initialUfSelections, onUserFilterSelectionsChange: handleUserFilterSelectionsChange, dataSource: ds }, key));
|
package/dist/views/PageView.js
CHANGED
|
@@ -12,10 +12,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
import { useState } from 'react';
|
|
13
13
|
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
|
14
14
|
import { SchemaRenderer, useAdapter } from '@object-ui/react';
|
|
15
|
-
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
15
|
+
import { Empty, EmptyTitle, EmptyDescription, Spinner } from '@object-ui/components';
|
|
16
16
|
import { FileText, Pencil } from 'lucide-react';
|
|
17
17
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
18
|
-
import {
|
|
18
|
+
import { useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
19
19
|
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
20
20
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
21
21
|
import { useExpressionContext } from '../providers/ExpressionProvider';
|
|
@@ -31,9 +31,8 @@ export function PageView() {
|
|
|
31
31
|
const location = useLocation();
|
|
32
32
|
// Editing a page mutates the shared metadata definition, so the entry point
|
|
33
33
|
// is admin-only (mirrors the view/report/dashboard runtime editors).
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const { pages, objects } = useMetadata();
|
|
34
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
35
|
+
const { pages, objects, getTypeStatus } = useMetadata();
|
|
37
36
|
// ADR-0048 Phase 2 — prefer the page owned by the current app's package so
|
|
38
37
|
// two packages shipping `page/<same-name>` each resolve within their own
|
|
39
38
|
// container instead of by load order.
|
|
@@ -44,6 +43,16 @@ export function PageView() {
|
|
|
44
43
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
45
44
|
const page = preferLocal(pages, pageName, activeApp?._packageId);
|
|
46
45
|
if (!page) {
|
|
46
|
+
// `page` metadata is lazy-loaded: on the very first access `pages` is an
|
|
47
|
+
// empty array while the fetch is in flight, which would flash a false
|
|
48
|
+
// "page not found" (or a blank body) — exactly the post-signup landing
|
|
49
|
+
// race where the app's home page is the first thing rendered. Show a
|
|
50
|
+
// loading state until the `page` type is actually resolved, then trust the
|
|
51
|
+
// not-found. (getTypeStatus absent = hand-rolled context = always ready.)
|
|
52
|
+
const pageStatus = getTypeStatus?.('page');
|
|
53
|
+
if (pageStatus === 'idle' || pageStatus === 'loading') {
|
|
54
|
+
return (_jsx("div", { className: "h-full flex items-center justify-center p-8", "data-testid": "page-loading", children: _jsx(Spinner, { className: "h-5 w-5 text-muted-foreground" }) }));
|
|
55
|
+
}
|
|
47
56
|
return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(FileText, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.pageNotFound') }), _jsx(EmptyDescription, { children: t('empty.pageNotFoundDescription', { name: pageName }) })] }) }));
|
|
48
57
|
}
|
|
49
58
|
const params = Object.fromEntries(searchParams.entries());
|
|
@@ -1700,6 +1700,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
1700
1700
|
showReferenceRail: objectDef?.detail?.showReferenceRail === true || undefined,
|
|
1701
1701
|
hideReferenceRail: objectDef?.detail?.hideReferenceRail === true || undefined,
|
|
1702
1702
|
hideRelatedTab: objectDef?.detail?.hideRelatedTab === true || undefined,
|
|
1703
|
+
relatedLayout: objectDef?.detail?.relatedLayout === 'tabs' ? 'tabs' : undefined,
|
|
1703
1704
|
...(assignedSlots ? { slots: assignedSlots } : {}),
|
|
1704
1705
|
});
|
|
1705
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: {
|
package/dist/views/ReportView.js
CHANGED
|
@@ -16,7 +16,7 @@ import { preferLocal } from '../utils/preferLocal';
|
|
|
16
16
|
import { useAdapter } from '../providers/AdapterProvider';
|
|
17
17
|
import { useMetadataClient } from './metadata-admin/useMetadata';
|
|
18
18
|
import { persistRuntimeMetadata } from './runtime-metadata-persistence';
|
|
19
|
-
import {
|
|
19
|
+
import { useIsWorkspaceAdmin } from '@object-ui/auth';
|
|
20
20
|
import { DrillDownDrawer } from '@object-ui/plugin-dashboard';
|
|
21
21
|
import { DrillNavigationProvider } from '@object-ui/react';
|
|
22
22
|
import { useOpenRecordList } from './useOpenRecordList';
|
|
@@ -40,8 +40,7 @@ export function ReportView({ dataSource }) {
|
|
|
40
40
|
const metadataClient = useMetadataClient();
|
|
41
41
|
// Editing a report mutates the SHARED definition, so it is an admin-only
|
|
42
42
|
// quick-edit affordance (mirrors ObjectView's view-config gate).
|
|
43
|
-
const
|
|
44
|
-
const isAdmin = user?.role === 'admin';
|
|
43
|
+
const isAdmin = useIsWorkspaceAdmin();
|
|
45
44
|
const [configPanelOpen, setConfigPanelOpen] = useState(false);
|
|
46
45
|
// Version counter — incremented on save to refresh the stable config reference
|
|
47
46
|
const [configVersion, setConfigVersion] = useState(0);
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
// Types still falling through to server-only validation:
|
|
7
7
|
// - `validation`: not a top-level metadata file; lives inside object. (DataValidationRuleSchema
|
|
8
8
|
// exists but has empty shape, so it's not useful for client validation.)
|
|
9
|
-
// - `
|
|
9
|
+
// - `policy`: spec 11.2.0 (PR #2078) removed the generic `PolicySchema` (the org-wide
|
|
10
|
+
// password/network/session/audit policy) from `@objectstack/spec/security`, and the
|
|
11
|
+
// canonical metadata-type→schema registry (spec kernel/metadata-type-schemas.ts) has
|
|
12
|
+
// no `policy` entry — so there is no client schema. `RowLevelSecurityPolicySchema`
|
|
13
|
+
// remains on /security but is a different shape (a per-object RLS rule), NOT the
|
|
14
|
+
// `policy` metadata file, so it must not be substituted.
|
|
10
15
|
// - `trigger`: no standalone TriggerSchema export at runtime (only
|
|
11
16
|
// ConnectorTriggerSchema / WebhookEventSchema variants).
|
|
12
17
|
// - `sharing_rule`: SharingRuleSchema is declared but has empty shape — server-only.
|
|
@@ -52,7 +57,8 @@ const LOADERS = {
|
|
|
52
57
|
// packages/spec/src/kernel/metadata-type-schemas.ts for the canonical mapping.
|
|
53
58
|
permission: async () => (await import('@objectstack/spec/security')).PermissionSetSchema,
|
|
54
59
|
profile: async () => (await import('@objectstack/spec/security')).PermissionSetSchema,
|
|
55
|
-
policy
|
|
60
|
+
// `policy` intentionally omitted — spec 11.2.0 dropped `PolicySchema` and the metadata-type
|
|
61
|
+
// registry has no `policy` schema; drafts fall through to server-side validation (see top).
|
|
56
62
|
// identity
|
|
57
63
|
role: async () => (await import('@objectstack/spec/identity')).RoleSchema,
|
|
58
64
|
// api
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared color-variant swatch picker.
|
|
3
|
-
*
|
|
4
|
-
* Metadata color fields (`colorVariant` on metrics / dashboard widgets, badge
|
|
5
|
-
* tones, …) are a fixed SEMANTIC palette, not arbitrary hex. A row of colored
|
|
6
|
-
* swatches is far more scannable than a text dropdown — the admin sees the
|
|
7
|
-
* actual color, like Linear/Notion label pickers. Reused by the generic
|
|
8
|
-
* SchemaForm `color-picker` widget and the curated inspectors so every color
|
|
9
|
-
* field looks and behaves the same.
|
|
10
|
-
*/
|
|
11
|
-
import * as React from 'react';
|
|
12
1
|
export interface ColorVariant {
|
|
13
2
|
value: string;
|
|
14
3
|
label: string;
|
|
@@ -27,4 +16,4 @@ export declare function ColorVariantPicker({ value, onChange, disabled, options
|
|
|
27
16
|
value: string;
|
|
28
17
|
label?: string;
|
|
29
18
|
}>;
|
|
30
|
-
}):
|
|
19
|
+
}): import("react").JSX.Element;
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
3
|
+
/**
|
|
4
|
+
* Shared color-variant swatch picker.
|
|
5
|
+
*
|
|
6
|
+
* Metadata color fields (`colorVariant` on metrics / dashboard widgets, badge
|
|
7
|
+
* tones, …) are a fixed SEMANTIC palette, not arbitrary hex. A row of colored
|
|
8
|
+
* swatches is far more scannable than a text dropdown — the admin sees the
|
|
9
|
+
* actual color, like Linear/Notion label pickers. Reused by the generic
|
|
10
|
+
* SchemaForm `color-picker` widget and the curated inspectors so every color
|
|
11
|
+
* field looks and behaves the same.
|
|
12
|
+
*/
|
|
2
13
|
import { cn } from '@object-ui/components';
|
|
3
14
|
/** Canonical semantic palette (mirrors the renderer's colorVariant tokens). */
|
|
4
15
|
export const COLOR_VARIANTS = [
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OutlineStrip — clickable chip strip that lets users select
|
|
3
|
-
* sub-elements inside a preview whose main canvas renders through a
|
|
4
|
-
* sealed external renderer (SchemaRenderer, ReportRenderer, …) and
|
|
5
|
-
* therefore can't intercept clicks directly.
|
|
6
|
-
*
|
|
7
|
-
* The strip sits above the canvas. Each chip emits a selection on
|
|
8
|
-
* click; the currently-selected one gets a ring. Empty list collapses
|
|
9
|
-
* the strip entirely so the canvas takes the full preview height
|
|
10
|
-
* outside design mode.
|
|
11
|
-
*/
|
|
12
|
-
import * as React from 'react';
|
|
13
1
|
export interface OutlineEntry {
|
|
14
2
|
/** Selection id to emit. */
|
|
15
3
|
id: string;
|
|
@@ -29,4 +17,4 @@ export declare function OutlineStrip({ title, entries, selectedId, onSelect, onA
|
|
|
29
17
|
onAdd?: () => void;
|
|
30
18
|
/** Tooltip / aria-label for the Add chip. Defaults to "Add". */
|
|
31
19
|
addLabel?: string;
|
|
32
|
-
}):
|
|
20
|
+
}): import("react").JSX.Element | null;
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
3
|
+
/**
|
|
4
|
+
* OutlineStrip — clickable chip strip that lets users select
|
|
5
|
+
* sub-elements inside a preview whose main canvas renders through a
|
|
6
|
+
* sealed external renderer (SchemaRenderer, ReportRenderer, …) and
|
|
7
|
+
* therefore can't intercept clicks directly.
|
|
8
|
+
*
|
|
9
|
+
* The strip sits above the canvas. Each chip emits a selection on
|
|
10
|
+
* click; the currently-selected one gets a ring. Empty list collapses
|
|
11
|
+
* the strip entirely so the canvas takes the full preview height
|
|
12
|
+
* outside design mode.
|
|
13
|
+
*/
|
|
2
14
|
import { Plus } from 'lucide-react';
|
|
3
15
|
import { cn } from '@object-ui/components';
|
|
4
16
|
export function OutlineStrip({ title, entries, selectedId, onSelect, onAdd, addLabel, }) {
|
|
@@ -14,6 +14,7 @@ import { buildExpandFields } from '@object-ui/core';
|
|
|
14
14
|
import { buildDefaultPageSchema } from '@object-ui/plugin-detail';
|
|
15
15
|
import { PreviewShell, PreviewErrorBoundary, PreviewMessage } from './PreviewShell';
|
|
16
16
|
import { OutlineStrip } from './OutlineStrip';
|
|
17
|
+
import { SourcePageEditor } from './SourcePageEditor';
|
|
17
18
|
import { PageBlockCanvas } from './PageBlockCanvas';
|
|
18
19
|
import { InterfaceListPage } from '../../InterfaceListPage';
|
|
19
20
|
import { t as tr } from '../i18n';
|
|
@@ -205,6 +206,14 @@ export function PagePreview({ draft, editing, selection, onSelectionChange, onPa
|
|
|
205
206
|
if (isInterfacePage) {
|
|
206
207
|
return (_jsx(PreviewShell, { hint: "page \u00B7 interface", children: _jsx(PreviewErrorBoundary, { fallbackHint: "The interface page references a source object/view that isn't available.", children: _jsx(InterfaceListPage, { page: draft, onConfigChange: canEdit ? (patch) => onPatch({ interfaceConfig: { ...((draft.interfaceConfig) || {}), ...patch } }) : undefined }) }) }));
|
|
207
208
|
}
|
|
209
|
+
// Source page (ADR-0080/0081) → `kind:'html'`/`'react'` pages are a `source`
|
|
210
|
+
// string, not a region tree. The design canvas does not apply (and would
|
|
211
|
+
// choke on the absent regions/children), so edit the source directly with a
|
|
212
|
+
// live preview. This is the fix for the editor crashing on these pages.
|
|
213
|
+
const pageKind = draft.kind;
|
|
214
|
+
if (pageKind === 'react' || pageKind === 'html') {
|
|
215
|
+
return (_jsx(SourcePageEditor, { draft: draft, onPatch: canEdit ? onPatch : undefined, readOnly: !canEdit }));
|
|
216
|
+
}
|
|
208
217
|
// Empty draft → no preview; but if we're in design mode show the
|
|
209
218
|
// canvas so users can author from scratch.
|
|
210
219
|
if (!schema || Object.keys(schema).length <= 1) {
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
* SourcePageEditor — the Studio editor surface for `kind:'html'` and
|
|
9
|
+
* `kind:'react'` pages (ADR-0080/0081). These pages ARE a `source` string
|
|
10
|
+
* (JSX/HTML or real React), not a region tree — so the structured design
|
|
11
|
+
* canvas does not apply (and would choke on the missing `regions`/`children`).
|
|
12
|
+
* Instead we present a code editor for the `source` field beside a live preview
|
|
13
|
+
* rendered through the runtime SchemaRenderer. Edits patch `draft.source`.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors JsonSourceEditor's Monaco + textarea-fallback + theme handling, but
|
|
16
|
+
* edits ONE field (the source) instead of the whole-record JSON, with a
|
|
17
|
+
* JSX/TSX language mode.
|
|
18
|
+
*/
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
export interface SourcePageEditorProps {
|
|
21
|
+
draft: Record<string, unknown>;
|
|
22
|
+
/** Patch the draft; undefined in read-only mode. */
|
|
23
|
+
onPatch?: (patch: Record<string, unknown>) => void;
|
|
24
|
+
readOnly?: boolean;
|
|
25
|
+
fallbackDelayMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function SourcePageEditor({ draft, onPatch, readOnly, fallbackDelayMs }: SourcePageEditorProps): React.JSX.Element;
|
|
28
|
+
export default SourcePageEditor;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ObjectUI
|
|
4
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* SourcePageEditor — the Studio editor surface for `kind:'html'` and
|
|
10
|
+
* `kind:'react'` pages (ADR-0080/0081). These pages ARE a `source` string
|
|
11
|
+
* (JSX/HTML or real React), not a region tree — so the structured design
|
|
12
|
+
* canvas does not apply (and would choke on the missing `regions`/`children`).
|
|
13
|
+
* Instead we present a code editor for the `source` field beside a live preview
|
|
14
|
+
* rendered through the runtime SchemaRenderer. Edits patch `draft.source`.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors JsonSourceEditor's Monaco + textarea-fallback + theme handling, but
|
|
17
|
+
* edits ONE field (the source) instead of the whole-record JSON, with a
|
|
18
|
+
* JSX/TSX language mode.
|
|
19
|
+
*/
|
|
20
|
+
import * as React from 'react';
|
|
21
|
+
import { Skeleton } from '@object-ui/components';
|
|
22
|
+
import { SchemaRenderer } from '@object-ui/react';
|
|
23
|
+
import { PreviewShell, PreviewErrorBoundary } from './PreviewShell';
|
|
24
|
+
const LazyMonaco = React.lazy(async () => {
|
|
25
|
+
const mod = await import('@monaco-editor/react');
|
|
26
|
+
return { default: mod.default };
|
|
27
|
+
});
|
|
28
|
+
export function SourcePageEditor({ draft, onPatch, readOnly, fallbackDelayMs = 4000 }) {
|
|
29
|
+
const kind = draft.kind === 'react' ? 'react' : 'html';
|
|
30
|
+
const source = typeof draft.source === 'string' ? draft.source : '';
|
|
31
|
+
const [text, setText] = React.useState(source);
|
|
32
|
+
const lastCommittedRef = React.useRef(source);
|
|
33
|
+
const containerRef = React.useRef(null);
|
|
34
|
+
// Sync from upstream (Reset / inspector edits) without clobbering keystrokes.
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
if (source !== lastCommittedRef.current) {
|
|
37
|
+
setText(source);
|
|
38
|
+
lastCommittedRef.current = source;
|
|
39
|
+
}
|
|
40
|
+
}, [source]);
|
|
41
|
+
// Monaco-unavailable fallback (headless / CSP) → plain textarea.
|
|
42
|
+
const [monacoUnavailable, setMonacoUnavailable] = React.useState(false);
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
if (monacoUnavailable)
|
|
45
|
+
return;
|
|
46
|
+
const id = setTimeout(() => {
|
|
47
|
+
const el = containerRef.current;
|
|
48
|
+
if (!el || !el.querySelector('.view-line'))
|
|
49
|
+
setMonacoUnavailable(true);
|
|
50
|
+
}, fallbackDelayMs);
|
|
51
|
+
return () => clearTimeout(id);
|
|
52
|
+
}, [monacoUnavailable, fallbackDelayMs]);
|
|
53
|
+
const [theme, setTheme] = React.useState(() => typeof document !== 'undefined' && document.documentElement.classList.contains('dark') ? 'vs-dark' : 'light');
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
if (typeof document === 'undefined')
|
|
56
|
+
return;
|
|
57
|
+
const root = document.documentElement;
|
|
58
|
+
const update = () => setTheme(root.classList.contains('dark') ? 'vs-dark' : 'light');
|
|
59
|
+
const obs = new MutationObserver(update);
|
|
60
|
+
obs.observe(root, { attributes: true, attributeFilter: ['class'] });
|
|
61
|
+
return () => obs.disconnect();
|
|
62
|
+
}, []);
|
|
63
|
+
const handleChange = (next) => {
|
|
64
|
+
const v = next ?? '';
|
|
65
|
+
setText(v);
|
|
66
|
+
lastCommittedRef.current = v;
|
|
67
|
+
onPatch?.({ source: v });
|
|
68
|
+
};
|
|
69
|
+
const previewSchema = React.useMemo(() => ({ ...draft, type: draft.type ?? 'page' }), [draft]);
|
|
70
|
+
return (_jsx(PreviewShell, { hint: `page · ${kind} source`, children: _jsxs("div", { className: "grid h-full grid-cols-1 divide-y divide-border lg:grid-cols-2 lg:divide-x lg:divide-y-0", children: [_jsx("div", { ref: containerRef, className: "h-full min-h-[260px] overflow-hidden bg-background", children: monacoUnavailable ? (_jsx("textarea", { value: text, onChange: (e) => handleChange(e.target.value), readOnly: readOnly, spellCheck: false, "aria-label": "Page source", className: "h-full w-full resize-none bg-background p-3 font-mono text-xs leading-relaxed outline-none" })) : (_jsx(React.Suspense, { fallback: _jsx(Skeleton, { className: "h-full w-full" }), children: _jsx(LazyMonaco, { value: text, language: "typescript", path: kind === 'react' ? 'page.tsx' : 'page.html.tsx', theme: theme, onChange: handleChange, options: {
|
|
71
|
+
readOnly,
|
|
72
|
+
minimap: { enabled: false },
|
|
73
|
+
fontSize: 12,
|
|
74
|
+
lineNumbers: 'on',
|
|
75
|
+
scrollBeyondLastLine: false,
|
|
76
|
+
automaticLayout: true,
|
|
77
|
+
folding: true,
|
|
78
|
+
wordWrap: 'on',
|
|
79
|
+
tabSize: 2,
|
|
80
|
+
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
|
|
81
|
+
} }) })) }), _jsx("div", { className: "h-full min-h-[260px] overflow-auto bg-muted/20", children: _jsx(PreviewErrorBoundary, { fallbackHint: "The page source threw while rendering \u2014 fix the code on the left.", children: _jsx(SchemaRenderer, { schema: previewSchema }) }) })] }) }));
|
|
82
|
+
}
|
|
83
|
+
export default SourcePageEditor;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StudioDesignSurface — the open-source WYSIWYG design surface (ADR-0080).
|
|
3
|
+
*
|
|
4
|
+
* Routed as /studio/:packageId/{data|automations|interfaces} — three pillars,
|
|
5
|
+
* each composed AROUND existing renderers (no new editor code):
|
|
6
|
+
* - Interfaces: the real App navigation tree → live canvas (getMetadataPreview)
|
|
7
|
+
* + inspector (getMetadataInspector), edits persisting via draft → publish.
|
|
8
|
+
* - Data: the package's objects → fields + record grid.
|
|
9
|
+
* - Automations: flows → FlowPreview (default OFF / review-then-enable).
|
|
10
|
+
*
|
|
11
|
+
* Open-core boundary: the left AI copilot is NOT part of the open-source
|
|
12
|
+
* surface — it is an injected slot (`aiSlot`) the cloud edition fills.
|
|
13
|
+
*/
|
|
14
|
+
import * as React from 'react';
|
|
15
|
+
export interface StudioDesignSurfaceProps {
|
|
16
|
+
/** Open-core slot — the cloud edition injects its AI copilot panel here. */
|
|
17
|
+
aiSlot?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export declare function StudioDesignSurface({ aiSlot }: StudioDesignSurfaceProps): React.ReactElement;
|
|
20
|
+
export default StudioDesignSurface;
|