@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +522 -0
  2. package/README.md +23 -0
  3. package/dist/console/ConsoleShell.js +17 -2
  4. package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
  5. package/dist/console/home/CloudOnboardingNext.js +14 -4
  6. package/dist/console/home/HomePage.js +34 -7
  7. package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
  8. package/dist/console/organizations/OrganizationsPage.js +16 -7
  9. package/dist/hooks/useConsoleActionRuntime.js +32 -3
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +3 -0
  12. package/dist/preview/DraftChangesPanel.d.ts +3 -1
  13. package/dist/preview/DraftChangesPanel.js +6 -5
  14. package/dist/utils/deriveRelatedLists.d.ts +20 -5
  15. package/dist/utils/deriveRelatedLists.js +31 -13
  16. package/dist/utils/resolveViewId.d.ts +23 -0
  17. package/dist/utils/resolveViewId.js +37 -0
  18. package/dist/utils/warnSuppressedListNav.d.ts +10 -0
  19. package/dist/utils/warnSuppressedListNav.js +40 -0
  20. package/dist/views/InterfaceListPage.js +6 -4
  21. package/dist/views/ObjectView.js +61 -10
  22. package/dist/views/RecordDetailView.js +131 -104
  23. package/dist/views/RecordFormPage.js +7 -1
  24. package/dist/views/RelatedRecordActionsBridge.d.ts +24 -0
  25. package/dist/views/RelatedRecordActionsBridge.js +114 -0
  26. package/dist/views/metadata-admin/PackagesPage.js +18 -7
  27. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
  28. package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
  29. package/dist/views/metadata-admin/i18n.d.ts +12 -21
  30. package/dist/views/metadata-admin/i18n.js +343 -2
  31. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +25 -11
  32. package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
  33. package/dist/views/metadata-admin/permission-slice.js +70 -0
  34. package/dist/views/metadata-admin/previews/AppNavCanvas.js +11 -7
  35. package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
  36. package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
  37. package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
  38. package/dist/views/studio-design/BuilderLanding.js +133 -0
  39. package/dist/views/studio-design/ObjectFormDesigner.d.ts +31 -0
  40. package/dist/views/studio-design/ObjectFormDesigner.js +226 -0
  41. package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
  42. package/dist/views/studio-design/ObjectSettingsPanel.js +45 -0
  43. package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
  44. package/dist/views/studio-design/ObjectValidationsPanel.js +78 -0
  45. package/dist/views/studio-design/StudioDesignSurface.js +793 -146
  46. package/dist/views/studio-design/metadataError.d.ts +23 -0
  47. package/dist/views/studio-design/metadataError.js +44 -0
  48. package/dist/views/studio-design/packages-io.d.ts +27 -0
  49. package/dist/views/studio-design/packages-io.js +61 -0
  50. 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 { Boxes, FileText, Database, LayoutDashboard, BarChart3, Table2, Folder, Compass, Workflow, SlidersHorizontal, MousePointer2, Eye, Loader2, Save, Pencil, Check, Plus, X, } from 'lucide-react';
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
- 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: [_jsxs("span", { className: "flex items-center gap-1.5 whitespace-nowrap text-[13px] font-medium", children: [_jsx(Boxes, { className: "h-4 w-4" }), " ", packageId] }), _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 ' +
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.label] }, p.key))) })] }), _jsx("div", { className: "min-h-0 flex-1", children: tab === 'data' ? (_jsx(DataPillar, { packageId: packageId })) : tab === 'automations' ? (_jsx(AutomationsPillar, { packageId: packageId })) : (_jsx(InterfacesPillar, { packageId: packageId })) })] })] }));
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
- function InterfacesPillar({ packageId }) {
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 = 'zh-CN';
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
- // Resolve the App by package id load its navigation tree.
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 apps = (await client.list('app'));
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 app = (apps || []).find((a) => a._packageId === packageId || a.packageId === packageId || a.name === packageId) ?? (apps || [])[0];
130
- if (!app)
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
- setAppLabel(String(app.label ?? app.name ?? packageId));
133
- setAppName(String(app.name));
470
+ }
471
+ setAppLabel(label);
472
+ setAppName(name);
134
473
  const [layRaw, appDraftResp] = await Promise.all([
135
- client.layered('app', String(app.name)),
136
- client.getDraft('app', String(app.name)).catch(() => null),
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(e instanceof Error ? e.message : String(e));
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(e instanceof Error ? e.message : String(e));
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(e instanceof Error ? e.message : String(e));
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
- await client.save('app', appName, appDraft, { mode: 'draft' });
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(e instanceof Error ? e.message : String(e));
608
+ setError(formatMetadataError(e));
265
609
  }
266
610
  finally {
267
611
  setNavSaving(false);
268
612
  }
269
- }, [client, appName, appDraft]);
270
- const doNavPublish = React.useCallback(async () => {
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 ? '完成编辑' : '编辑导航(拖拽排序 / 重命名 / 增删)', className: 'inline-flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-[10px] ' +
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" }), " \u5B8C\u6210"] })) : (_jsxs(_Fragment, { children: [_jsx(Pencil, { className: "h-3 w-3" }), " \u7F16\u8F91"] })) })] }), 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: "\u672A\u53D1\u5E03" })), _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" })), "\u4FDD\u5B58\u8349\u7A3F"] }), _jsxs("button", { onClick: doNavPublish, disabled: !navHasDraft || !!navSaving, className: "inline-flex items-center gap-1 rounded-md bg-primary px-2 py-1 text-[11px] font-medium text-primary-foreground disabled:opacity-50", children: [navSaving === 'publish' ? _jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : null, "\u53D1\u5E03"] })] }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-2", children: navTree.length === 0 ? (_jsx("p", { className: "px-2 py-3 text-[11px] text-muted-foreground", children: error ? '加载失败' : '加载中…' })) : editNav ? (_jsx(AppNavCanvas, { draft: appDraft, rootKey: "navigation", onPatch: onNavPatch, selection: navSel, onSelectionChange: (s) => setNavSel(s ? { kind: s.kind, id: s.id } : null) })) : (_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" }), " \u9884\u89C8\u5373\u8FD0\u884C \u00B7 \u540C\u4E00\u6E32\u67D3\u5668"] }), 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", 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: "\u4ECE\u5DE6\u4FA7\u9009\u62E9\u4E00\u4E2A\u83DC\u5355\u9879" })) : 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" }), " \u52A0\u8F7D\u4E2D\u2026"] })) : 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 })) : (_jsxs("div", { className: "py-12 text-center text-xs text-muted-foreground", children: [current.type, " \u6682\u7528\u53EA\u8BFB\u9884\u89C8,\u8BBE\u8BA1\u80FD\u529B\u5EFA\u8BBE\u4E2D\u3002"] })) }), isEditable ? (_jsxs("p", { className: "mt-2 flex items-center gap-1 text-[11px] text-muted-foreground", children: [_jsx(MousePointer2, { className: "h-3 w-3" }), " \u70B9\u9009\u79EF\u6728 \u2192 \u53F3\u4FA7\u76F4\u63A5\u6539 \u00B7 \u6539\u5B8C\u300C\u4FDD\u5B58\u8349\u7A3F\u300D\u2192\u300C\u53D1\u5E03\u300D"] })) : 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" }), " \u8FD0\u884C\u6001\u5217\u8868\u9884\u89C8 \u00B7 \u6539\u5B57\u6BB5 / \u7ED3\u6784\u8BF7\u5230 ", _jsx("span", { className: "font-medium", children: "Data" }), " \u652F\u67F1"] })) : 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: "\u5C5E\u6027" }), 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": "\u53D6\u6D88\u9009\u62E9", children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }), _jsx("div", { className: "p-3", children: 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" }), "\u5728\u753B\u5E03\u91CC\u70B9\u9009\u4E00\u4E2A\u79EF\u6728,", _jsx("br", {}), "\u5B83\u7684\u5C5E\u6027\u4F1A\u5728\u8FD9\u91CC\u76F4\u63A5\u7F16\u8F91\u3002"] })) })] })] })] }));
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 = 'zh-CN';
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
- const list = (await client.list('object', { packageId }));
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(e instanceof Error ? e.message : String(e));
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(e instanceof Error ? e.message : String(e));
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
- const doSave = React.useCallback(async () => {
426
- if (!current)
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
- setSaving('draft');
825
+ }
826
+ setCreateBusy(true);
429
827
  setError(null);
430
828
  try {
431
- await client.save('object', current.name, objDraft, { mode: 'draft' });
432
- setHasDraft(true);
433
- setDirty(false);
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(e instanceof Error ? e.message : String(e));
847
+ setError(formatMetadataError(e));
437
848
  }
438
849
  finally {
439
- setSaving(false);
850
+ setCreateBusy(false);
440
851
  }
441
- }, [client, current, objDraft]);
442
- const doPublish = React.useCallback(async () => {
852
+ }, [newLabel, newName, objects, client, packageId, onDraftSaved]);
853
+ const doSave = React.useCallback(async () => {
443
854
  if (!current)
444
855
  return;
445
- setSaving('publish');
856
+ setSaving('draft');
446
857
  setError(null);
447
858
  try {
448
- await client.publish('object', current.name);
449
- // Bust the data-layer object-schema cache (separate from the metadata client)
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
- setGridVer((v) => v + 1);
862
+ onDraftSaved?.();
455
863
  }
456
864
  catch (e) {
457
- setError(e instanceof Error ? e.message : String(e));
865
+ setError(formatMetadataError(e));
458
866
  }
459
867
  finally {
460
868
  setSaving(false);
461
869
  }
462
- }, [adapter, client, current]);
870
+ }, [client, current, objDraft, onDraftSaved]);
463
871
  // Drag-reorder columns → reorder the object's `fields` metadata (field display
464
- // order follows metadata order), then publish so the new order persists.
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('publish');
888
+ setSaving('draft');
480
889
  setError(null);
481
890
  try {
482
- await client.save('object', current.name, body, { mode: 'draft' });
483
- await client.publish('object', current.name);
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
- setGridVer((v) => v + 1); // remount so the grid reflects the persisted order
894
+ onDraftSaved?.();
895
+ setGridVer((v) => v + 1); // remount so the grid reflects the new (draft) order
488
896
  }
489
897
  catch (e) {
490
- setError(e instanceof Error ? e.message : String(e));
898
+ setError(formatMetadataError(e));
491
899
  }
492
900
  finally {
493
901
  setSaving(false);
494
902
  }
495
- }, [client, current, objDraft, adapter]);
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] }), _jsxs("span", { children: [fieldCount, " \u5B57\u6BB5"] })] })) : (_jsx("span", { className: "text-[11px] text-muted-foreground", children: "\u9009\u62E9\u4E00\u4E2A\u5BF9\u8C61" })), 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 || !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" }), "\u4FDD\u5B58\u8349\u7A3F"] }), _jsxs("button", { onClick: doPublish, disabled: !current || !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: "w-52 shrink-0 overflow-auto border-r p-2", children: [_jsx("p", { className: "px-2 pb-1 pt-1 text-[11px] font-medium text-muted-foreground", children: "\u5BF9\u8C61" }), objects.length === 0 && (_jsx("p", { className: "px-2 py-3 text-[11px] text-muted-foreground", children: error ? '加载失败' : '加载中…' })), objects.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 ' +
498
- (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("main", { className: "flex min-w-0 flex-1 flex-col overflow-hidden p-4", children: !current ? (_jsx("div", { className: "py-16 text-center text-sm text-muted-foreground", children: "\u9009\u62E9\u4E00\u4E2A\u5BF9\u8C61" })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mb-3 flex shrink-0 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" }), " \u8FD0\u884C\u6001\u5217\u8868 \u00B7 \u540C\u4E00\u6E32\u67D3\u5668"] }), _jsxs("button", { type: "button", onClick: addField, title: "\u6DFB\u52A0\u4E00\u4E2A\u5B57\u6BB5(\u968F\u540E\u5728\u53F3\u4FA7\u8BBE\u7F6E\u7C7B\u578B\u4E0E\u5C5E\u6027)", 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" }), " \u6DFB\u52A0\u5B57\u6BB5"] })] }), error && (_jsx("div", { className: "mb-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-1.5 text-[11px] text-destructive", children: error })), _jsx("div", { className: "min-h-0 flex-1 overflow-auto rounded-lg border bg-background", children: _jsx(GridFieldAuthoringProvider, { value: {
499
- onAddColumn: addField,
500
- addColumnLabel: '添加字段',
501
- onEditColumn: (fieldName) => {
502
- // ignore non-field columns (e.g. the row-actions column)
503
- if (readFields(objDraft.fields).entries.some((e) => e.name === fieldName)) {
504
- setFieldSel({ kind: 'field', id: fieldName });
505
- }
506
- },
507
- editColumnLabel: '编辑字段属性',
508
- onReorderFields: doReorderFields,
509
- }, children: _jsx(SchemaRendererProvider, { dataSource: adapter, children: _jsx(PluginObjectView, { schema: {
510
- type: 'object-view',
511
- objectName: current.name,
512
- // No saved view exists in design mode, so show the object's
513
- // own fields as columns (in metadata order), dropping
514
- // framework-managed/audit fields so the grid opens on the
515
- // meaningful columns first — the way Airtable does.
516
- table: {
517
- fields: readFields(objDraft.fields)
518
- .entries.map((e) => e.name)
519
- .filter((n) => !STUDIO_SYSTEM_FIELD_NAMES.has(n)),
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
- }, 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" }), " \u5217\u5934\u300C+\u300D\u52A0\u5B57\u6BB5 \u00B7 \u7B14\u5F62\u6539\u5C5E\u6027 \u00B7 \u62D6\u5217\u5934\u91CD\u6392 \u00B7 \u6539\u5B8C\u300C\u4FDD\u5B58\u8349\u7A3F\u300D\u2192\u300C\u53D1\u5E03\u300D"] })] })) }), 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: "\u5B57\u6BB5\u5C5E\u6027" }), _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": "\u5173\u95ED", 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, {
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 = 'zh-CN';
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(e instanceof Error ? e.message : String(e));
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(e instanceof Error ? e.message : String(e));
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(e instanceof Error ? e.message : String(e));
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: "\u9ED8\u8BA4 OFF \u00B7 \u5BA1\u9605\u540E\u518D\u542F\u7528" }), 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: "w-52 shrink-0 overflow-auto border-r p-2", children: [_jsx("p", { className: "px-2 pb-1 pt-1 text-[11px] font-medium text-muted-foreground", children: "\u81EA\u52A8\u5316 \u00B7 flow" }), flows.length === 0 ? (_jsx("p", { className: "px-2 py-3 text-[11px] text-muted-foreground", children: error ? '加载失败' : '加载中…' })) : (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 ' +
638
- (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))))] }), _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" }), " \u53EF\u89C6\u5316\u7F16\u6392 \u00B7 \u70B9\u9009\u8282\u70B9\u914D\u7F6E"] }), 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", 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: "\u9009\u62E9\u4E00\u4E2A\u81EA\u52A8\u5316" })) : 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" }), " \u52A0\u8F7D\u4E2D\u2026"] })) : Preview ? (React.createElement(Preview, {
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" }), " \u70B9\u9009\u8282\u70B9 \u2192 \u53F3\u4FA7\u914D\u7F6E \u00B7 \u6539\u5B8C\u300C\u4FDD\u5B58\u8349\u7A3F\u300D\u2192\u300C\u53D1\u5E03\u300D"] }))] }), _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: "\u914D\u7F6E" }), 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": "\u53D6\u6D88\u9009\u62E9", children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }), _jsx("div", { className: "p-3", children: selection && inspector && current ? (React.createElement(inspector, {
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" }), "\u5728\u753B\u5E03\u91CC\u70B9\u9009\u4E00\u4E2A\u8282\u70B9,", _jsx("br", {}), "\u5B83\u7684\u914D\u7F6E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u3002"] })) })] })] })] }));
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;