@object-ui/app-shell 11.3.0 → 11.4.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 +522 -0
- package/README.md +23 -0
- package/dist/console/ConsoleShell.js +17 -2
- 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/hooks/useConsoleActionRuntime.js +32 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/preview/DraftChangesPanel.d.ts +3 -1
- package/dist/preview/DraftChangesPanel.js +6 -5
- package/dist/utils/deriveRelatedLists.d.ts +20 -5
- package/dist/utils/deriveRelatedLists.js +31 -13
- 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.js +6 -4
- package/dist/views/ObjectView.js +61 -10
- package/dist/views/RecordDetailView.js +131 -104
- package/dist/views/RecordFormPage.js +7 -1
- package/dist/views/RelatedRecordActionsBridge.d.ts +24 -0
- package/dist/views/RelatedRecordActionsBridge.js +114 -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/i18n.d.ts +12 -21
- package/dist/views/metadata-admin/i18n.js +343 -2
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +25 -11
- 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 +11 -7
- 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 +133 -0
- package/dist/views/studio-design/ObjectFormDesigner.d.ts +31 -0
- package/dist/views/studio-design/ObjectFormDesigner.js +226 -0
- package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectSettingsPanel.js +45 -0
- package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectValidationsPanel.js +78 -0
- package/dist/views/studio-design/StudioDesignSurface.js +793 -146
- package/dist/views/studio-design/metadataError.d.ts +23 -0
- package/dist/views/studio-design/metadataError.js +44 -0
- package/dist/views/studio-design/packages-io.d.ts +27 -0
- package/dist/views/studio-design/packages-io.js +61 -0
- package/package.json +42 -39
|
@@ -14,21 +14,32 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
14
14
|
* surface — it is an injected slot (`aiSlot`) the cloud edition fills.
|
|
15
15
|
*/
|
|
16
16
|
import * as React from 'react';
|
|
17
|
-
import { useParams, Link } from 'react-router-dom';
|
|
17
|
+
import { useParams, useNavigate, Link } from 'react-router-dom';
|
|
18
18
|
import { SchemaRenderer, useAdapter, SchemaRendererProvider } from '@object-ui/react';
|
|
19
19
|
import { GridFieldAuthoringProvider } from '@object-ui/components';
|
|
20
20
|
import { ObjectView as PluginObjectView } from '@object-ui/plugin-view';
|
|
21
21
|
import { ListView } from '@object-ui/plugin-list';
|
|
22
|
-
import {
|
|
22
|
+
import { ObjectForm } from '@object-ui/plugin-form';
|
|
23
|
+
import { Boxes, FileText, Database, LayoutDashboard, BarChart3, Table2, Folder, Compass, Workflow, SlidersHorizontal, MousePointer2, Eye, Loader2, Save, Pencil, Check, Plus, X, GitBranch, Rocket, ChevronDown, Lock, ExternalLink, Home as HomeIcon, Shield, } from 'lucide-react';
|
|
23
24
|
import { getMetadataPreview } from '../metadata-admin/preview-registry';
|
|
25
|
+
import { PermissionMatrixEditPage } from '../metadata-admin/PermissionMatrixEditor';
|
|
24
26
|
import { getMetadataInspector } from '../metadata-admin/inspector-registry';
|
|
25
27
|
import { useMetadataClient } from '../metadata-admin/useMetadata';
|
|
28
|
+
import { formatMetadataError, formatPublishFailures } from './metadataError';
|
|
29
|
+
import { t, tFormat, useMetadataLocale } from '../metadata-admin/i18n';
|
|
26
30
|
import { AppNavCanvas } from '../metadata-admin/previews/AppNavCanvas';
|
|
27
|
-
import { readFields, writeFields, newField } from '../metadata-admin/previews/object-fields-io';
|
|
31
|
+
import { readFields, writeFields, newField, toFieldName, toFieldNameLoose, } from '../metadata-admin/previews/object-fields-io';
|
|
32
|
+
import { ObjectFormDesigner } from './ObjectFormDesigner';
|
|
33
|
+
import { ObjectValidationsPanel } from './ObjectValidationsPanel';
|
|
34
|
+
import { ObjectSettingsPanel } from './ObjectSettingsPanel';
|
|
35
|
+
import { fetchPackages, createBasePackage, PACKAGE_ID_RE } from './packages-io';
|
|
36
|
+
import { DraftChangesPanel } from '../../preview/DraftChangesPanel';
|
|
37
|
+
import { toast } from 'sonner';
|
|
28
38
|
const PILLARS = [
|
|
29
39
|
{ key: 'data', label: 'Data', Icon: Database },
|
|
30
40
|
{ key: 'automations', label: 'Automations', Icon: Workflow },
|
|
31
41
|
{ key: 'interfaces', label: 'Interfaces', Icon: LayoutDashboard },
|
|
42
|
+
{ key: 'access', label: 'Access', Icon: Shield },
|
|
32
43
|
];
|
|
33
44
|
const KIND_ICON = {
|
|
34
45
|
group: Folder,
|
|
@@ -75,14 +86,236 @@ function extractDraftBody(resp) {
|
|
|
75
86
|
return null;
|
|
76
87
|
return Object.keys(body).length > 0 ? body : null;
|
|
77
88
|
}
|
|
89
|
+
/** Top-bar package switcher: list app packages (可写 base vs 只读 code), switch by
|
|
90
|
+
* navigation, and create a new writable base inline (POST /packages {id,name}). */
|
|
91
|
+
function PackageSwitcher({ packageId, tab }) {
|
|
92
|
+
const navigate = useNavigate();
|
|
93
|
+
const locale = useMetadataLocale();
|
|
94
|
+
const [open, setOpen] = React.useState(false);
|
|
95
|
+
const [pkgs, setPkgs] = React.useState(null);
|
|
96
|
+
const [creating, setCreating] = React.useState(false);
|
|
97
|
+
const [newName, setNewName] = React.useState('');
|
|
98
|
+
const [newId, setNewId] = React.useState('');
|
|
99
|
+
const [idTouched, setIdTouched] = React.useState(false);
|
|
100
|
+
const [busy, setBusy] = React.useState(false);
|
|
101
|
+
const [err, setErr] = React.useState(null);
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
let cancelled = false;
|
|
104
|
+
fetchPackages()
|
|
105
|
+
.then((parsed) => {
|
|
106
|
+
if (!cancelled)
|
|
107
|
+
setPkgs(parsed);
|
|
108
|
+
})
|
|
109
|
+
.catch(() => {
|
|
110
|
+
/* leave null — switcher still works for navigation-free display */
|
|
111
|
+
});
|
|
112
|
+
return () => {
|
|
113
|
+
cancelled = true;
|
|
114
|
+
};
|
|
115
|
+
}, [packageId]);
|
|
116
|
+
const current = pkgs?.find((p) => p.id === packageId) ?? null;
|
|
117
|
+
const doCreate = React.useCallback(async () => {
|
|
118
|
+
const name = newName.trim();
|
|
119
|
+
const id = newId.trim();
|
|
120
|
+
if (!name || !PACKAGE_ID_RE.test(id))
|
|
121
|
+
return;
|
|
122
|
+
setBusy(true);
|
|
123
|
+
setErr(null);
|
|
124
|
+
try {
|
|
125
|
+
await createBasePackage(id, name);
|
|
126
|
+
toast.success(tFormat('engine.studio.pkg.created', locale, { name }));
|
|
127
|
+
setOpen(false);
|
|
128
|
+
setCreating(false);
|
|
129
|
+
setNewName('');
|
|
130
|
+
setNewId('');
|
|
131
|
+
setIdTouched(false);
|
|
132
|
+
navigate(`/studio/${encodeURIComponent(id)}/data`);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
setErr(formatMetadataError(e));
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
setBusy(false);
|
|
139
|
+
}
|
|
140
|
+
}, [newName, newId, navigate]);
|
|
141
|
+
return (_jsxs("div", { className: "relative", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "flex items-center gap-1.5 whitespace-nowrap rounded-md px-1.5 py-0.5 text-[13px] font-medium hover:bg-muted", title: t('engine.studio.pkg.switchTitle', locale), children: [_jsx(Boxes, { className: "h-4 w-4" }), " ", current?.name ?? packageId, current && !current.writable && (_jsxs("span", { className: "inline-flex items-center gap-0.5 rounded bg-amber-400/15 px-1.5 py-0.5 text-[10px] font-normal text-amber-600 dark:text-amber-300", children: [_jsx(Lock, { className: "h-2.5 w-2.5" }), " ", t('engine.studio.pkg.readonly', locale)] })), _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), open && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40", onClick: () => setOpen(false) }), _jsxs("div", { className: "absolute left-0 top-full z-50 mt-1 w-80 rounded-lg border bg-background p-1.5 shadow-lg", children: [_jsx("p", { className: "px-2 pb-1 pt-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: t('engine.studio.pkg.heading', locale) }), _jsxs("div", { className: "max-h-64 overflow-auto", children: [pkgs === null && _jsx("p", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: t('engine.studio.loading', locale) }), pkgs?.length === 0 && _jsx("p", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: t('engine.studio.pkg.none', locale) }), pkgs?.map((p) => (_jsxs("button", { type: "button", onClick: () => {
|
|
142
|
+
setOpen(false);
|
|
143
|
+
navigate(`/studio/${encodeURIComponent(p.id)}/${tab}`);
|
|
144
|
+
}, className: 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs ' +
|
|
145
|
+
(p.id === packageId ? 'bg-muted font-medium' : 'hover:bg-muted/60'), children: [_jsx(Boxes, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate", children: p.name }), _jsx("span", { className: "block truncate font-mono text-[10px] text-muted-foreground", children: p.id })] }), p.writable ? (_jsx("span", { className: "rounded bg-emerald-400/15 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300", children: t('engine.studio.pkg.writable', locale) })) : (_jsxs("span", { className: "inline-flex items-center gap-0.5 rounded bg-amber-400/15 px-1.5 py-0.5 text-[10px] text-amber-600 dark:text-amber-300", children: [_jsx(Lock, { className: "h-2.5 w-2.5" }), " ", t('engine.studio.pkg.readonly', locale)] }))] }, p.id)))] }), _jsx("div", { className: "mt-1 border-t pt-1.5", children: creating ? (_jsxs("div", { className: "flex flex-col gap-1.5 px-1 pb-1", children: [_jsx("input", { autoFocus: true, value: newName, onChange: (e) => {
|
|
146
|
+
setNewName(e.target.value);
|
|
147
|
+
if (!idTouched) {
|
|
148
|
+
const slug = toFieldNameLoose(e.target.value).replace(/_/g, '-');
|
|
149
|
+
setNewId(slug ? `com.example.${slug}` : '');
|
|
150
|
+
}
|
|
151
|
+
}, onKeyDown: (e) => {
|
|
152
|
+
if (e.key === 'Enter')
|
|
153
|
+
void doCreate();
|
|
154
|
+
if (e.key === 'Escape')
|
|
155
|
+
setCreating(false);
|
|
156
|
+
}, placeholder: t('engine.studio.pkg.namePlaceholder', locale), className: "h-7 w-full rounded-md border bg-background px-2 text-[11px] outline-none focus:ring-1 focus:ring-primary" }), _jsx("input", { value: newId, onChange: (e) => {
|
|
157
|
+
setIdTouched(true);
|
|
158
|
+
setNewId(e.target.value.toLowerCase().replace(/[^a-z0-9_.-]/g, ''));
|
|
159
|
+
}, onKeyDown: (e) => {
|
|
160
|
+
if (e.key === 'Enter')
|
|
161
|
+
void doCreate();
|
|
162
|
+
if (e.key === 'Escape')
|
|
163
|
+
setCreating(false);
|
|
164
|
+
}, placeholder: t('engine.studio.pkg.idPlaceholder', locale), className: "h-7 w-full rounded-md border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-primary" }), err && _jsx("p", { className: "text-[10px] text-destructive", children: err }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("button", { type: "button", onClick: () => void doCreate(), disabled: busy || !newName.trim() || !PACKAGE_ID_RE.test(newId.trim()), className: "inline-flex flex-1 items-center justify-center gap-1 rounded-md bg-primary px-2 py-1 text-[11px] font-medium text-primary-foreground disabled:opacity-50", children: [busy ? _jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : _jsx(Plus, { className: "h-3 w-3" }), t('engine.studio.pkg.createWritable', locale)] }), _jsx("button", { type: "button", onClick: () => setCreating(false), className: "rounded-md border px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted", children: t('engine.studio.cancel', locale) })] })] })) : (_jsxs("button", { type: "button", onClick: () => setCreating(true), className: "flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " ", t('engine.studio.pkg.new', locale)] })) })] })] }))] }));
|
|
165
|
+
}
|
|
78
166
|
export function StudioDesignSurface({ aiSlot }) {
|
|
79
167
|
const params = useParams();
|
|
80
168
|
const packageId = params.packageId ?? 'com.example.showcase';
|
|
81
169
|
const tab = params.tab ?? 'interfaces';
|
|
82
|
-
|
|
170
|
+
const locale = useMetadataLocale();
|
|
171
|
+
// Package-level publish (ADR-0033/0037/0048): edits accumulate as per-item
|
|
172
|
+
// drafts STAMPED with this package (each save passes packageId → the draft row's
|
|
173
|
+
// sys_metadata.package_id). Publishing promotes exactly THIS package's drafts in
|
|
174
|
+
// one atomic pass (POST /packages/:id/publish-drafts), reviewed as a whole in
|
|
175
|
+
// DraftChangesPanel. There is no per-item publish.
|
|
176
|
+
const [changesOpen, setChangesOpen] = React.useState(false);
|
|
177
|
+
const [pendingCount, setPendingCount] = React.useState(null);
|
|
178
|
+
const [publishing, setPublishing] = React.useState(false);
|
|
179
|
+
const [publishNonce, setPublishNonce] = React.useState(0); // ↑ → pillars re-read the published baseline
|
|
180
|
+
const [draftNonce, setDraftNonce] = React.useState(0); // ↑ → refresh the pending-draft count
|
|
181
|
+
const refreshPending = React.useCallback(async () => {
|
|
182
|
+
try {
|
|
183
|
+
const res = await fetch(`/api/v1/meta/_drafts?packageId=${encodeURIComponent(packageId)}`, {
|
|
184
|
+
credentials: 'include',
|
|
185
|
+
headers: { Accept: 'application/json' },
|
|
186
|
+
cache: 'no-store',
|
|
187
|
+
});
|
|
188
|
+
if (!res.ok)
|
|
189
|
+
return setPendingCount(null);
|
|
190
|
+
const data = (await res.json());
|
|
191
|
+
const list = (Array.isArray(data) ? data : (data?.drafts ?? []));
|
|
192
|
+
setPendingCount(list.length);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
setPendingCount(null);
|
|
196
|
+
}
|
|
197
|
+
}, [packageId]);
|
|
198
|
+
React.useEffect(() => {
|
|
199
|
+
void refreshPending();
|
|
200
|
+
}, [refreshPending, publishNonce, draftNonce]);
|
|
201
|
+
const doPublish = React.useCallback(async () => {
|
|
202
|
+
setPublishing(true);
|
|
203
|
+
try {
|
|
204
|
+
const res = await fetch(`/api/v1/packages/${encodeURIComponent(packageId)}/publish-drafts`, {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
credentials: 'include',
|
|
207
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
208
|
+
body: '{}',
|
|
209
|
+
});
|
|
210
|
+
const payload = (await res.json().catch(() => null));
|
|
211
|
+
if (!res.ok || payload?.success === false) {
|
|
212
|
+
// Hard failure (e.g. package not found) — carry the field-anchored issues.
|
|
213
|
+
throw Object.assign(new Error(payload?.error?.message || `HTTP ${res.status}`), {
|
|
214
|
+
issues: payload?.error?.details?.issues,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const failed = payload?.data?.failed ?? [];
|
|
218
|
+
if (failed.length > 0) {
|
|
219
|
+
// Partial publish: some drafts did NOT go live. The server returns 200
|
|
220
|
+
// with them buried in `failed[]`, so the UI used to claim success and
|
|
221
|
+
// swallow the reason — surface which drafts failed and why instead.
|
|
222
|
+
toast.error(formatPublishFailures(failed));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
toast.success(t('engine.studio.publishedAll', locale));
|
|
226
|
+
setChangesOpen(false);
|
|
227
|
+
}
|
|
228
|
+
setPublishNonce((n) => n + 1);
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
toast.error(formatMetadataError(e));
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
setPublishing(false);
|
|
235
|
+
}
|
|
236
|
+
await refreshPending();
|
|
237
|
+
}, [packageId, refreshPending]);
|
|
238
|
+
const onDraftSaved = React.useCallback(() => setDraftNonce((n) => n + 1), []);
|
|
239
|
+
const hasPending = (pendingCount ?? 0) > 0;
|
|
240
|
+
// Builder → running-app bridge (Airtable's Launch): the builder edits the
|
|
241
|
+
// package (设计界面), the app is its published front-end. If this package
|
|
242
|
+
// ships an app, offer 打开应用 — opened in a new tab so the builder context
|
|
243
|
+
// survives. (App → builder is the reverse bridge, tracked separately.)
|
|
244
|
+
const shellNavigate = useNavigate();
|
|
245
|
+
const shellClient = useMetadataClient();
|
|
246
|
+
const [packageApp, setPackageApp] = React.useState(null);
|
|
247
|
+
// 创建应用 (package has no app yet): create a draft `app` item — the published
|
|
248
|
+
// front-end's on-ramp. The button flips to 打开应用 after the package publish.
|
|
249
|
+
const [appCreating, setAppCreating] = React.useState(false);
|
|
250
|
+
const [appLabel, setAppLabel] = React.useState('');
|
|
251
|
+
const [appName, setAppName] = React.useState('');
|
|
252
|
+
const [appNameTouched, setAppNameTouched] = React.useState(false);
|
|
253
|
+
const [appBusy, setAppBusy] = React.useState(false);
|
|
254
|
+
const [appDraftPending, setAppDraftPending] = React.useState(null);
|
|
255
|
+
const doCreateApp = React.useCallback(async () => {
|
|
256
|
+
const label = appLabel.trim();
|
|
257
|
+
const name = toFieldName(appName.trim() || label);
|
|
258
|
+
if (!label || !name || name === 'field')
|
|
259
|
+
return;
|
|
260
|
+
setAppBusy(true);
|
|
261
|
+
try {
|
|
262
|
+
await shellClient.save('app', name, { name, label, active: true, navigation: [] }, { mode: 'draft', packageId });
|
|
263
|
+
toast.success(tFormat('engine.studio.app.savedDraft', locale, { label }));
|
|
264
|
+
setAppDraftPending(label);
|
|
265
|
+
setAppCreating(false);
|
|
266
|
+
setAppLabel('');
|
|
267
|
+
setAppName('');
|
|
268
|
+
setAppNameTouched(false);
|
|
269
|
+
setDraftNonce((n) => n + 1);
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
toast.error(formatMetadataError(e));
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
setAppBusy(false);
|
|
276
|
+
}
|
|
277
|
+
}, [appLabel, appName, shellClient, packageId]);
|
|
278
|
+
React.useEffect(() => {
|
|
279
|
+
let cancelled = false;
|
|
280
|
+
(async () => {
|
|
281
|
+
try {
|
|
282
|
+
const apps = (await shellClient.list('app', { packageId }));
|
|
283
|
+
const first = (apps || [])
|
|
284
|
+
.map((a) => ({ name: String(a.name ?? ''), label: String(a.label ?? a.name ?? '') }))
|
|
285
|
+
.filter((a) => a.name)[0];
|
|
286
|
+
if (!cancelled)
|
|
287
|
+
setPackageApp(first ?? null);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
if (!cancelled)
|
|
291
|
+
setPackageApp(null);
|
|
292
|
+
}
|
|
293
|
+
})();
|
|
294
|
+
return () => {
|
|
295
|
+
cancelled = true;
|
|
296
|
+
};
|
|
297
|
+
}, [shellClient, packageId, publishNonce]);
|
|
298
|
+
return (_jsxs("div", { className: "flex h-screen w-full overflow-hidden bg-background text-foreground", children: [aiSlot ? _jsx("aside", { className: "w-64 shrink-0 overflow-auto border-r bg-muted/40", children: aiSlot }) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("header", { className: "flex items-center gap-3 border-b px-3 py-2", children: [_jsx("button", { type: "button", onClick: () => shellNavigate('/home'), title: t('engine.studio.home', locale), className: "rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground", children: _jsx(HomeIcon, { className: "h-4 w-4" }) }), _jsx(PackageSwitcher, { packageId: packageId, tab: tab }), _jsx("span", { className: "text-muted-foreground", children: "\u00B7" }), _jsx("nav", { className: "flex gap-1", children: PILLARS.map((p) => (_jsxs(Link, { to: `/studio/${packageId}/${p.key}`, className: 'inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs transition-colors ' +
|
|
83
299
|
(tab === p.key
|
|
84
300
|
? 'bg-primary/10 font-medium text-primary'
|
|
85
|
-
: 'text-muted-foreground hover:bg-muted hover:text-foreground'), children: [_jsx(p.Icon, { className: "h-3.5 w-3.5" }), p.
|
|
301
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'), children: [_jsx(p.Icon, { className: "h-3.5 w-3.5" }), t(`engine.studio.pillar.${p.key}`, locale)] }, p.key))) }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [packageApp ? (_jsxs("button", { type: "button", onClick: () => window.open(`/apps/${encodeURIComponent(packageApp.name)}`, '_blank'), title: tFormat('engine.studio.app.openTitle', locale, { label: packageApp.label }), className: "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), t('engine.studio.app.open', locale)] })) : appDraftPending ? (_jsx("span", { title: t('engine.studio.app.willOpenAfterPublish', locale), className: "rounded bg-amber-400/15 px-2 py-0.5 text-[11px] text-amber-600 dark:text-amber-300", children: tFormat('engine.studio.app.pending', locale, { label: appDraftPending }) })) : (_jsxs("div", { className: "relative", children: [_jsxs("button", { type: "button", onClick: () => setAppCreating((v) => !v), title: t('engine.studio.app.noneTitle', locale), className: "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), t('engine.studio.app.create', locale)] }), appCreating && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40", onClick: () => setAppCreating(false) }), _jsxs("div", { className: "absolute right-0 top-full z-50 mt-1 flex w-72 flex-col gap-1.5 rounded-lg border bg-background p-2 shadow-lg", children: [_jsx("input", { autoFocus: true, value: appLabel, onChange: (e) => {
|
|
302
|
+
setAppLabel(e.target.value);
|
|
303
|
+
if (!appNameTouched)
|
|
304
|
+
setAppName(toFieldNameLoose(e.target.value));
|
|
305
|
+
}, onKeyDown: (e) => {
|
|
306
|
+
if (e.key === 'Enter')
|
|
307
|
+
void doCreateApp();
|
|
308
|
+
if (e.key === 'Escape')
|
|
309
|
+
setAppCreating(false);
|
|
310
|
+
}, placeholder: t('engine.studio.app.namePlaceholder', locale), className: "h-7 w-full rounded-md border bg-background px-2 text-[11px] outline-none focus:ring-1 focus:ring-primary" }), _jsx("input", { value: appName, onChange: (e) => {
|
|
311
|
+
setAppNameTouched(true);
|
|
312
|
+
setAppName(toFieldNameLoose(e.target.value));
|
|
313
|
+
}, onKeyDown: (e) => {
|
|
314
|
+
if (e.key === 'Enter')
|
|
315
|
+
void doCreateApp();
|
|
316
|
+
if (e.key === 'Escape')
|
|
317
|
+
setAppCreating(false);
|
|
318
|
+
}, placeholder: t('engine.studio.app.idPlaceholder', locale), className: "h-7 w-full rounded-md border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-primary" }), _jsxs("button", { type: "button", onClick: () => void doCreateApp(), disabled: appBusy || !appLabel.trim() || !toFieldName(appName.trim() || appLabel) || toFieldName(appName.trim() || appLabel) === 'field', className: "inline-flex items-center justify-center gap-1 rounded-md bg-primary px-2 py-1 text-[11px] font-medium text-primary-foreground disabled:opacity-50", children: [appBusy ? _jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : _jsx(Plus, { className: "h-3 w-3" }), t('engine.studio.createDraft', locale)] })] })] }))] })), _jsxs("button", { type: "button", onClick: () => setChangesOpen(true), className: "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(GitBranch, { className: "h-3.5 w-3.5" }), t('engine.studio.changes', locale), hasPending ? ` · ${pendingCount}` : ''] }), _jsxs("button", { type: "button", onClick: doPublish, disabled: publishing || !hasPending, title: hasPending ? t('engine.studio.publishTitle', locale) : t('engine.studio.publishNoneTitle', locale), className: "inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50", children: [publishing ? _jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : _jsx(Rocket, { className: "h-3.5 w-3.5" }), t('engine.studio.publish', locale)] })] })] }), _jsx("div", { className: "min-h-0 flex-1", children: tab === 'data' ? (_jsx(DataPillar, { packageId: packageId, publishNonce: publishNonce, onDraftSaved: onDraftSaved })) : tab === 'automations' ? (_jsx(AutomationsPillar, { packageId: packageId, publishNonce: publishNonce, onDraftSaved: onDraftSaved })) : tab === 'access' ? (_jsx(AccessPillar, { packageId: packageId, publishNonce: publishNonce, onDraftSaved: onDraftSaved })) : (_jsx(InterfacesPillar, { packageId: packageId, publishNonce: publishNonce, draftNonce: draftNonce, onDraftSaved: onDraftSaved, onCreateApp: () => setAppCreating(true) })) })] }), _jsx(DraftChangesPanel, { open: changesOpen, onOpenChange: setChangesOpen, packageId: packageId })] }));
|
|
86
319
|
}
|
|
87
320
|
/** Recursive App-navigation tree (groups + typed leaves). */
|
|
88
321
|
function NavTree({ nodes, active, onPick, }) {
|
|
@@ -98,9 +331,60 @@ function NavTree({ nodes, active, onPick, }) {
|
|
|
98
331
|
}) }));
|
|
99
332
|
}
|
|
100
333
|
/** Interfaces pillar — real App nav · live canvas · inspector. */
|
|
101
|
-
|
|
334
|
+
/**
|
|
335
|
+
* StudioNavItemInspector — right-panel editor for the selected nav item while
|
|
336
|
+
* editing an app's navigation. The Studio adds flat top-level items
|
|
337
|
+
* (`navigation[i]`), so binding is a business-friendly object picker rather
|
|
338
|
+
* than the raw path field of the generic AppNavInspector: picking an object
|
|
339
|
+
* writes `{ object }` (which the runtime resolves to that object's record
|
|
340
|
+
* list) and, if the label is still the placeholder, adopts the object's label.
|
|
341
|
+
*/
|
|
342
|
+
function StudioNavItemInspector({ navId, appDraft, objects, onNavPatch, onClear, }) {
|
|
343
|
+
const locale = useMetadataLocale();
|
|
344
|
+
const idx = React.useMemo(() => {
|
|
345
|
+
const m = /^navigation\[(\d+)\]$/.exec(navId);
|
|
346
|
+
return m ? Number(m[1]) : -1;
|
|
347
|
+
}, [navId]);
|
|
348
|
+
const nav = React.useMemo(() => (Array.isArray(appDraft.navigation) ? appDraft.navigation : []), [appDraft]);
|
|
349
|
+
const node = idx >= 0 ? nav[idx] : null;
|
|
350
|
+
if (!node) {
|
|
351
|
+
return (_jsx("div", { className: "px-2 py-10 text-center text-xs text-muted-foreground", children: t('engine.studio.nav.selectItem', locale) }));
|
|
352
|
+
}
|
|
353
|
+
const patch = (updates) => {
|
|
354
|
+
onNavPatch({ navigation: nav.map((n, i) => (i === idx ? { ...n, ...updates } : n)) });
|
|
355
|
+
};
|
|
356
|
+
const boundObject = String(node.object ?? node.objectName ?? '');
|
|
357
|
+
const curLabel = String(node.label ?? node.title ?? node.name ?? '');
|
|
358
|
+
// A nav card is a placeholder until its label is edited or a target adopts a
|
|
359
|
+
// real label. Match both the legacy English sentinel and the locale-specific
|
|
360
|
+
// default from AppNavCanvas so items created in any locale are recognized.
|
|
361
|
+
const isPlaceholder = !curLabel || curLabel === 'New item' || curLabel === t('engine.appNav.newItem', locale);
|
|
362
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-[11px] font-medium text-muted-foreground", children: t('engine.studio.nav.label', locale) }), _jsx("input", { value: curLabel, onChange: (e) => patch({ label: e.target.value }), placeholder: t('engine.studio.nav.labelPlaceholder', locale), className: "w-full rounded border bg-background px-2 py-1 text-xs" })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1 block text-[11px] font-medium text-muted-foreground", children: t('engine.studio.nav.linkObject', locale) }), _jsxs("select", { value: boundObject, onChange: (e) => {
|
|
363
|
+
const objName = e.target.value;
|
|
364
|
+
const obj = objects.find((o) => o.name === objName);
|
|
365
|
+
if (!objName) {
|
|
366
|
+
// Unbind → back to an (invalid, dropped-on-save) placeholder.
|
|
367
|
+
patch({ type: undefined, objectName: undefined, object: undefined });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// Emit a spec-complete ObjectNavItem: the app schema's nav is a
|
|
371
|
+
// discriminated union on `type` and BaseNavItem requires a
|
|
372
|
+
// snake_case `id`. Missing either fails "navigation.0: Invalid
|
|
373
|
+
// input" at save. `object`/`path` are cleared so no stray keys
|
|
374
|
+
// linger from the blank placeholder.
|
|
375
|
+
patch({
|
|
376
|
+
id: node.id || `nav_${objName}`,
|
|
377
|
+
type: 'object',
|
|
378
|
+
objectName: objName,
|
|
379
|
+
object: undefined,
|
|
380
|
+
path: undefined,
|
|
381
|
+
label: isPlaceholder && obj ? obj.label : curLabel,
|
|
382
|
+
});
|
|
383
|
+
}, className: "w-full rounded border bg-background px-2 py-1 text-xs", children: [_jsx("option", { value: "", children: t('engine.studio.nav.chooseObject', locale) }), objects.map((o) => (_jsxs("option", { value: o.name, children: [o.label, " (", o.name, ")"] }, o.name)))] }), _jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: boundObject ? t('engine.studio.nav.boundHint', locale) : t('engine.studio.nav.unboundHint', locale) }), objects.length === 0 && (_jsx("p", { className: "mt-1 text-[11px] text-amber-600 dark:text-amber-400", children: t('engine.studio.nav.noObjects', locale) }))] }), _jsx("button", { type: "button", onClick: onClear, className: "text-[11px] text-muted-foreground underline-offset-2 hover:underline", children: t('engine.studio.deselect', locale) })] }));
|
|
384
|
+
}
|
|
385
|
+
function InterfacesPillar({ packageId, publishNonce = 0, draftNonce = 0, onDraftSaved, onCreateApp, }) {
|
|
102
386
|
const client = useMetadataClient();
|
|
103
|
-
const locale =
|
|
387
|
+
const locale = useMetadataLocale();
|
|
104
388
|
const [appLabel, setAppLabel] = React.useState(packageId);
|
|
105
389
|
const [appName, setAppName] = React.useState(null);
|
|
106
390
|
const [appDraft, setAppDraft] = React.useState({});
|
|
@@ -118,22 +402,77 @@ function InterfacesPillar({ packageId }) {
|
|
|
118
402
|
const [saving, setSaving] = React.useState(false);
|
|
119
403
|
const [hasDraft, setHasDraft] = React.useState(false);
|
|
120
404
|
const [error, setError] = React.useState(null);
|
|
121
|
-
//
|
|
405
|
+
// App resolution status — tells "still loading" apart from "this package has
|
|
406
|
+
// no app", so the canvas shows a real empty state instead of an endless
|
|
407
|
+
// spinner.
|
|
408
|
+
const [appStatus, setAppStatus] = React.useState('loading');
|
|
409
|
+
// Objects in THIS package (published ∪ draft) — the nav item inspector's
|
|
410
|
+
// object picker, so nav can be wired to sibling objects before publishing.
|
|
411
|
+
const [pkgObjects, setPkgObjects] = React.useState([]);
|
|
122
412
|
React.useEffect(() => {
|
|
123
413
|
let cancelled = false;
|
|
124
414
|
(async () => {
|
|
125
415
|
try {
|
|
126
|
-
const
|
|
416
|
+
const [pub, drafts] = await Promise.all([
|
|
417
|
+
client.list('object', { packageId }),
|
|
418
|
+
client.listDrafts({ packageId, type: 'object' }).catch(() => []),
|
|
419
|
+
]);
|
|
127
420
|
if (cancelled)
|
|
128
421
|
return;
|
|
129
|
-
const
|
|
130
|
-
|
|
422
|
+
const byName = new Map();
|
|
423
|
+
for (const raw of [...(pub || []), ...(drafts || [])]) {
|
|
424
|
+
const o = raw;
|
|
425
|
+
const name = String(o.name ?? '');
|
|
426
|
+
if (!name || byName.has(name))
|
|
427
|
+
continue;
|
|
428
|
+
byName.set(name, { name, label: String(o.label ?? o.name ?? name) });
|
|
429
|
+
}
|
|
430
|
+
setPkgObjects([...byName.values()]);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
/* non-fatal — picker just stays empty */
|
|
434
|
+
}
|
|
435
|
+
})();
|
|
436
|
+
return () => {
|
|
437
|
+
cancelled = true;
|
|
438
|
+
};
|
|
439
|
+
}, [client, packageId, publishNonce, draftNonce]);
|
|
440
|
+
// Resolve THIS package's App → load its navigation tree. The query is scoped
|
|
441
|
+
// to the package (`list('app', { packageId })`) so a design surface only ever
|
|
442
|
+
// shows the current package's app — never another package's. `list()` sees
|
|
443
|
+
// published metadata only, so a freshly-created (unpublished) app is found via
|
|
444
|
+
// `listDrafts()` instead, keeping it designable before its first publish.
|
|
445
|
+
React.useEffect(() => {
|
|
446
|
+
let cancelled = false;
|
|
447
|
+
setAppStatus('loading');
|
|
448
|
+
(async () => {
|
|
449
|
+
try {
|
|
450
|
+
const published = (await client.list('app', { packageId }));
|
|
451
|
+
if (cancelled)
|
|
452
|
+
return;
|
|
453
|
+
let name = published?.[0]?.name ? String(published[0].name) : null;
|
|
454
|
+
let label = published?.[0]
|
|
455
|
+
? String(published[0].label ?? published[0].name ?? packageId)
|
|
456
|
+
: packageId;
|
|
457
|
+
if (!name) {
|
|
458
|
+
const drafts = await client.listDrafts({ packageId, type: 'app' });
|
|
459
|
+
if (cancelled)
|
|
460
|
+
return;
|
|
461
|
+
const d = drafts?.[0];
|
|
462
|
+
if (d?.name) {
|
|
463
|
+
name = String(d.name);
|
|
464
|
+
label = String(d.name);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (!name) {
|
|
468
|
+
setAppStatus('missing');
|
|
131
469
|
return;
|
|
132
|
-
|
|
133
|
-
|
|
470
|
+
}
|
|
471
|
+
setAppLabel(label);
|
|
472
|
+
setAppName(name);
|
|
134
473
|
const [layRaw, appDraftResp] = await Promise.all([
|
|
135
|
-
client.layered('app',
|
|
136
|
-
client.getDraft('app',
|
|
474
|
+
client.layered('app', name),
|
|
475
|
+
client.getDraft('app', name).catch(() => null),
|
|
137
476
|
]);
|
|
138
477
|
if (cancelled)
|
|
139
478
|
return;
|
|
@@ -141,8 +480,12 @@ function InterfacesPillar({ packageId }) {
|
|
|
141
480
|
const eff = (lay.effective ?? lay.code ?? {});
|
|
142
481
|
const appDraftBody = extractDraftBody(appDraftResp);
|
|
143
482
|
const body = appDraftBody ? { ...eff, ...appDraftBody } : eff;
|
|
483
|
+
if (typeof body.label === 'string' || typeof body.name === 'string') {
|
|
484
|
+
setAppLabel(String(body.label ?? body.name ?? label));
|
|
485
|
+
}
|
|
144
486
|
setAppDraft(body);
|
|
145
487
|
setNavHasDraft(!!appDraftBody);
|
|
488
|
+
setAppStatus('ready');
|
|
146
489
|
const tree = Array.isArray(body.navigation) ? body.navigation : [];
|
|
147
490
|
// auto-open the first resolvable leaf
|
|
148
491
|
const firstLeaf = (function find(nodes) {
|
|
@@ -163,14 +506,16 @@ function InterfacesPillar({ packageId }) {
|
|
|
163
506
|
setCurrent((cur) => cur ?? firstLeaf);
|
|
164
507
|
}
|
|
165
508
|
catch (e) {
|
|
166
|
-
if (!cancelled)
|
|
167
|
-
setError(
|
|
509
|
+
if (!cancelled) {
|
|
510
|
+
setError(formatMetadataError(e));
|
|
511
|
+
setAppStatus('missing');
|
|
512
|
+
}
|
|
168
513
|
}
|
|
169
514
|
})();
|
|
170
515
|
return () => {
|
|
171
516
|
cancelled = true;
|
|
172
517
|
};
|
|
173
|
-
}, [client, packageId]);
|
|
518
|
+
}, [client, packageId, publishNonce, draftNonce]);
|
|
174
519
|
const Preview = getMetadataPreview(current?.type ?? '');
|
|
175
520
|
const Inspector = getMetadataInspector(current?.type ?? '');
|
|
176
521
|
// Object leaves render as a runtime records grid (preview = runtime); schema
|
|
@@ -204,7 +549,7 @@ function InterfacesPillar({ packageId }) {
|
|
|
204
549
|
}
|
|
205
550
|
catch (e) {
|
|
206
551
|
if (!cancelled)
|
|
207
|
-
setError(
|
|
552
|
+
setError(formatMetadataError(e));
|
|
208
553
|
}
|
|
209
554
|
finally {
|
|
210
555
|
if (!cancelled)
|
|
@@ -214,38 +559,24 @@ function InterfacesPillar({ packageId }) {
|
|
|
214
559
|
return () => {
|
|
215
560
|
cancelled = true;
|
|
216
561
|
};
|
|
217
|
-
}, [client, current, isEditable]);
|
|
562
|
+
}, [client, current, isEditable, publishNonce]);
|
|
218
563
|
const onPatch = React.useCallback((patch) => setDraft((d) => ({ ...d, ...patch })), []);
|
|
219
564
|
const doSave = React.useCallback(async () => {
|
|
220
565
|
if (!current)
|
|
221
566
|
return;
|
|
222
567
|
setSaving('draft');
|
|
223
568
|
try {
|
|
224
|
-
await client.save(current.type, current.name, draft, { mode: 'draft' });
|
|
569
|
+
await client.save(current.type, current.name, draft, { mode: 'draft', packageId });
|
|
225
570
|
setHasDraft(true);
|
|
571
|
+
onDraftSaved?.();
|
|
226
572
|
}
|
|
227
573
|
catch (e) {
|
|
228
|
-
setError(
|
|
574
|
+
setError(formatMetadataError(e));
|
|
229
575
|
}
|
|
230
576
|
finally {
|
|
231
577
|
setSaving(false);
|
|
232
578
|
}
|
|
233
|
-
}, [client, current, draft]);
|
|
234
|
-
const doPublish = React.useCallback(async () => {
|
|
235
|
-
if (!current)
|
|
236
|
-
return;
|
|
237
|
-
setSaving('publish');
|
|
238
|
-
try {
|
|
239
|
-
await client.publish(current.type, current.name);
|
|
240
|
-
setHasDraft(false);
|
|
241
|
-
}
|
|
242
|
-
catch (e) {
|
|
243
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
244
|
-
}
|
|
245
|
-
finally {
|
|
246
|
-
setSaving(false);
|
|
247
|
-
}
|
|
248
|
-
}, [client, current]);
|
|
579
|
+
}, [client, current, draft, onDraftSaved]);
|
|
249
580
|
// nav editing — patch appDraft.navigation, then save/publish the App overlay
|
|
250
581
|
const onNavPatch = React.useCallback((patch) => {
|
|
251
582
|
setAppDraft((d) => ({ ...d, ...patch }));
|
|
@@ -256,37 +587,39 @@ function InterfacesPillar({ packageId }) {
|
|
|
256
587
|
return;
|
|
257
588
|
setNavSaving('draft');
|
|
258
589
|
try {
|
|
259
|
-
|
|
590
|
+
// "Add nav item" inserts a blank placeholder that only becomes a valid,
|
|
591
|
+
// spec-conformant item once a target is picked in the inspector. Drop
|
|
592
|
+
// still-untargeted placeholders (no `type`) so one stray blank can't fail
|
|
593
|
+
// the whole app's spec validation ("navigation.N: Invalid input"), and
|
|
594
|
+
// backfill a snake_case id defensively.
|
|
595
|
+
const rawNav = Array.isArray(appDraft.navigation) ? appDraft.navigation : [];
|
|
596
|
+
const cleanedNav = rawNav
|
|
597
|
+
.filter((n) => n && typeof n.type === 'string')
|
|
598
|
+
.map((n, i) => {
|
|
599
|
+
const item = n;
|
|
600
|
+
return typeof item.id === 'string' && item.id ? item : { ...item, id: `nav_item_${i + 1}` };
|
|
601
|
+
});
|
|
602
|
+
await client.save('app', appName, { ...appDraft, navigation: cleanedNav }, { mode: 'draft', packageId });
|
|
260
603
|
setNavHasDraft(true);
|
|
261
604
|
setNavDirty(false);
|
|
605
|
+
onDraftSaved?.();
|
|
262
606
|
}
|
|
263
607
|
catch (e) {
|
|
264
|
-
setError(
|
|
608
|
+
setError(formatMetadataError(e));
|
|
265
609
|
}
|
|
266
610
|
finally {
|
|
267
611
|
setNavSaving(false);
|
|
268
612
|
}
|
|
269
|
-
}, [client, appName, appDraft]);
|
|
270
|
-
|
|
271
|
-
if (!appName)
|
|
272
|
-
return;
|
|
273
|
-
setNavSaving('publish');
|
|
274
|
-
try {
|
|
275
|
-
await client.publish('app', appName);
|
|
276
|
-
setNavHasDraft(false);
|
|
277
|
-
}
|
|
278
|
-
catch (e) {
|
|
279
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
280
|
-
}
|
|
281
|
-
finally {
|
|
282
|
-
setNavSaving(false);
|
|
283
|
-
}
|
|
284
|
-
}, [client, appName]);
|
|
285
|
-
return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-1.5", children: [current ? (_jsxs("span", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [_jsx("span", { className: "text-[13px] font-medium text-foreground", children: current.label }), _jsxs("span", { className: "rounded bg-muted px-1.5 py-0.5", children: [current.type, " \u00B7 ", current.name] })] })) : (_jsx("span", { className: "text-[11px] text-muted-foreground", children: "\u4ECE\u5DE6\u4FA7\u9009\u62E9\u4E00\u4E2A\u83DC\u5355\u9879" })), hasDraft && (_jsx("span", { className: "rounded bg-amber-400/15 px-2 py-0.5 text-[11px] text-amber-600 dark:text-amber-300", children: "\u672A\u53D1\u5E03\u8349\u7A3F" })), _jsxs("button", { onClick: doSave, disabled: !current || !isEditable || !!saving, className: "ml-auto inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs hover:bg-muted disabled:opacity-50", children: [saving === 'draft' ? _jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : _jsx(Save, { className: "h-3.5 w-3.5" }), "\u4FDD\u5B58\u8349\u7A3F"] }), _jsxs("button", { onClick: doPublish, disabled: !current || !isEditable || !hasDraft || !!saving, className: "inline-flex items-center gap-1 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50", children: [saving === 'publish' ? _jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : null, "\u53D1\u5E03"] })] }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsxs("nav", { className: (editNav ? 'w-72' : 'w-52') + ' flex shrink-0 flex-col border-r', children: [_jsxs("div", { className: "shrink-0 border-b px-2 py-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-1", children: [_jsxs("p", { className: "truncate text-[11px] font-medium text-muted-foreground", children: [appLabel, " \u00B7 \u5BFC\u822A"] }), _jsx("button", { type: "button", onClick: () => {
|
|
613
|
+
}, [client, appName, appDraft, onDraftSaved]);
|
|
614
|
+
return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-1.5", children: [current ? (_jsxs("span", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [_jsx("span", { className: "text-[13px] font-medium text-foreground", children: current.label }), _jsxs("span", { className: "rounded bg-muted px-1.5 py-0.5", children: [current.type, " \u00B7 ", current.name] })] })) : (_jsx("span", { className: "text-[11px] text-muted-foreground", children: t('engine.studio.if.pickLeft', locale) })), hasDraft && (_jsx("span", { className: "rounded bg-amber-400/15 px-2 py-0.5 text-[11px] text-amber-600 dark:text-amber-300", children: t('engine.studio.unpublishedDraft', locale) })), _jsxs("button", { onClick: doSave, disabled: !current || !isEditable || !!saving, className: "ml-auto inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs hover:bg-muted disabled:opacity-50", children: [saving === 'draft' ? _jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : _jsx(Save, { className: "h-3.5 w-3.5" }), t('engine.studio.saveDraft', locale)] })] }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsxs("nav", { className: (editNav ? 'w-72' : 'w-52') + ' flex shrink-0 flex-col border-r', children: [_jsxs("div", { className: "shrink-0 border-b px-2 py-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-1", children: [_jsx("p", { className: "truncate text-[11px] font-medium text-muted-foreground", children: tFormat('engine.studio.if.navHeading', locale, { app: appLabel }) }), appStatus === 'ready' && (_jsx("button", { type: "button", onClick: () => {
|
|
286
615
|
setEditNav((v) => !v);
|
|
287
616
|
setNavSel(null);
|
|
288
|
-
}, title: editNav ? '
|
|
289
|
-
(editNav ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-muted'), children: editNav ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-3 w-3" }), "
|
|
617
|
+
}, title: editNav ? t('engine.studio.if.doneEditTitle', locale) : t('engine.studio.if.editNavTitle', locale), className: 'inline-flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-[10px] ' +
|
|
618
|
+
(editNav ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-muted'), children: editNav ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "h-3 w-3" }), " ", t('engine.studio.done', locale)] })) : (_jsxs(_Fragment, { children: [_jsx(Pencil, { className: "h-3 w-3" }), " ", t('engine.studio.edit', locale)] })) }))] }), editNav && (_jsxs("div", { className: "mt-1.5 flex items-center gap-1.5", children: [navHasDraft && (_jsx("span", { className: "rounded bg-amber-400/15 px-1.5 py-0.5 text-[10px] text-amber-600 dark:text-amber-300", children: t('engine.studio.unpublished', locale) })), _jsxs("button", { onClick: doNavSave, disabled: !navDirty || !!navSaving, className: "inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] hover:bg-muted disabled:opacity-50", children: [navSaving === 'draft' ? (_jsx(Loader2, { className: "h-3 w-3 animate-spin" })) : (_jsx(Save, { className: "h-3 w-3" })), t('engine.studio.saveDraft', locale)] })] }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-2", children: appStatus === 'missing' && !error ? (_jsxs("div", { className: "px-2 py-3", children: [_jsx("p", { className: "text-[11px] text-muted-foreground", children: t('engine.studio.if.noApp', locale) }), onCreateApp && (_jsxs("button", { type: "button", onClick: onCreateApp, className: "mt-2 inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] hover:bg-muted", children: [_jsx(Plus, { className: "h-3 w-3" }), " ", t('engine.studio.app.create', locale)] }))] })) : editNav ? (_jsx(AppNavCanvas, { draft: appDraft, rootKey: "navigation", onPatch: onNavPatch, selection: navSel, onSelectionChange: (s) => setNavSel(s ? { kind: s.kind, id: s.id } : null) })) : navTree.length === 0 ? (_jsx("p", { className: "px-2 py-3 text-[11px] text-muted-foreground", children: error
|
|
619
|
+
? t('engine.studio.loadFailed', locale)
|
|
620
|
+
: appStatus === 'loading'
|
|
621
|
+
? t('engine.studio.loading', locale)
|
|
622
|
+
: t('engine.studio.if.noNavItems', locale) })) : (_jsx(NavTree, { nodes: navTree, active: current, onPick: setCurrent })) })] }), _jsxs("main", { className: "min-w-0 flex-1 overflow-auto bg-muted/30 p-4", children: [_jsxs("div", { className: "mb-3 flex items-center gap-2", children: [_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[11px] text-primary", children: [_jsx(Eye, { className: "h-3 w-3" }), " ", t('engine.studio.if.previewIsRuntime', locale)] }), current && (_jsxs("span", { className: "text-[11px] text-muted-foreground", children: [current.type, " \u00B7 ", current.name] }))] }), error && (_jsx("div", { className: "mb-3 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive whitespace-pre-line", children: error })), _jsx("div", { className: "rounded-lg border bg-background p-4", children: appStatus === 'missing' && !error ? (_jsxs("div", { className: "flex flex-col items-center justify-center gap-3 py-16 text-center", children: [_jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(LayoutDashboard, { className: "h-6 w-6 text-muted-foreground" }) }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: t('engine.studio.if.noAppTitle', locale) }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t('engine.studio.if.noAppHint', locale) })] }), onCreateApp && (_jsxs("button", { type: "button", onClick: onCreateApp, className: "inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " ", t('engine.studio.app.create', locale)] }))] })) : !current ? (_jsx("div", { className: "py-16 text-center text-sm text-muted-foreground", children: t('engine.studio.if.pickLeft', locale) })) : loading ? (_jsxs("div", { className: "flex items-center justify-center gap-2 py-16 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " ", t('engine.studio.loading', locale)] })) : current.type === 'object' ? (_jsx(SchemaRenderer, { schema: { type: 'object-view', objectName: current.name } })) : Preview ? (_jsx(Preview, { type: current.type, name: current.name, draft: draft, editing: true, selection: selection, onSelectionChange: setSelection, onPatch: onPatch, locale: locale })) : (_jsx("div", { className: "py-12 text-center text-xs text-muted-foreground", children: tFormat('engine.studio.if.readonlyPreview', locale, { type: current.type }) })) }), isEditable ? (_jsxs("p", { className: "mt-2 flex items-center gap-1 text-[11px] text-muted-foreground", children: [_jsx(MousePointer2, { className: "h-3 w-3" }), " ", t('engine.studio.if.editHint', locale)] })) : current?.type === 'object' ? (_jsxs("p", { className: "mt-2 flex items-center gap-1 text-[11px] text-muted-foreground", children: [_jsx(Database, { className: "h-3 w-3" }), " ", t('engine.studio.if.objectHintPre', locale), _jsx("span", { className: "font-medium", children: "Data" }), t('engine.studio.if.objectHintPost', locale)] })) : null] }), _jsxs("aside", { className: "w-72 shrink-0 overflow-auto border-l", children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-center gap-2 border-b bg-background/95 px-3 py-2 backdrop-blur", children: [_jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "text-[13px] font-medium", children: t('engine.studio.inspector.props', locale) }), selection && (_jsx("button", { type: "button", onClick: () => setSelection(null), className: "ml-auto rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground", "aria-label": t('engine.studio.deselect', locale), children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }), _jsx("div", { className: "p-3", children: editNav && navSel ? (_jsx(StudioNavItemInspector, { navId: navSel.id, appDraft: appDraft, objects: pkgObjects, onNavPatch: onNavPatch, onClear: () => setNavSel(null) })) : selection && Inspector && current ? (_jsx(Inspector, { type: current.type, name: current.name, draft: draft, selection: selection, onPatch: onPatch, onClearSelection: () => setSelection(null), onSelectionChange: setSelection, readOnly: false, locale: locale })) : (_jsxs("div", { className: "flex flex-col items-center gap-2 px-2 py-10 text-center text-xs text-muted-foreground", children: [_jsx(MousePointer2, { className: "h-5 w-5" }), t('engine.studio.inspector.emptyLine1', locale), _jsx("br", {}), t('engine.studio.inspector.emptyLine2', locale)] })) })] })] })] }));
|
|
290
623
|
}
|
|
291
624
|
/** Next unused `field_N` name for a freshly-added field. */
|
|
292
625
|
function nextFieldName(existing) {
|
|
@@ -336,11 +669,12 @@ function renderStudioGridList(props) {
|
|
|
336
669
|
addDeleteRecordsInline: true,
|
|
337
670
|
}, dataSource: ds, onEdit: onEdit, className: className, refreshKey: refreshKey }));
|
|
338
671
|
}
|
|
339
|
-
function DataPillar({ packageId }) {
|
|
672
|
+
function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
340
673
|
const client = useMetadataClient();
|
|
341
674
|
const adapter = useAdapter();
|
|
342
|
-
const locale =
|
|
675
|
+
const locale = useMetadataLocale();
|
|
343
676
|
const [objects, setObjects] = React.useState([]);
|
|
677
|
+
const [objectsLoaded, setObjectsLoaded] = React.useState(false);
|
|
344
678
|
const [current, setCurrent] = React.useState(null);
|
|
345
679
|
const [objDraft, setObjDraft] = React.useState({});
|
|
346
680
|
const [loading, setLoading] = React.useState(false);
|
|
@@ -351,22 +685,65 @@ function DataPillar({ packageId }) {
|
|
|
351
685
|
const [hasDraft, setHasDraft] = React.useState(false);
|
|
352
686
|
const [saving, setSaving] = React.useState(false);
|
|
353
687
|
const [gridVer, setGridVer] = React.useState(0);
|
|
688
|
+
// Records grid ⇄ Form ⇄ Validations ⇄ Settings — four views of the SAME
|
|
689
|
+
// object. Grid/Form are the runtime renderer (same-renderer principle);
|
|
690
|
+
// Validations edits `validations` rules; Settings edits object basics +
|
|
691
|
+
// the ADR-0085 semantic roles. All patch the one `objDraft`.
|
|
692
|
+
const [viewMode, setViewMode] = React.useState('grid');
|
|
693
|
+
// Within the Form view: 布局 (WYSIWYG drag/section designer) ⇄ 预览 (live form).
|
|
694
|
+
const [formMode, setFormMode] = React.useState('layout');
|
|
695
|
+
// Tracks which object's baseline is currently loaded — so we (re)load exactly
|
|
696
|
+
// once per selected object and never clobber an in-progress draft.
|
|
697
|
+
const loadedNameRef = React.useRef(null);
|
|
698
|
+
// Left-rail search + inline "new object" creator (design §4: rail = search + New).
|
|
699
|
+
const [query, setQuery] = React.useState('');
|
|
700
|
+
const [creating, setCreating] = React.useState(false);
|
|
701
|
+
const [newLabel, setNewLabel] = React.useState('');
|
|
702
|
+
const [newName, setNewName] = React.useState('');
|
|
703
|
+
const [nameTouched, setNameTouched] = React.useState(false);
|
|
704
|
+
const [createBusy, setCreateBusy] = React.useState(false);
|
|
705
|
+
// Whether the selected object exists beyond the draft (published/code baseline).
|
|
706
|
+
// A draft-only object has NO physical table yet (DDL lands at publish), so the
|
|
707
|
+
// Records grid must not fire data SQL against it.
|
|
708
|
+
const [hasBaseline, setHasBaseline] = React.useState(true);
|
|
354
709
|
React.useEffect(() => {
|
|
355
710
|
let cancelled = false;
|
|
356
711
|
(async () => {
|
|
357
712
|
try {
|
|
358
|
-
|
|
713
|
+
// Published objects + pending DRAFT objects, merged. `list()` only
|
|
714
|
+
// sees published/active metadata, so a freshly-created writable base
|
|
715
|
+
// whose objects are all drafts would render an empty (previously:
|
|
716
|
+
// forever-"loading") rail. Draft headers carry no label — show the
|
|
717
|
+
// machine name until the draft body loads on selection.
|
|
718
|
+
const [list, draftHeaders] = await Promise.all([
|
|
719
|
+
client.list('object', { packageId }),
|
|
720
|
+
client.listDrafts({ packageId, type: 'object' }).catch(() => []),
|
|
721
|
+
]);
|
|
359
722
|
if (cancelled)
|
|
360
723
|
return;
|
|
361
724
|
const items = (list || [])
|
|
362
725
|
.map((o) => ({ type: 'object', name: String(o.name ?? ''), label: String(o.label ?? o.name ?? '') }))
|
|
363
726
|
.filter((o) => o.name);
|
|
727
|
+
const known = new Set(items.map((o) => o.name));
|
|
728
|
+
for (const d of draftHeaders) {
|
|
729
|
+
if (d.name && !known.has(d.name)) {
|
|
730
|
+
items.push({ type: 'object', name: d.name, label: d.name });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
364
733
|
setObjects(items);
|
|
365
734
|
setCurrent((c) => c ?? items[0] ?? null);
|
|
735
|
+
// First-run: an empty writable package opens the creator right away —
|
|
736
|
+
// the first thing to do here is make an object, so put the inputs up.
|
|
737
|
+
if (items.length === 0)
|
|
738
|
+
setCreating(true);
|
|
366
739
|
}
|
|
367
740
|
catch (e) {
|
|
368
741
|
if (!cancelled)
|
|
369
|
-
setError(
|
|
742
|
+
setError(formatMetadataError(e));
|
|
743
|
+
}
|
|
744
|
+
finally {
|
|
745
|
+
if (!cancelled)
|
|
746
|
+
setObjectsLoaded(true);
|
|
370
747
|
}
|
|
371
748
|
})();
|
|
372
749
|
return () => {
|
|
@@ -376,6 +753,15 @@ function DataPillar({ packageId }) {
|
|
|
376
753
|
React.useEffect(() => {
|
|
377
754
|
if (!current)
|
|
378
755
|
return;
|
|
756
|
+
// Load once per selected object. Bail if this object's baseline is already
|
|
757
|
+
// loaded — a client-identity churn or a child remount must NOT re-fetch and
|
|
758
|
+
// clobber the in-progress form-layout draft the designer is editing.
|
|
759
|
+
// Keyed by object + publishNonce: a package publish (nonce++) re-reads the
|
|
760
|
+
// fresh published baseline; otherwise we never clobber an in-progress draft.
|
|
761
|
+
const loadKey = `${current.name}#${publishNonce}`;
|
|
762
|
+
if (loadedNameRef.current === loadKey)
|
|
763
|
+
return;
|
|
764
|
+
loadedNameRef.current = loadKey;
|
|
379
765
|
let cancelled = false;
|
|
380
766
|
setLoading(true);
|
|
381
767
|
setError(null);
|
|
@@ -394,10 +780,11 @@ function DataPillar({ packageId }) {
|
|
|
394
780
|
const draftBody = extractDraftBody(draftResp);
|
|
395
781
|
setObjDraft(draftBody ? { ...baseline, ...draftBody } : baseline);
|
|
396
782
|
setHasDraft(!!draftBody);
|
|
783
|
+
setHasBaseline(!!(lay.effective ?? lay.code));
|
|
397
784
|
}
|
|
398
785
|
catch (e) {
|
|
399
786
|
if (!cancelled)
|
|
400
|
-
setError(
|
|
787
|
+
setError(formatMetadataError(e));
|
|
401
788
|
}
|
|
402
789
|
finally {
|
|
403
790
|
if (!cancelled)
|
|
@@ -407,7 +794,7 @@ function DataPillar({ packageId }) {
|
|
|
407
794
|
return () => {
|
|
408
795
|
cancelled = true;
|
|
409
796
|
};
|
|
410
|
-
}, [client, current]);
|
|
797
|
+
}, [client, current, publishNonce]);
|
|
411
798
|
const fieldCount = React.useMemo(() => readFields(objDraft.fields).entries.length, [objDraft]);
|
|
412
799
|
const onPatch = React.useCallback((patch) => {
|
|
413
800
|
setObjDraft((d) => ({ ...d, ...patch }));
|
|
@@ -417,51 +804,73 @@ function DataPillar({ packageId }) {
|
|
|
417
804
|
const addField = React.useCallback(() => {
|
|
418
805
|
const view = readFields(objDraft.fields);
|
|
419
806
|
const name = nextFieldName(view.entries.map((e) => e.name));
|
|
420
|
-
view.entries.push(newField(name, 'text', '
|
|
807
|
+
view.entries.push(newField(name, 'text', t('engine.studio.data.newFieldLabel', locale)));
|
|
421
808
|
setObjDraft((d) => ({ ...d, fields: writeFields(view) }));
|
|
422
809
|
setDirty(true);
|
|
423
810
|
setFieldSel({ kind: 'field', id: name });
|
|
424
811
|
}, [objDraft]);
|
|
425
|
-
|
|
426
|
-
|
|
812
|
+
// "+ new object": create a fresh object as a DRAFT in this package (runtime
|
|
813
|
+
// create — same path the classic Studio editor uses), seeded with one text
|
|
814
|
+
// field so the form/grid isn't empty. It stays draft-only (no physical table)
|
|
815
|
+
// until the package publish, so we land on 表单·布局 — the metadata-level
|
|
816
|
+
// surface that never fires data SQL.
|
|
817
|
+
const doCreateObject = React.useCallback(async () => {
|
|
818
|
+
const label = newLabel.trim();
|
|
819
|
+
const name = toFieldName(newName.trim() || label);
|
|
820
|
+
if (!label || !name || name === 'field')
|
|
821
|
+
return; // CJK label → identifier must be typed
|
|
822
|
+
if (objects.some((o) => o.name === name)) {
|
|
823
|
+
setError(tFormat('engine.studio.data.idExists', locale, { name }));
|
|
427
824
|
return;
|
|
428
|
-
|
|
825
|
+
}
|
|
826
|
+
setCreateBusy(true);
|
|
429
827
|
setError(null);
|
|
430
828
|
try {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
829
|
+
const body = {
|
|
830
|
+
name,
|
|
831
|
+
label,
|
|
832
|
+
fields: { name: { type: 'text', label: t('engine.studio.data.nameFieldLabel', locale) } },
|
|
833
|
+
};
|
|
834
|
+
await client.save('object', name, body, { mode: 'draft', packageId });
|
|
835
|
+
const surface = { type: 'object', name, label };
|
|
836
|
+
setObjects((prev) => [...prev, surface]);
|
|
837
|
+
setCurrent(surface);
|
|
838
|
+
setViewMode('form');
|
|
839
|
+
setFormMode('layout');
|
|
840
|
+
setCreating(false);
|
|
841
|
+
setNewLabel('');
|
|
842
|
+
setNewName('');
|
|
843
|
+
setNameTouched(false);
|
|
844
|
+
onDraftSaved?.();
|
|
434
845
|
}
|
|
435
846
|
catch (e) {
|
|
436
|
-
setError(
|
|
847
|
+
setError(formatMetadataError(e));
|
|
437
848
|
}
|
|
438
849
|
finally {
|
|
439
|
-
|
|
850
|
+
setCreateBusy(false);
|
|
440
851
|
}
|
|
441
|
-
}, [client,
|
|
442
|
-
const
|
|
852
|
+
}, [newLabel, newName, objects, client, packageId, onDraftSaved]);
|
|
853
|
+
const doSave = React.useCallback(async () => {
|
|
443
854
|
if (!current)
|
|
444
855
|
return;
|
|
445
|
-
setSaving('
|
|
856
|
+
setSaving('draft');
|
|
446
857
|
setError(null);
|
|
447
858
|
try {
|
|
448
|
-
await client.
|
|
449
|
-
|
|
450
|
-
// so the remounted grid re-fetches the new/edited columns without a reload.
|
|
451
|
-
adapter?.clearCache?.();
|
|
452
|
-
setHasDraft(false);
|
|
859
|
+
await client.save('object', current.name, objDraft, { mode: 'draft', packageId });
|
|
860
|
+
setHasDraft(true);
|
|
453
861
|
setDirty(false);
|
|
454
|
-
|
|
862
|
+
onDraftSaved?.();
|
|
455
863
|
}
|
|
456
864
|
catch (e) {
|
|
457
|
-
setError(
|
|
865
|
+
setError(formatMetadataError(e));
|
|
458
866
|
}
|
|
459
867
|
finally {
|
|
460
868
|
setSaving(false);
|
|
461
869
|
}
|
|
462
|
-
}, [
|
|
870
|
+
}, [client, current, objDraft, onDraftSaved]);
|
|
463
871
|
// Drag-reorder columns → reorder the object's `fields` metadata (field display
|
|
464
|
-
// order follows metadata order),
|
|
872
|
+
// order follows metadata order), saved as a DRAFT. Published later via the
|
|
873
|
+
// package release — NOT auto-published per reorder as it used to be.
|
|
465
874
|
const doReorderFields = React.useCallback(async (orderedNames) => {
|
|
466
875
|
if (!current)
|
|
467
876
|
return;
|
|
@@ -476,49 +885,111 @@ function DataPillar({ packageId }) {
|
|
|
476
885
|
const entries = view.entries.map((e) => (visible.has(e.name) ? visibleInOrder[vi++] : e));
|
|
477
886
|
const body = { ...objDraft, fields: writeFields({ ...view, entries }) };
|
|
478
887
|
setObjDraft(body);
|
|
479
|
-
setSaving('
|
|
888
|
+
setSaving('draft');
|
|
480
889
|
setError(null);
|
|
481
890
|
try {
|
|
482
|
-
await client.save('object', current.name, body, { mode: 'draft' });
|
|
483
|
-
|
|
484
|
-
adapter?.clearCache?.();
|
|
485
|
-
setHasDraft(false);
|
|
891
|
+
await client.save('object', current.name, body, { mode: 'draft', packageId });
|
|
892
|
+
setHasDraft(true);
|
|
486
893
|
setDirty(false);
|
|
487
|
-
|
|
894
|
+
onDraftSaved?.();
|
|
895
|
+
setGridVer((v) => v + 1); // remount so the grid reflects the new (draft) order
|
|
488
896
|
}
|
|
489
897
|
catch (e) {
|
|
490
|
-
setError(
|
|
898
|
+
setError(formatMetadataError(e));
|
|
491
899
|
}
|
|
492
900
|
finally {
|
|
493
901
|
setSaving(false);
|
|
494
902
|
}
|
|
495
|
-
}, [client, current, objDraft,
|
|
903
|
+
}, [client, current, objDraft, onDraftSaved]);
|
|
496
904
|
const inspector = getMetadataInspector('object');
|
|
497
|
-
return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-1.5", children: [current ? (_jsxs("span", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [_jsx("span", { className: "text-[13px] font-medium text-foreground", children: current.label }), _jsxs("span", { className: "rounded bg-muted px-1.5 py-0.5", children: ["object \u00B7 ", current.name] }),
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
},
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
905
|
+
return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-1.5", children: [current ? (_jsxs("span", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [_jsx("span", { className: "text-[13px] font-medium text-foreground", children: current.label }), _jsxs("span", { className: "rounded bg-muted px-1.5 py-0.5", children: ["object \u00B7 ", current.name] }), _jsx("span", { children: tFormat('engine.studio.data.fieldCount', locale, { count: fieldCount }) })] })) : (_jsx("span", { className: "text-[11px] text-muted-foreground", children: t('engine.studio.data.pickObject', locale) })), hasDraft && (_jsx("span", { className: "rounded bg-amber-400/15 px-2 py-0.5 text-[11px] text-amber-600 dark:text-amber-300", children: t('engine.studio.unpublishedDraft', locale) })), _jsxs("button", { onClick: doSave, disabled: !current || !dirty || !!saving, className: "ml-auto inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs hover:bg-muted disabled:opacity-50", children: [saving === 'draft' ? _jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : _jsx(Save, { className: "h-3.5 w-3.5" }), t('engine.studio.saveDraft', locale)] })] }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsxs("nav", { className: "flex w-52 shrink-0 flex-col border-r", children: [_jsxs("div", { className: "shrink-0 p-2 pb-1", children: [_jsx("p", { className: "px-2 pb-1 pt-1 text-[11px] font-medium text-muted-foreground", children: t('engine.studio.data.objects', locale) }), _jsx("input", { value: query, onChange: (e) => setQuery(e.target.value), placeholder: t('engine.studio.data.searchObjects', locale), className: "h-7 w-full rounded-md border bg-background px-2 text-[11px] outline-none placeholder:text-muted-foreground/70 focus:ring-1 focus:ring-primary" })] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-auto p-2 pt-1", children: [objects.length === 0 && (_jsx("p", { className: "px-2 py-3 text-[11px] text-muted-foreground", children: error ? t('engine.studio.loadFailed', locale) : objectsLoaded ? t('engine.studio.data.noObjects', locale) : t('engine.studio.loading', locale) })), objects
|
|
906
|
+
.filter((o) => !query.trim() ||
|
|
907
|
+
o.label.toLowerCase().includes(query.trim().toLowerCase()) ||
|
|
908
|
+
o.name.toLowerCase().includes(query.trim().toLowerCase()))
|
|
909
|
+
.map((o) => (_jsxs("button", { onClick: () => setCurrent(o), className: 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs ' +
|
|
910
|
+
(current?.name === o.name ? 'bg-muted font-medium' : 'text-foreground/90 hover:bg-muted/60'), children: [_jsx(Database, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 truncate", children: o.label })] }, o.name)))] }), _jsx("div", { className: "shrink-0 border-t p-2", children: creating ? (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("input", { autoFocus: true, value: newLabel, onChange: (e) => {
|
|
911
|
+
setNewLabel(e.target.value);
|
|
912
|
+
if (!nameTouched)
|
|
913
|
+
setNewName(toFieldNameLoose(e.target.value));
|
|
914
|
+
}, onKeyDown: (e) => {
|
|
915
|
+
if (e.key === 'Enter')
|
|
916
|
+
void doCreateObject();
|
|
917
|
+
if (e.key === 'Escape')
|
|
918
|
+
setCreating(false);
|
|
919
|
+
}, placeholder: t('engine.studio.data.labelPlaceholder', locale), className: "h-7 w-full rounded-md border bg-background px-2 text-[11px] outline-none focus:ring-1 focus:ring-primary" }), _jsx("input", { value: newName, onChange: (e) => {
|
|
920
|
+
setNameTouched(true);
|
|
921
|
+
setNewName(toFieldNameLoose(e.target.value));
|
|
922
|
+
}, onKeyDown: (e) => {
|
|
923
|
+
if (e.key === 'Enter')
|
|
924
|
+
void doCreateObject();
|
|
925
|
+
if (e.key === 'Escape')
|
|
926
|
+
setCreating(false);
|
|
927
|
+
}, placeholder: t('engine.studio.data.idPlaceholder', locale), className: "h-7 w-full rounded-md border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-primary" }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("button", { type: "button", onClick: () => void doCreateObject(), disabled: createBusy || !newLabel.trim() || !toFieldName(newName.trim() || newLabel) || toFieldName(newName.trim() || newLabel) === 'field', className: "inline-flex flex-1 items-center justify-center gap-1 rounded-md bg-primary px-2 py-1 text-[11px] font-medium text-primary-foreground disabled:opacity-50", children: [createBusy ? _jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : _jsx(Plus, { className: "h-3 w-3" }), t('engine.studio.createDraft', locale)] }), _jsx("button", { type: "button", onClick: () => setCreating(false), className: "rounded-md border px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted", children: t('engine.studio.cancel', locale) })] })] })) : (_jsxs("button", { type: "button", onClick: () => setCreating(true), className: "inline-flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " ", t('engine.studio.data.newObject', locale)] })) })] }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col overflow-hidden p-4", children: !current ? (objectsLoaded && objects.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center gap-2 py-16 text-center", children: [_jsx("p", { className: "text-sm font-medium", children: t('engine.studio.data.firstObjectTitle', locale) }), _jsx("p", { className: "max-w-sm text-[11px] leading-5 text-muted-foreground", children: t('engine.studio.data.firstObjectHint', locale) })] })) : (_jsx("div", { className: "py-16 text-center text-sm text-muted-foreground", children: t('engine.studio.data.pickObject', locale) }))) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mb-3 flex shrink-0 items-center gap-2", children: [_jsxs("div", { className: "inline-flex rounded-md border p-0.5 text-[11px]", children: [_jsx("button", { type: "button", onClick: () => setViewMode('grid'), className: 'rounded px-2.5 py-0.5 ' +
|
|
928
|
+
(viewMode === 'grid' ? 'bg-muted font-medium' : 'text-muted-foreground hover:text-foreground'), children: t('engine.studio.data.tab.records', locale) }), _jsx("button", { type: "button", onClick: () => setViewMode('form'), className: 'rounded px-2.5 py-0.5 ' +
|
|
929
|
+
(viewMode === 'form' ? 'bg-muted font-medium' : 'text-muted-foreground hover:text-foreground'), children: t('engine.studio.data.tab.form', locale) }), _jsx("button", { type: "button", onClick: () => setViewMode('rules'), className: 'rounded px-2.5 py-0.5 ' +
|
|
930
|
+
(viewMode === 'rules' ? 'bg-muted font-medium' : 'text-muted-foreground hover:text-foreground'), children: t('engine.studio.data.tab.rules', locale) }), _jsx("button", { type: "button", onClick: () => setViewMode('settings'), className: 'rounded px-2.5 py-0.5 ' +
|
|
931
|
+
(viewMode === 'settings' ? 'bg-muted font-medium' : 'text-muted-foreground hover:text-foreground'), children: t('engine.studio.data.tab.settings', locale) })] }), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[11px] text-primary", children: [_jsx(Eye, { className: "h-3 w-3" }), ' ', viewMode === 'grid'
|
|
932
|
+
? t('engine.studio.data.badge.grid', locale)
|
|
933
|
+
: viewMode === 'rules'
|
|
934
|
+
? t('engine.studio.data.badge.rules', locale)
|
|
935
|
+
: viewMode === 'settings'
|
|
936
|
+
? t('engine.studio.data.badge.settings', locale)
|
|
937
|
+
: formMode === 'layout'
|
|
938
|
+
? t('engine.studio.data.badge.formLayout', locale)
|
|
939
|
+
: t('engine.studio.data.badge.formPreview', locale)] }), (viewMode === 'grid' || viewMode === 'form') && (_jsxs("button", { type: "button", onClick: addField, title: t('engine.studio.data.addFieldTitle', locale), className: "ml-auto inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " ", t('engine.studio.data.addField', locale)] }))] }), error && (_jsx("div", { className: "mb-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-1.5 text-[11px] text-destructive whitespace-pre-line", children: error })), viewMode === 'rules' ? (_jsx(ObjectValidationsPanel, { draft: objDraft, onPatch: onPatch })) : viewMode === 'settings' ? (_jsx(ObjectSettingsPanel, { name: current.name, draft: objDraft, onPatch: onPatch, locale: locale })) : viewMode === 'grid' && !hasBaseline ? (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-2 rounded-lg border border-dashed bg-muted/20 text-center", children: [_jsx("p", { className: "text-sm font-medium", children: t('engine.studio.data.draftObjectTitle', locale) }), _jsx("p", { className: "max-w-md text-[11px] leading-5 text-muted-foreground", children: t('engine.studio.data.draftObjectHint', locale) }), _jsx("button", { type: "button", onClick: () => {
|
|
940
|
+
setViewMode('form');
|
|
941
|
+
setFormMode('layout');
|
|
942
|
+
}, className: "mt-1 inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs hover:bg-muted", children: t('engine.studio.data.goDesignFields', locale) })] })) : viewMode === 'grid' ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "min-h-0 flex-1 overflow-auto rounded-lg border bg-background", children: _jsx(GridFieldAuthoringProvider, { value: {
|
|
943
|
+
onAddColumn: addField,
|
|
944
|
+
addColumnLabel: t('engine.studio.data.addField', locale),
|
|
945
|
+
onEditColumn: (fieldName) => {
|
|
946
|
+
// ignore non-field columns (e.g. the row-actions column)
|
|
947
|
+
if (readFields(objDraft.fields).entries.some((e) => e.name === fieldName)) {
|
|
948
|
+
setFieldSel({ kind: 'field', id: fieldName });
|
|
949
|
+
}
|
|
520
950
|
},
|
|
521
|
-
|
|
951
|
+
editColumnLabel: t('engine.studio.data.editFieldProps', locale),
|
|
952
|
+
onReorderFields: doReorderFields,
|
|
953
|
+
}, children: _jsx(SchemaRendererProvider, { dataSource: adapter, children: _jsx(PluginObjectView, { schema: {
|
|
954
|
+
type: 'object-view',
|
|
955
|
+
objectName: current.name,
|
|
956
|
+
// No saved view exists in design mode, so show the object's
|
|
957
|
+
// own fields as columns (in metadata order), dropping
|
|
958
|
+
// framework-managed/audit fields so the grid opens on the
|
|
959
|
+
// meaningful columns first — the way Airtable does.
|
|
960
|
+
table: {
|
|
961
|
+
fields: readFields(objDraft.fields)
|
|
962
|
+
.entries.map((e) => e.name)
|
|
963
|
+
.filter((n) => !STUDIO_SYSTEM_FIELD_NAMES.has(n)),
|
|
964
|
+
},
|
|
965
|
+
}, dataSource: adapter, renderListView: renderStudioGridList }, `${current.name}:${gridVer}`) }) }) }), _jsxs("p", { className: "mt-2 flex items-center gap-1 text-[11px] text-muted-foreground", children: [_jsx(MousePointer2, { className: "h-3 w-3" }), " ", t('engine.studio.data.gridHint', locale)] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mb-3 flex items-center gap-2", children: [_jsxs("div", { className: "inline-flex rounded-md border p-0.5 text-[11px]", children: [_jsx("button", { type: "button", onClick: () => setFormMode('layout'), className: 'rounded px-2.5 py-0.5 ' +
|
|
966
|
+
(formMode === 'layout' ? 'bg-muted font-medium' : 'text-muted-foreground hover:text-foreground'), children: t('engine.studio.data.form.layout', locale) }), _jsx("button", { type: "button", onClick: () => setFormMode('preview'), className: 'rounded px-2.5 py-0.5 ' +
|
|
967
|
+
(formMode === 'preview' ? 'bg-muted font-medium' : 'text-muted-foreground hover:text-foreground'), children: t('engine.studio.data.form.preview', locale) })] }), _jsx("span", { className: "text-[11px] text-muted-foreground", children: formMode === 'layout' ? t('engine.studio.data.form.layoutBadge', locale) : t('engine.studio.data.form.previewBadge', locale) }), formMode === 'preview' && (dirty || hasDraft) && (_jsx("span", { className: "inline-flex items-center gap-1 rounded bg-amber-400/15 px-2 py-0.5 text-[11px] text-amber-600 dark:text-amber-300", children: t('engine.studio.data.form.previewWarn', locale) }))] }), formMode === 'layout' ? (_jsx(ObjectFormDesigner, { draft: objDraft, systemFieldNames: STUDIO_SYSTEM_FIELD_NAMES, onChange: onPatch, selectedField: fieldSel?.kind === 'field' ? fieldSel.id : null, onSelectField: (name) => setFieldSel({ kind: 'field', id: name }), onAddField: addField })) : !hasBaseline ? (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-2 rounded-lg border border-dashed bg-muted/20 text-center", children: [_jsx("p", { className: "text-sm font-medium", children: t('engine.studio.data.form.noPublishedTitle', locale) }), _jsx("p", { className: "max-w-md text-[11px] leading-5 text-muted-foreground", children: t('engine.studio.data.form.noPublishedHint', locale) })] })) : (_jsxs(_Fragment, { children: [_jsx("style", { children: `
|
|
968
|
+
/* Design preview: the form is a click-to-select canvas, not a data-entry
|
|
969
|
+
* form. Disable interaction on field contents so a click anywhere on a
|
|
970
|
+
* field routes to its [data-field] wrapper (→ select), and neutralize the
|
|
971
|
+
* create/cancel footer so nothing is submittable here. */
|
|
972
|
+
.os-form-authoring [data-field]{border-radius:8px;cursor:pointer;transition:box-shadow .12s;padding:8px;margin:-8px 0;}
|
|
973
|
+
.os-form-authoring [data-field] *{pointer-events:none;}
|
|
974
|
+
.os-form-authoring [data-field]:hover{box-shadow:0 0 0 1px hsl(var(--border));}
|
|
975
|
+
.os-form-authoring form > div:last-child:has(button){display:none;}
|
|
976
|
+
${fieldSel?.kind === 'field'
|
|
977
|
+
? `.os-form-authoring [data-field="${String(fieldSel.id).replace(/[^\w-]/g, '')}"]{box-shadow:0 0 0 2px hsl(var(--primary));}`
|
|
978
|
+
: ''}
|
|
979
|
+
` }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto rounded-lg border bg-background p-6", children: _jsx("div", { className: "os-form-authoring mx-auto max-w-2xl", onClick: (e) => {
|
|
980
|
+
const el = e.target.closest('[data-field]');
|
|
981
|
+
const name = el?.getAttribute('data-field');
|
|
982
|
+
if (name && readFields(objDraft.fields).entries.some((f) => f.name === name)) {
|
|
983
|
+
setFieldSel({ kind: 'field', id: name });
|
|
984
|
+
}
|
|
985
|
+
}, children: _jsx(SchemaRendererProvider, { dataSource: adapter, children: _jsx(ObjectForm, { schema: {
|
|
986
|
+
type: 'object-form',
|
|
987
|
+
objectName: current.name,
|
|
988
|
+
mode: 'create',
|
|
989
|
+
fields: readFields(objDraft.fields)
|
|
990
|
+
.entries.map((e) => e.name)
|
|
991
|
+
.filter((n) => !STUDIO_SYSTEM_FIELD_NAMES.has(n)),
|
|
992
|
+
}, dataSource: adapter }, `${current.name}:${gridVer}:form`) }) }) }), _jsxs("p", { className: "mt-2 flex items-center gap-1 text-[11px] text-muted-foreground", children: [_jsx(MousePointer2, { className: "h-3 w-3" }), " ", t('engine.studio.data.formHint', locale)] })] }))] }))] })) }), current && fieldSel && inspector && (_jsxs("aside", { className: "flex w-80 shrink-0 flex-col border-l", children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-center gap-2 border-b bg-background/95 px-3 py-2 backdrop-blur", children: [_jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "text-[13px] font-medium", children: t('engine.studio.data.fieldProps', locale) }), _jsx("button", { type: "button", onClick: () => setFieldSel(null), className: "ml-auto rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground", "aria-label": t('engine.studio.close', locale), children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-3", children: React.createElement(inspector, {
|
|
522
993
|
type: 'object',
|
|
523
994
|
name: current.name,
|
|
524
995
|
draft: objDraft,
|
|
@@ -531,9 +1002,9 @@ function DataPillar({ packageId }) {
|
|
|
531
1002
|
}) })] }))] })] }));
|
|
532
1003
|
}
|
|
533
1004
|
/** Automations pillar — flows: list → FlowPreview (default OFF / review-then-enable). */
|
|
534
|
-
function AutomationsPillar({ packageId }) {
|
|
1005
|
+
function AutomationsPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
535
1006
|
const client = useMetadataClient();
|
|
536
|
-
const locale =
|
|
1007
|
+
const locale = useMetadataLocale();
|
|
537
1008
|
const [flows, setFlows] = React.useState([]);
|
|
538
1009
|
const [current, setCurrent] = React.useState(null);
|
|
539
1010
|
const [draft, setDraft] = React.useState({});
|
|
@@ -542,6 +1013,15 @@ function AutomationsPillar({ packageId }) {
|
|
|
542
1013
|
const [saving, setSaving] = React.useState(false);
|
|
543
1014
|
const [hasDraft, setHasDraft] = React.useState(false);
|
|
544
1015
|
const [error, setError] = React.useState(null);
|
|
1016
|
+
// Tells "still fetching the list" apart from "fetched, package has no flows"
|
|
1017
|
+
// — without it the empty rail showed an endless "加载中…" for a fresh package.
|
|
1018
|
+
const [listed, setListed] = React.useState(false);
|
|
1019
|
+
// Inline create — a fresh package starts with zero flows, so the pillar must
|
|
1020
|
+
// offer a way to author the first one (mirrors the object/app creators).
|
|
1021
|
+
const [creating, setCreating] = React.useState(false);
|
|
1022
|
+
const [newLabel, setNewLabel] = React.useState('');
|
|
1023
|
+
const [newName, setNewName] = React.useState('');
|
|
1024
|
+
const [createBusy, setCreateBusy] = React.useState(false);
|
|
545
1025
|
const Preview = getMetadataPreview(current?.type ?? '');
|
|
546
1026
|
const inspector = getMetadataInspector('flow');
|
|
547
1027
|
const isEditable = !!Preview;
|
|
@@ -560,13 +1040,55 @@ function AutomationsPillar({ packageId }) {
|
|
|
560
1040
|
}
|
|
561
1041
|
catch (e) {
|
|
562
1042
|
if (!cancelled)
|
|
563
|
-
setError(
|
|
1043
|
+
setError(formatMetadataError(e));
|
|
1044
|
+
}
|
|
1045
|
+
finally {
|
|
1046
|
+
if (!cancelled)
|
|
1047
|
+
setListed(true);
|
|
564
1048
|
}
|
|
565
1049
|
})();
|
|
566
1050
|
return () => {
|
|
567
1051
|
cancelled = true;
|
|
568
1052
|
};
|
|
569
1053
|
}, [client, packageId]);
|
|
1054
|
+
const doCreateFlow = React.useCallback(async () => {
|
|
1055
|
+
const label = newLabel.trim();
|
|
1056
|
+
const name = toFieldName(newName.trim() || label);
|
|
1057
|
+
if (!label || !name || name === 'field')
|
|
1058
|
+
return;
|
|
1059
|
+
setCreateBusy(true);
|
|
1060
|
+
setError(null);
|
|
1061
|
+
try {
|
|
1062
|
+
// Minimal valid, autolaunched skeleton: start → end. The designer fills in
|
|
1063
|
+
// the trigger + nodes; publishing it is a separate, user-initiated step.
|
|
1064
|
+
const skeleton = {
|
|
1065
|
+
name,
|
|
1066
|
+
label,
|
|
1067
|
+
type: 'autolaunched',
|
|
1068
|
+
nodes: [
|
|
1069
|
+
{ id: 'start', type: 'start', label: t('engine.studio.auto.nodeStart', locale) },
|
|
1070
|
+
{ id: 'end', type: 'end', label: t('engine.studio.auto.nodeEnd', locale) },
|
|
1071
|
+
],
|
|
1072
|
+
edges: [{ id: 'e1', source: 'start', target: 'end' }],
|
|
1073
|
+
};
|
|
1074
|
+
await client.save('flow', name, skeleton, { mode: 'draft', packageId });
|
|
1075
|
+
const item = { type: 'flow', name, label };
|
|
1076
|
+
setFlows((fs) => [...fs.filter((f) => f.name !== name), item]);
|
|
1077
|
+
setCurrent(item);
|
|
1078
|
+
setHasDraft(true);
|
|
1079
|
+
setCreating(false);
|
|
1080
|
+
setNewLabel('');
|
|
1081
|
+
setNewName('');
|
|
1082
|
+
onDraftSaved?.();
|
|
1083
|
+
toast.success(tFormat('engine.studio.auto.savedDraft', locale, { label }));
|
|
1084
|
+
}
|
|
1085
|
+
catch (e) {
|
|
1086
|
+
setError(formatMetadataError(e));
|
|
1087
|
+
}
|
|
1088
|
+
finally {
|
|
1089
|
+
setCreateBusy(false);
|
|
1090
|
+
}
|
|
1091
|
+
}, [newLabel, newName, client, packageId, onDraftSaved]);
|
|
570
1092
|
React.useEffect(() => {
|
|
571
1093
|
if (!current)
|
|
572
1094
|
return;
|
|
@@ -590,7 +1112,7 @@ function AutomationsPillar({ packageId }) {
|
|
|
590
1112
|
}
|
|
591
1113
|
catch (e) {
|
|
592
1114
|
if (!cancelled)
|
|
593
|
-
setError(
|
|
1115
|
+
setError(formatMetadataError(e));
|
|
594
1116
|
}
|
|
595
1117
|
finally {
|
|
596
1118
|
if (!cancelled)
|
|
@@ -600,7 +1122,7 @@ function AutomationsPillar({ packageId }) {
|
|
|
600
1122
|
return () => {
|
|
601
1123
|
cancelled = true;
|
|
602
1124
|
};
|
|
603
|
-
}, [client, current]);
|
|
1125
|
+
}, [client, current, publishNonce]);
|
|
604
1126
|
const onPatch = React.useCallback((patch) => setDraft((d) => ({ ...d, ...patch })), []);
|
|
605
1127
|
const doSave = React.useCallback(async () => {
|
|
606
1128
|
if (!current)
|
|
@@ -608,34 +1130,30 @@ function AutomationsPillar({ packageId }) {
|
|
|
608
1130
|
setSaving('draft');
|
|
609
1131
|
setError(null);
|
|
610
1132
|
try {
|
|
611
|
-
await client.save('flow', current.name, draft, { mode: 'draft' });
|
|
1133
|
+
await client.save('flow', current.name, draft, { mode: 'draft', packageId });
|
|
612
1134
|
setHasDraft(true);
|
|
1135
|
+
onDraftSaved?.();
|
|
613
1136
|
}
|
|
614
1137
|
catch (e) {
|
|
615
|
-
setError(
|
|
616
|
-
}
|
|
617
|
-
finally {
|
|
618
|
-
setSaving(false);
|
|
619
|
-
}
|
|
620
|
-
}, [client, current, draft]);
|
|
621
|
-
const doPublish = React.useCallback(async () => {
|
|
622
|
-
if (!current)
|
|
623
|
-
return;
|
|
624
|
-
setSaving('publish');
|
|
625
|
-
setError(null);
|
|
626
|
-
try {
|
|
627
|
-
await client.publish('flow', current.name);
|
|
628
|
-
setHasDraft(false);
|
|
629
|
-
}
|
|
630
|
-
catch (e) {
|
|
631
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
1138
|
+
setError(formatMetadataError(e));
|
|
632
1139
|
}
|
|
633
1140
|
finally {
|
|
634
1141
|
setSaving(false);
|
|
635
1142
|
}
|
|
636
|
-
}, [client, current]);
|
|
637
|
-
return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-1.5", children: [_jsx("span", { className: "text-[11px] text-muted-foreground", children:
|
|
638
|
-
|
|
1143
|
+
}, [client, current, draft, onDraftSaved]);
|
|
1144
|
+
return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-1.5", children: [_jsx("span", { className: "text-[11px] text-muted-foreground", children: t('engine.studio.auto.defaultOff', locale) }), hasDraft && (_jsx("span", { className: "rounded bg-amber-400/15 px-2 py-0.5 text-[11px] text-amber-600 dark:text-amber-300", children: t('engine.studio.unpublishedDraft', locale) })), _jsxs("button", { onClick: doSave, disabled: !current || !isEditable || !!saving, className: "ml-auto inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs hover:bg-muted disabled:opacity-50", children: [saving === 'draft' ? _jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : _jsx(Save, { className: "h-3.5 w-3.5" }), t('engine.studio.saveDraft', locale)] })] }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsxs("nav", { className: "flex w-52 shrink-0 flex-col overflow-auto border-r p-2", children: [_jsxs("div", { className: "flex items-center gap-1 px-2 pb-1 pt-1", children: [_jsx("p", { className: "flex-1 text-[11px] font-medium text-muted-foreground", children: t('engine.studio.auto.heading', locale) }), _jsxs("button", { type: "button", onClick: () => setCreating((v) => !v), title: t('engine.studio.auto.newTitle', locale), className: "inline-flex items-center gap-0.5 rounded border px-1.5 py-0.5 text-[11px] hover:bg-muted", children: [_jsx(Plus, { className: "h-3 w-3" }), " ", t('engine.studio.new', locale)] })] }), flows.length > 0 &&
|
|
1145
|
+
flows.map((f) => (_jsxs("button", { onClick: () => setCurrent(f), className: 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs ' +
|
|
1146
|
+
(current?.name === f.name ? 'bg-muted font-medium' : 'text-foreground/90 hover:bg-muted/60'), children: [_jsx(Workflow, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 truncate", children: f.label })] }, f.name))), flows.length === 0 && !creating && (_jsx("p", { className: "px-2 py-3 text-[11px] text-muted-foreground", children: error ? t('engine.studio.loadFailed', locale) : !listed ? t('engine.studio.loading', locale) : t('engine.studio.auto.none', locale) })), creating && (_jsxs("div", { className: "mt-1 space-y-1.5 rounded-md border bg-muted/30 p-2", children: [_jsx("input", { autoFocus: true, value: newLabel, onChange: (e) => setNewLabel(e.target.value), onKeyDown: (e) => {
|
|
1147
|
+
if (e.key === 'Enter')
|
|
1148
|
+
void doCreateFlow();
|
|
1149
|
+
if (e.key === 'Escape')
|
|
1150
|
+
setCreating(false);
|
|
1151
|
+
}, placeholder: t('engine.studio.auto.namePlaceholder', locale), className: "w-full rounded border bg-background px-2 py-1 text-xs" }), _jsx("input", { value: newName, onChange: (e) => setNewName(e.target.value), onKeyDown: (e) => {
|
|
1152
|
+
if (e.key === 'Enter')
|
|
1153
|
+
void doCreateFlow();
|
|
1154
|
+
if (e.key === 'Escape')
|
|
1155
|
+
setCreating(false);
|
|
1156
|
+
}, placeholder: t('engine.studio.auto.idPlaceholder', locale), className: "w-full rounded border bg-background px-2 py-1 font-mono text-[11px]" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("button", { type: "button", onClick: () => void doCreateFlow(), disabled: !newLabel.trim() || createBusy, className: "inline-flex flex-1 items-center justify-center gap-1 rounded bg-primary px-2 py-1 text-[11px] font-medium text-primary-foreground disabled:opacity-50", children: [createBusy ? _jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : _jsx(Plus, { className: "h-3 w-3" }), t('engine.studio.createDraft', locale)] }), _jsx("button", { type: "button", onClick: () => setCreating(false), className: "rounded border px-2 py-1 text-[11px] hover:bg-muted", children: t('engine.studio.cancel', locale) })] })] }))] }), _jsxs("main", { className: "min-w-0 flex-1 overflow-auto bg-muted/30 p-4", children: [_jsxs("div", { className: "mb-3 flex items-center gap-2", children: [_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[11px] text-primary", children: [_jsx(Workflow, { className: "h-3 w-3" }), " ", t('engine.studio.auto.canvasHint', locale)] }), current && _jsxs("span", { className: "text-[11px] text-muted-foreground", children: ["flow \u00B7 ", current.name] })] }), error && (_jsx("div", { className: "mb-3 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive whitespace-pre-line", children: error })), _jsx("div", { className: "rounded-lg border bg-background p-4", children: !current ? (_jsx("div", { className: "py-16 text-center text-sm text-muted-foreground", children: t('engine.studio.auto.pick', locale) })) : loading ? (_jsxs("div", { className: "flex items-center justify-center gap-2 py-16 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " ", t('engine.studio.loading', locale)] })) : Preview ? (React.createElement(Preview, {
|
|
639
1157
|
type: current.type,
|
|
640
1158
|
name: current.name,
|
|
641
1159
|
draft,
|
|
@@ -644,7 +1162,7 @@ function AutomationsPillar({ packageId }) {
|
|
|
644
1162
|
onSelectionChange: setSelection,
|
|
645
1163
|
onPatch,
|
|
646
1164
|
locale,
|
|
647
|
-
})) : (_jsx("pre", { className: "overflow-auto text-[11px] text-muted-foreground", children: JSON.stringify(draft, null, 2) })) }), isEditable && (_jsxs("p", { className: "mt-2 flex items-center gap-1 text-[11px] text-muted-foreground", children: [_jsx(MousePointer2, { className: "h-3 w-3" }), "
|
|
1165
|
+
})) : (_jsx("pre", { className: "overflow-auto text-[11px] text-muted-foreground", children: JSON.stringify(draft, null, 2) })) }), isEditable && (_jsxs("p", { className: "mt-2 flex items-center gap-1 text-[11px] text-muted-foreground", children: [_jsx(MousePointer2, { className: "h-3 w-3" }), " ", t('engine.studio.auto.editHint', locale)] }))] }), _jsxs("aside", { className: "w-72 shrink-0 overflow-auto border-l", children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-center gap-2 border-b bg-background/95 px-3 py-2 backdrop-blur", children: [_jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "text-[13px] font-medium", children: t('engine.studio.auto.config', locale) }), selection && (_jsx("button", { type: "button", onClick: () => setSelection(null), className: "ml-auto rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground", "aria-label": t('engine.studio.deselect', locale), children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }), _jsx("div", { className: "p-3", children: selection && inspector && current ? (React.createElement(inspector, {
|
|
648
1166
|
type: 'flow',
|
|
649
1167
|
name: current.name,
|
|
650
1168
|
draft,
|
|
@@ -654,6 +1172,135 @@ function AutomationsPillar({ packageId }) {
|
|
|
654
1172
|
onSelectionChange: setSelection,
|
|
655
1173
|
readOnly: false,
|
|
656
1174
|
locale,
|
|
657
|
-
})) : (_jsxs("div", { className: "flex flex-col items-center gap-2 px-2 py-10 text-center text-xs text-muted-foreground", children: [_jsx(MousePointer2, { className: "h-5 w-5" }),
|
|
1175
|
+
})) : (_jsxs("div", { className: "flex flex-col items-center gap-2 px-2 py-10 text-center text-xs text-muted-foreground", children: [_jsx(MousePointer2, { className: "h-5 w-5" }), t('engine.studio.auto.emptyLine1', locale), _jsx("br", {}), t('engine.studio.auto.emptyLine2', locale)] })) })] })] })] }));
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Access pillar — the permission workbench (builder-ui §7, ADR-0084's fourth
|
|
1179
|
+
* content pillar). Left rail: the environment's permission sets / profiles;
|
|
1180
|
+
* main: the Salesforce-style PermissionMatrixEditPage (objects × CRUD/VAMA +
|
|
1181
|
+
* field-level R/W).
|
|
1182
|
+
*
|
|
1183
|
+
* Scope note (ADR-0086 P0/P1/P2): the pillar is scoped to the current package.
|
|
1184
|
+
* The left rail lists only permission sets this package owns — the metadata API
|
|
1185
|
+
* filters `permission` by the record-level `package_id` provenance server-side
|
|
1186
|
+
* (P1), so environment-owned platform defaults (`admin_full_access`,
|
|
1187
|
+
* `member_default`, …) are excluded by the backend. The object MATRIX lists only
|
|
1188
|
+
* the objects this package declares, and Save merges just that slice back,
|
|
1189
|
+
* leaving other packages' contributed rows untouched (P0). Save writes a package
|
|
1190
|
+
* DRAFT and publishes with the whole package via the top-bar Publish (P2, D6).
|
|
1191
|
+
*/
|
|
1192
|
+
function AccessPillar({ packageId, publishNonce, onDraftSaved, }) {
|
|
1193
|
+
const client = useMetadataClient();
|
|
1194
|
+
const locale = useMetadataLocale();
|
|
1195
|
+
const [perms, setPerms] = React.useState([]);
|
|
1196
|
+
const [loaded, setLoaded] = React.useState(false);
|
|
1197
|
+
const [error, setError] = React.useState(null);
|
|
1198
|
+
const [current, setCurrent] = React.useState(null);
|
|
1199
|
+
const [query, setQuery] = React.useState('');
|
|
1200
|
+
// inline creator (same rail pattern as the Data pillar's object creator)
|
|
1201
|
+
const [creating, setCreating] = React.useState(false);
|
|
1202
|
+
const [newLabel, setNewLabel] = React.useState('');
|
|
1203
|
+
const [newName, setNewName] = React.useState('');
|
|
1204
|
+
const [nameTouched, setNameTouched] = React.useState(false);
|
|
1205
|
+
const [busy, setBusy] = React.useState(false);
|
|
1206
|
+
const load = React.useCallback(async () => {
|
|
1207
|
+
try {
|
|
1208
|
+
// Scope the rail to this package server-side (ADR-0086 P1): the metadata
|
|
1209
|
+
// API filters `permission` by the record-level `package_id` provenance, so
|
|
1210
|
+
// it returns only the sets this package owns — environment-owned platform
|
|
1211
|
+
// defaults (`admin_full_access`, `member_default`, …) are excluded by the
|
|
1212
|
+
// backend, not the client. (The `?package=` list rows don't echo the
|
|
1213
|
+
// provenance columns, so a client-side filter can't do this.)
|
|
1214
|
+
//
|
|
1215
|
+
// ADR-0086 P2 (D6): a package permission set is draft/published metadata,
|
|
1216
|
+
// so the rail shows published ∪ pending-draft sets — a set created (or
|
|
1217
|
+
// renamed) as a draft but not yet published must still appear, just like
|
|
1218
|
+
// the Data/Interfaces pillars merge their drafts. Draft headers are
|
|
1219
|
+
// already package-scoped by `listDrafts({ packageId })`.
|
|
1220
|
+
const [list, drafts] = await Promise.all([
|
|
1221
|
+
client.list('permission', { packageId }),
|
|
1222
|
+
client.listDrafts({ packageId, type: 'permission' }).catch(() => []),
|
|
1223
|
+
]);
|
|
1224
|
+
const byName = new Map();
|
|
1225
|
+
for (const p of list || []) {
|
|
1226
|
+
const name = String(p.name ?? p.id ?? '');
|
|
1227
|
+
if (!name)
|
|
1228
|
+
continue;
|
|
1229
|
+
byName.set(name, {
|
|
1230
|
+
name,
|
|
1231
|
+
label: String(p.label ?? p.name ?? ''),
|
|
1232
|
+
isProfile: !!p.isProfile,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
for (const d of drafts || []) {
|
|
1236
|
+
const name = String(d?.name ?? '');
|
|
1237
|
+
if (!name || byName.has(name))
|
|
1238
|
+
continue;
|
|
1239
|
+
byName.set(name, { name, label: name });
|
|
1240
|
+
}
|
|
1241
|
+
const scoped = [...byName.values()];
|
|
1242
|
+
setPerms(scoped);
|
|
1243
|
+
setCurrent((c) => c ?? scoped[0]?.name ?? null);
|
|
1244
|
+
}
|
|
1245
|
+
catch (e) {
|
|
1246
|
+
setError(formatMetadataError(e));
|
|
1247
|
+
}
|
|
1248
|
+
finally {
|
|
1249
|
+
setLoaded(true);
|
|
1250
|
+
}
|
|
1251
|
+
}, [client, packageId]);
|
|
1252
|
+
React.useEffect(() => {
|
|
1253
|
+
void load();
|
|
1254
|
+
// Re-read after a package publish so drafts that went live collapse into
|
|
1255
|
+
// the published rail (ADR-0086 P2).
|
|
1256
|
+
}, [load, publishNonce]);
|
|
1257
|
+
const doCreate = React.useCallback(async () => {
|
|
1258
|
+
const label = newLabel.trim();
|
|
1259
|
+
const name = toFieldName(newName.trim() || label);
|
|
1260
|
+
if (!label || !name || name === 'field')
|
|
1261
|
+
return;
|
|
1262
|
+
setBusy(true);
|
|
1263
|
+
try {
|
|
1264
|
+
// Package door → create as a DRAFT stamped with this package (D6/D7),
|
|
1265
|
+
// published atomically with the rest of the package.
|
|
1266
|
+
await client.save('permission', name, { name, label, objects: {}, fields: {} }, { mode: 'draft', packageId });
|
|
1267
|
+
toast.success(tFormat('engine.studio.access.created', locale, { label }));
|
|
1268
|
+
setCreating(false);
|
|
1269
|
+
setNewLabel('');
|
|
1270
|
+
setNewName('');
|
|
1271
|
+
setNameTouched(false);
|
|
1272
|
+
onDraftSaved?.();
|
|
1273
|
+
await load();
|
|
1274
|
+
setCurrent(name);
|
|
1275
|
+
}
|
|
1276
|
+
catch (e) {
|
|
1277
|
+
toast.error(formatMetadataError(e));
|
|
1278
|
+
}
|
|
1279
|
+
finally {
|
|
1280
|
+
setBusy(false);
|
|
1281
|
+
}
|
|
1282
|
+
}, [client, newLabel, newName, load, packageId, onDraftSaved, locale]);
|
|
1283
|
+
const filtered = perms.filter((p) => !query.trim() ||
|
|
1284
|
+
p.label.toLowerCase().includes(query.trim().toLowerCase()) ||
|
|
1285
|
+
p.name.toLowerCase().includes(query.trim().toLowerCase()));
|
|
1286
|
+
return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-1.5", children: [_jsxs("span", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [_jsx(Shield, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "text-[13px] font-medium text-foreground", children: t('engine.studio.access.title', locale) }), _jsx("span", { className: "rounded bg-muted px-1.5 py-0.5", children: t('engine.studio.access.subtitle', locale) })] }), _jsx("span", { title: t('engine.studio.access.bannerTitle', locale), className: "ml-auto rounded bg-amber-400/15 px-2 py-0.5 text-[11px] text-amber-600 dark:text-amber-300", children: t('engine.studio.access.banner', locale) })] }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsxs("nav", { className: "flex w-52 shrink-0 flex-col border-r", children: [_jsxs("div", { className: "p-2 pb-0", children: [_jsx("p", { className: "px-2 pb-1 pt-1 text-[11px] font-medium text-muted-foreground", children: t('engine.studio.access.heading', locale) }), _jsx("input", { value: query, onChange: (e) => setQuery(e.target.value), placeholder: t('engine.studio.access.search', locale), className: "mb-1 h-7 w-full rounded-md border bg-background px-2 text-[11px] outline-none focus:ring-1 focus:ring-primary" })] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-auto p-2 pt-1", children: [perms.length === 0 && (_jsx("p", { className: "px-2 py-3 text-[11px] text-muted-foreground", children: error ? t('engine.studio.loadFailed', locale) : loaded ? t('engine.studio.access.none', locale) : t('engine.studio.loading', locale) })), filtered.map((p) => (_jsxs("button", { onClick: () => setCurrent(p.name), className: 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs ' +
|
|
1287
|
+
(current === p.name ? 'bg-muted font-medium' : 'text-foreground/90 hover:bg-muted/60'), children: [_jsx(Shield, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }), _jsx("span", { className: "flex-1 truncate", children: p.label }), p.isProfile && (_jsx("span", { className: "rounded bg-muted px-1 py-px text-[9px] uppercase text-muted-foreground", children: "profile" }))] }, p.name)))] }), _jsx("div", { className: "shrink-0 border-t p-2", children: creating ? (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("input", { autoFocus: true, value: newLabel, onChange: (e) => {
|
|
1288
|
+
setNewLabel(e.target.value);
|
|
1289
|
+
if (!nameTouched)
|
|
1290
|
+
setNewName(toFieldNameLoose(e.target.value));
|
|
1291
|
+
}, onKeyDown: (e) => {
|
|
1292
|
+
if (e.key === 'Enter')
|
|
1293
|
+
void doCreate();
|
|
1294
|
+
if (e.key === 'Escape')
|
|
1295
|
+
setCreating(false);
|
|
1296
|
+
}, placeholder: t('engine.studio.access.labelPlaceholder', locale), className: "h-7 w-full rounded-md border bg-background px-2 text-[11px] outline-none focus:ring-1 focus:ring-primary" }), _jsx("input", { value: newName, onChange: (e) => {
|
|
1297
|
+
setNameTouched(true);
|
|
1298
|
+
setNewName(toFieldNameLoose(e.target.value));
|
|
1299
|
+
}, onKeyDown: (e) => {
|
|
1300
|
+
if (e.key === 'Enter')
|
|
1301
|
+
void doCreate();
|
|
1302
|
+
if (e.key === 'Escape')
|
|
1303
|
+
setCreating(false);
|
|
1304
|
+
}, placeholder: t('engine.studio.access.idPlaceholder', locale), className: "h-7 w-full rounded-md border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-primary" }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("button", { type: "button", onClick: () => void doCreate(), disabled: busy || !newLabel.trim() || !toFieldName(newName.trim() || newLabel) || toFieldName(newName.trim() || newLabel) === 'field', className: "inline-flex flex-1 items-center justify-center gap-1 rounded-md bg-primary px-2 py-1 text-[11px] font-medium text-primary-foreground disabled:opacity-50", children: [busy ? _jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : _jsx(Plus, { className: "h-3 w-3" }), t('engine.studio.create', locale)] }), _jsx("button", { type: "button", onClick: () => setCreating(false), className: "rounded-md border px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted", children: t('engine.studio.cancel', locale) })] })] })) : (_jsxs("button", { type: "button", onClick: () => setCreating(true), className: "flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), " ", t('engine.studio.access.new', locale)] })) })] }), _jsx("main", { className: "min-w-0 flex-1 overflow-auto", children: current ? (_jsx(PermissionMatrixEditPage, { type: "permission", name: current, packageId: packageId, publishNonce: publishNonce, onDraftSaved: onDraftSaved }, current)) : (_jsx("div", { className: "py-16 text-center text-sm text-muted-foreground", children: loaded && perms.length === 0 ? t('engine.studio.access.emptyMain', locale) : t('engine.studio.access.pick', locale) })) })] })] }));
|
|
658
1305
|
}
|
|
659
1306
|
export default StudioDesignSurface;
|