@object-ui/app-shell 11.4.0 → 11.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +178 -0
- package/README.md +9 -0
- package/dist/chrome/KeyboardShortcutsDialog.js +2 -1
- package/dist/console/AppContent.js +145 -26
- package/dist/console/ConsoleShell.js +8 -1
- package/dist/context/CommandPaletteProvider.js +2 -1
- package/dist/hooks/useObjectActions.js +16 -4
- package/dist/layout/AppHeader.js +13 -5
- package/dist/layout/AppSidebar.js +10 -4
- package/dist/observability/sentry.d.ts +5 -0
- package/dist/observability/sentry.js +6 -1
- package/dist/preview/DraftChangesPanel.d.ts +29 -1
- package/dist/preview/DraftChangesPanel.js +141 -14
- package/dist/urlParams.d.ts +68 -0
- package/dist/urlParams.js +76 -0
- package/dist/utils/appRoute.d.ts +15 -0
- package/dist/utils/appRoute.js +22 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/pageTabsUrlSync.d.ts +32 -0
- package/dist/utils/pageTabsUrlSync.js +43 -0
- package/dist/utils/recordFormNavigation.d.ts +40 -0
- package/dist/utils/recordFormNavigation.js +30 -0
- package/dist/views/InterfaceListPage.d.ts +1 -0
- package/dist/views/InterfaceListPage.js +1 -1
- package/dist/views/ObjectDataPage.d.ts +29 -0
- package/dist/views/ObjectDataPage.js +227 -0
- package/dist/views/ObjectView.js +4 -3
- package/dist/views/RecordDetailView.js +61 -20
- package/dist/views/RelatedRecordActionsBridge.d.ts +10 -1
- package/dist/views/RelatedRecordActionsBridge.js +49 -16
- package/dist/views/metadata-admin/ResourceEditPage.js +39 -0
- package/dist/views/metadata-admin/i18n.js +214 -4
- package/dist/views/metadata-admin/inspectors/AppNavInspector.d.ts +11 -4
- package/dist/views/metadata-admin/inspectors/AppNavInspector.js +141 -7
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.d.ts +14 -0
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.js +76 -5
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +35 -19
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +8 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +3 -2
- package/dist/views/metadata-admin/inspectors/nav-target.d.ts +52 -0
- package/dist/views/metadata-admin/inspectors/nav-target.js +149 -0
- package/dist/views/metadata-admin/nav-selection.d.ts +20 -0
- package/dist/views/metadata-admin/nav-selection.js +81 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.js +9 -1
- package/dist/views/metadata-admin/previews/AppPreview.js +4 -2
- package/dist/views/studio-design/BuilderLanding.d.ts +1 -1
- package/dist/views/studio-design/BuilderLanding.js +12 -19
- package/dist/views/studio-design/ObjectFormDesigner.d.ts +5 -3
- package/dist/views/studio-design/ObjectFormDesigner.js +17 -12
- package/dist/views/studio-design/ObjectSettingsPanel.d.ts +1 -1
- package/dist/views/studio-design/ObjectSettingsPanel.js +4 -3
- package/dist/views/studio-design/ObjectValidationsPanel.js +6 -4
- package/dist/views/studio-design/PackageIdInput.d.ts +31 -0
- package/dist/views/studio-design/PackageIdInput.js +40 -0
- package/dist/views/studio-design/StudioDesignSurface.d.ts +13 -0
- package/dist/views/studio-design/StudioDesignSurface.js +227 -57
- package/dist/views/studio-design/packageSurfaces.d.ts +49 -0
- package/dist/views/studio-design/packageSurfaces.js +34 -0
- package/dist/views/studio-design/packages-io.d.ts +11 -0
- package/dist/views/studio-design/packages-io.js +12 -0
- package/dist/views/studio-design/skeletons.d.ts +16 -0
- package/dist/views/studio-design/skeletons.js +51 -0
- package/package.json +38 -38
|
@@ -14,7 +14,7 @@ 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, useNavigate, Link } from 'react-router-dom';
|
|
17
|
+
import { useParams, useNavigate, useSearchParams, 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';
|
|
@@ -25,7 +25,10 @@ import { getMetadataPreview } from '../metadata-admin/preview-registry';
|
|
|
25
25
|
import { PermissionMatrixEditPage } from '../metadata-admin/PermissionMatrixEditor';
|
|
26
26
|
import { getMetadataInspector } from '../metadata-admin/inspector-registry';
|
|
27
27
|
import { useMetadataClient } from '../metadata-admin/useMetadata';
|
|
28
|
+
import { DESIGNER_SEL_PARAM, parseNavSelParam, formatNavSelParam, findNavPositionById, navIdAtPosition, } from '../metadata-admin/nav-selection';
|
|
28
29
|
import { formatMetadataError, formatPublishFailures } from './metadataError';
|
|
30
|
+
import { loadPackageSurfaces } from './packageSurfaces';
|
|
31
|
+
import { buildObjectSkeleton, buildFlowSkeleton, buildAppSkeleton, buildPermissionSkeleton } from './skeletons';
|
|
29
32
|
import { t, tFormat, useMetadataLocale } from '../metadata-admin/i18n';
|
|
30
33
|
import { AppNavCanvas } from '../metadata-admin/previews/AppNavCanvas';
|
|
31
34
|
import { readFields, writeFields, newField, toFieldName, toFieldNameLoose, } from '../metadata-admin/previews/object-fields-io';
|
|
@@ -33,6 +36,7 @@ import { ObjectFormDesigner } from './ObjectFormDesigner';
|
|
|
33
36
|
import { ObjectValidationsPanel } from './ObjectValidationsPanel';
|
|
34
37
|
import { ObjectSettingsPanel } from './ObjectSettingsPanel';
|
|
35
38
|
import { fetchPackages, createBasePackage, PACKAGE_ID_RE } from './packages-io';
|
|
39
|
+
import { PackageIdInput, PackageIdSuggestionHint } from './PackageIdInput';
|
|
36
40
|
import { DraftChangesPanel } from '../../preview/DraftChangesPanel';
|
|
37
41
|
import { toast } from 'sonner';
|
|
38
42
|
const PILLARS = [
|
|
@@ -153,21 +157,38 @@ function PackageSwitcher({ packageId, tab }) {
|
|
|
153
157
|
void doCreate();
|
|
154
158
|
if (e.key === 'Escape')
|
|
155
159
|
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(
|
|
160
|
+
}, 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(PackageIdSuggestionHint, { show: !idTouched && !!newName.trim() && !newId, locale: locale }), _jsx(PackageIdInput, { value: newId, onChange: (v) => {
|
|
157
161
|
setIdTouched(true);
|
|
158
|
-
setNewId(
|
|
159
|
-
},
|
|
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)] })) })] })] }))] }));
|
|
162
|
+
setNewId(v);
|
|
163
|
+
}, onEnter: () => void doCreate(), onEscape: () => setCreating(false), placeholder: t('engine.studio.pkg.idPlaceholder', locale), locale: locale, testId: "pkg-switcher-id-input" }), 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
164
|
}
|
|
166
165
|
export function StudioDesignSurface({ aiSlot }) {
|
|
167
166
|
const params = useParams();
|
|
168
167
|
const packageId = params.packageId ?? 'com.example.showcase';
|
|
169
168
|
const tab = params.tab ?? 'interfaces';
|
|
170
169
|
const locale = useMetadataLocale();
|
|
170
|
+
// Courtesy gate (ADR-0057 D10): a read-only code/installed package refuses
|
|
171
|
+
// authoring server-side (ADR-0070), so don't let the user build up doomed
|
|
172
|
+
// local edits first — disable the authoring affordances up front. Unknown
|
|
173
|
+
// writability (fetch failed / still loading) stays ungated; the server gate
|
|
174
|
+
// remains the authority either way.
|
|
175
|
+
const [pkgWritable, setPkgWritable] = React.useState(null);
|
|
176
|
+
React.useEffect(() => {
|
|
177
|
+
let cancelled = false;
|
|
178
|
+
setPkgWritable(null);
|
|
179
|
+
fetchPackages()
|
|
180
|
+
.then((list) => {
|
|
181
|
+
if (!cancelled)
|
|
182
|
+
setPkgWritable(list.find((p) => p.id === packageId)?.writable ?? null);
|
|
183
|
+
})
|
|
184
|
+
.catch(() => {
|
|
185
|
+
/* unknown — leave ungated */
|
|
186
|
+
});
|
|
187
|
+
return () => {
|
|
188
|
+
cancelled = true;
|
|
189
|
+
};
|
|
190
|
+
}, [packageId]);
|
|
191
|
+
const readOnly = pkgWritable === false;
|
|
171
192
|
// Package-level publish (ADR-0033/0037/0048): edits accumulate as per-item
|
|
172
193
|
// drafts STAMPED with this package (each save passes packageId → the draft row's
|
|
173
194
|
// sys_metadata.package_id). Publishing promotes exactly THIS package's drafts in
|
|
@@ -252,6 +273,27 @@ export function StudioDesignSurface({ aiSlot }) {
|
|
|
252
273
|
const [appNameTouched, setAppNameTouched] = React.useState(false);
|
|
253
274
|
const [appBusy, setAppBusy] = React.useState(false);
|
|
254
275
|
const [appDraftPending, setAppDraftPending] = React.useState(null);
|
|
276
|
+
// Scaffold the new app's navigation from the package's objects (default on) —
|
|
277
|
+
// otherwise a fresh app has zero menu items and every object must be wired by
|
|
278
|
+
// hand in the Interfaces pillar (objectui#2262).
|
|
279
|
+
const [appAddObjects, setAppAddObjects] = React.useState(true);
|
|
280
|
+
const loadPackageObjects = React.useCallback(async () => {
|
|
281
|
+
// Published objects + pending DRAFT objects, merged — a fresh package's
|
|
282
|
+
// objects are usually still drafts (same merge the Data pillar rail does).
|
|
283
|
+
const [list, draftHeaders] = await Promise.all([
|
|
284
|
+
shellClient.list('object', { packageId }),
|
|
285
|
+
shellClient.listDrafts({ packageId, type: 'object' }).catch(() => []),
|
|
286
|
+
]);
|
|
287
|
+
const items = (list || [])
|
|
288
|
+
.map((o) => ({ name: String(o.name ?? ''), label: String(o.label ?? o.name ?? '') }))
|
|
289
|
+
.filter((o) => o.name);
|
|
290
|
+
const known = new Set(items.map((o) => o.name));
|
|
291
|
+
for (const d of draftHeaders) {
|
|
292
|
+
if (d.name && !known.has(d.name))
|
|
293
|
+
items.push({ name: d.name, label: d.name });
|
|
294
|
+
}
|
|
295
|
+
return items;
|
|
296
|
+
}, [shellClient, packageId]);
|
|
255
297
|
const doCreateApp = React.useCallback(async () => {
|
|
256
298
|
const label = appLabel.trim();
|
|
257
299
|
const name = toFieldName(appName.trim() || label);
|
|
@@ -259,7 +301,8 @@ export function StudioDesignSurface({ aiSlot }) {
|
|
|
259
301
|
return;
|
|
260
302
|
setAppBusy(true);
|
|
261
303
|
try {
|
|
262
|
-
|
|
304
|
+
const navObjects = appAddObjects ? await loadPackageObjects().catch(() => []) : [];
|
|
305
|
+
await shellClient.save('app', name, buildAppSkeleton(name, label, navObjects), { mode: 'draft', packageId });
|
|
263
306
|
toast.success(tFormat('engine.studio.app.savedDraft', locale, { label }));
|
|
264
307
|
setAppDraftPending(label);
|
|
265
308
|
setAppCreating(false);
|
|
@@ -274,7 +317,7 @@ export function StudioDesignSurface({ aiSlot }) {
|
|
|
274
317
|
finally {
|
|
275
318
|
setAppBusy(false);
|
|
276
319
|
}
|
|
277
|
-
}, [appLabel, appName, shellClient, packageId]);
|
|
320
|
+
}, [appLabel, appName, appAddObjects, loadPackageObjects, shellClient, packageId]);
|
|
278
321
|
React.useEffect(() => {
|
|
279
322
|
let cancelled = false;
|
|
280
323
|
(async () => {
|
|
@@ -298,7 +341,7 @@ export function StudioDesignSurface({ aiSlot }) {
|
|
|
298
341
|
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 ' +
|
|
299
342
|
(tab === p.key
|
|
300
343
|
? 'bg-primary/10 font-medium text-primary'
|
|
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) => {
|
|
344
|
+
: '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), disabled: readOnly, title: readOnly ? t('engine.studio.pkg.readonlyHint', locale) : 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 disabled:pointer-events-none disabled:opacity-50", 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
345
|
setAppLabel(e.target.value);
|
|
303
346
|
if (!appNameTouched)
|
|
304
347
|
setAppName(toFieldNameLoose(e.target.value));
|
|
@@ -315,7 +358,15 @@ export function StudioDesignSurface({ aiSlot }) {
|
|
|
315
358
|
void doCreateApp();
|
|
316
359
|
if (e.key === 'Escape')
|
|
317
360
|
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",
|
|
361
|
+
}, 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("label", { className: "flex cursor-pointer items-center gap-1.5 px-0.5 py-0.5 text-[11px] text-muted-foreground", children: [_jsx("input", { type: "checkbox", checked: appAddObjects, onChange: (e) => setAppAddObjects(e.target.checked), className: "h-3 w-3 accent-primary" }), t('engine.studio.app.scaffoldNav', locale)] }), _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",
|
|
362
|
+
// Publish is review-then-confirm: open the pending-changes panel,
|
|
363
|
+
// whose footer button fires the actual atomic package publish —
|
|
364
|
+
// never straight from this header click (objectui#2261).
|
|
365
|
+
onClick: () => setChangesOpen(true), disabled: publishing || !hasPending || readOnly, title: readOnly
|
|
366
|
+
? t('engine.studio.pkg.readonlyHint', locale)
|
|
367
|
+
: hasPending
|
|
368
|
+
? t('engine.studio.publishTitle', locale)
|
|
369
|
+
: 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, readOnly: readOnly })) : tab === 'automations' ? (_jsx(AutomationsPillar, { packageId: packageId, publishNonce: publishNonce, onDraftSaved: onDraftSaved, readOnly: readOnly })) : tab === 'access' ? (_jsx(AccessPillar, { packageId: packageId, publishNonce: publishNonce, onDraftSaved: onDraftSaved, readOnly: readOnly })) : (_jsx(InterfacesPillar, { packageId: packageId, publishNonce: publishNonce, draftNonce: draftNonce, onDraftSaved: onDraftSaved, onCreateApp: readOnly ? undefined : () => setAppCreating(true), readOnly: readOnly })) })] }), _jsx(DraftChangesPanel, { open: changesOpen, onOpenChange: setChangesOpen, packageId: packageId, onPublish: readOnly ? undefined : doPublish, publishing: publishing })] }));
|
|
319
370
|
}
|
|
320
371
|
/** Recursive App-navigation tree (groups + typed leaves). */
|
|
321
372
|
function NavTree({ nodes, active, onPick, }) {
|
|
@@ -382,7 +433,7 @@ function StudioNavItemInspector({ navId, appDraft, objects, onNavPatch, onClear,
|
|
|
382
433
|
});
|
|
383
434
|
}, 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
435
|
}
|
|
385
|
-
function InterfacesPillar({ packageId, publishNonce = 0, draftNonce = 0, onDraftSaved, onCreateApp, }) {
|
|
436
|
+
function InterfacesPillar({ packageId, publishNonce = 0, draftNonce = 0, onDraftSaved, onCreateApp, readOnly = false, }) {
|
|
386
437
|
const client = useMetadataClient();
|
|
387
438
|
const locale = useMetadataLocale();
|
|
388
439
|
const [appLabel, setAppLabel] = React.useState(packageId);
|
|
@@ -392,6 +443,37 @@ function InterfacesPillar({ packageId, publishNonce = 0, draftNonce = 0, onDraft
|
|
|
392
443
|
// nav editing — drag-drop reorder / rename / add / remove via AppNavCanvas
|
|
393
444
|
const [editNav, setEditNav] = React.useState(false);
|
|
394
445
|
const [navSel, setNavSel] = React.useState(null);
|
|
446
|
+
// #2272 — designer deep-link: `?sel=nav:<id>` selects the nav item with
|
|
447
|
+
// that spec `id` and switches the pillar into nav editing. The id is the
|
|
448
|
+
// stable external contract; positional `navigation[i]` selection ids stay
|
|
449
|
+
// internal. Selection changes mirror back to the URL (replace).
|
|
450
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
451
|
+
const navSelParam = parseNavSelParam(searchParams.get(DESIGNER_SEL_PARAM));
|
|
452
|
+
const appliedNavSelRef = React.useRef(null);
|
|
453
|
+
React.useEffect(() => {
|
|
454
|
+
if (!navSelParam || navTree.length === 0)
|
|
455
|
+
return;
|
|
456
|
+
if (appliedNavSelRef.current === navSelParam)
|
|
457
|
+
return;
|
|
458
|
+
const hit = findNavPositionById({ navigation: navTree }, navSelParam);
|
|
459
|
+
if (!hit)
|
|
460
|
+
return;
|
|
461
|
+
appliedNavSelRef.current = navSelParam;
|
|
462
|
+
setEditNav(true);
|
|
463
|
+
setNavSel({ kind: 'nav', id: hit.selectionId });
|
|
464
|
+
}, [navSelParam, navTree]);
|
|
465
|
+
React.useEffect(() => {
|
|
466
|
+
const navId = navSel ? navIdAtPosition({ navigation: navTree }, navSel.id) : null;
|
|
467
|
+
setSearchParams((prev) => {
|
|
468
|
+
const next = new URLSearchParams(prev);
|
|
469
|
+
if (navId)
|
|
470
|
+
next.set(DESIGNER_SEL_PARAM, formatNavSelParam(navId));
|
|
471
|
+
else
|
|
472
|
+
next.delete(DESIGNER_SEL_PARAM);
|
|
473
|
+
return next;
|
|
474
|
+
}, { replace: true });
|
|
475
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
476
|
+
}, [navSel]);
|
|
395
477
|
const [navDirty, setNavDirty] = React.useState(false);
|
|
396
478
|
const [navHasDraft, setNavHasDraft] = React.useState(false);
|
|
397
479
|
const [navSaving, setNavSaving] = React.useState(false);
|
|
@@ -611,7 +693,7 @@ function InterfacesPillar({ packageId, publishNonce = 0, draftNonce = 0, onDraft
|
|
|
611
693
|
setNavSaving(false);
|
|
612
694
|
}
|
|
613
695
|
}, [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: () => {
|
|
696
|
+
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 || readOnly, title: readOnly ? t('engine.studio.pkg.readonlyHint', locale) : undefined, 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' && !readOnly && (_jsx("button", { type: "button", onClick: () => {
|
|
615
697
|
setEditNav((v) => !v);
|
|
616
698
|
setNavSel(null);
|
|
617
699
|
}, 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] ' +
|
|
@@ -669,7 +751,7 @@ function renderStudioGridList(props) {
|
|
|
669
751
|
addDeleteRecordsInline: true,
|
|
670
752
|
}, dataSource: ds, onEdit: onEdit, className: className, refreshKey: refreshKey }));
|
|
671
753
|
}
|
|
672
|
-
function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
754
|
+
function DataPillar({ packageId, publishNonce = 0, onDraftSaved, readOnly = false, }) {
|
|
673
755
|
const client = useMetadataClient();
|
|
674
756
|
const adapter = useAdapter();
|
|
675
757
|
const locale = useMetadataLocale();
|
|
@@ -684,6 +766,10 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
684
766
|
const [dirty, setDirty] = React.useState(false);
|
|
685
767
|
const [hasDraft, setHasDraft] = React.useState(false);
|
|
686
768
|
const [saving, setSaving] = React.useState(false);
|
|
769
|
+
// Timestamp of the last successful draft save — renders a "last saved HH:MM"
|
|
770
|
+
// hint next to the Save button (framework#2615 P3: nothing confirmed a draft
|
|
771
|
+
// save persisted, unlike the sibling pillars which toast).
|
|
772
|
+
const [savedAt, setSavedAt] = React.useState(null);
|
|
687
773
|
const [gridVer, setGridVer] = React.useState(0);
|
|
688
774
|
// Records grid ⇄ Form ⇄ Validations ⇄ Settings — four views of the SAME
|
|
689
775
|
// object. Grid/Form are the runtime renderer (same-renderer principle);
|
|
@@ -734,7 +820,7 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
734
820
|
setCurrent((c) => c ?? items[0] ?? null);
|
|
735
821
|
// First-run: an empty writable package opens the creator right away —
|
|
736
822
|
// the first thing to do here is make an object, so put the inputs up.
|
|
737
|
-
if (items.length === 0)
|
|
823
|
+
if (items.length === 0 && !readOnly)
|
|
738
824
|
setCreating(true);
|
|
739
825
|
}
|
|
740
826
|
catch (e) {
|
|
@@ -749,7 +835,7 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
749
835
|
return () => {
|
|
750
836
|
cancelled = true;
|
|
751
837
|
};
|
|
752
|
-
}, [client, packageId]);
|
|
838
|
+
}, [client, packageId, readOnly]);
|
|
753
839
|
React.useEffect(() => {
|
|
754
840
|
if (!current)
|
|
755
841
|
return;
|
|
@@ -801,20 +887,26 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
801
887
|
setDirty(true);
|
|
802
888
|
}, []);
|
|
803
889
|
// "+ add field": append a fresh text field and select it for editing in the panel.
|
|
890
|
+
// Guarded in addition to being hidden — it's also reachable through
|
|
891
|
+
// GridFieldAuthoringProvider/ObjectFormDesigner.
|
|
804
892
|
const addField = React.useCallback(() => {
|
|
893
|
+
if (readOnly)
|
|
894
|
+
return;
|
|
805
895
|
const view = readFields(objDraft.fields);
|
|
806
896
|
const name = nextFieldName(view.entries.map((e) => e.name));
|
|
807
897
|
view.entries.push(newField(name, 'text', t('engine.studio.data.newFieldLabel', locale)));
|
|
808
898
|
setObjDraft((d) => ({ ...d, fields: writeFields(view) }));
|
|
809
899
|
setDirty(true);
|
|
810
900
|
setFieldSel({ kind: 'field', id: name });
|
|
811
|
-
}, [objDraft]);
|
|
901
|
+
}, [objDraft, readOnly]);
|
|
812
902
|
// "+ new object": create a fresh object as a DRAFT in this package (runtime
|
|
813
903
|
// create — same path the classic Studio editor uses), seeded with one text
|
|
814
904
|
// field so the form/grid isn't empty. It stays draft-only (no physical table)
|
|
815
905
|
// until the package publish, so we land on 表单·布局 — the metadata-level
|
|
816
906
|
// surface that never fires data SQL.
|
|
817
907
|
const doCreateObject = React.useCallback(async () => {
|
|
908
|
+
if (readOnly)
|
|
909
|
+
return;
|
|
818
910
|
const label = newLabel.trim();
|
|
819
911
|
const name = toFieldName(newName.trim() || label);
|
|
820
912
|
if (!label || !name || name === 'field')
|
|
@@ -826,11 +918,7 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
826
918
|
setCreateBusy(true);
|
|
827
919
|
setError(null);
|
|
828
920
|
try {
|
|
829
|
-
const body =
|
|
830
|
-
name,
|
|
831
|
-
label,
|
|
832
|
-
fields: { name: { type: 'text', label: t('engine.studio.data.nameFieldLabel', locale) } },
|
|
833
|
-
};
|
|
921
|
+
const body = buildObjectSkeleton(name, label, t('engine.studio.data.nameFieldLabel', locale));
|
|
834
922
|
await client.save('object', name, body, { mode: 'draft', packageId });
|
|
835
923
|
const surface = { type: 'object', name, label };
|
|
836
924
|
setObjects((prev) => [...prev, surface]);
|
|
@@ -849,7 +937,7 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
849
937
|
finally {
|
|
850
938
|
setCreateBusy(false);
|
|
851
939
|
}
|
|
852
|
-
}, [newLabel, newName, objects, client, packageId, onDraftSaved]);
|
|
940
|
+
}, [newLabel, newName, objects, client, packageId, onDraftSaved, readOnly]);
|
|
853
941
|
const doSave = React.useCallback(async () => {
|
|
854
942
|
if (!current)
|
|
855
943
|
return;
|
|
@@ -859,6 +947,8 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
859
947
|
await client.save('object', current.name, objDraft, { mode: 'draft', packageId });
|
|
860
948
|
setHasDraft(true);
|
|
861
949
|
setDirty(false);
|
|
950
|
+
setSavedAt(new Date());
|
|
951
|
+
toast.success(tFormat('engine.studio.data.savedDraft', locale, { label: current.label }));
|
|
862
952
|
onDraftSaved?.();
|
|
863
953
|
}
|
|
864
954
|
catch (e) {
|
|
@@ -867,7 +957,7 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
867
957
|
finally {
|
|
868
958
|
setSaving(false);
|
|
869
959
|
}
|
|
870
|
-
}, [client, current, objDraft, onDraftSaved]);
|
|
960
|
+
}, [client, current, objDraft, onDraftSaved, packageId, locale]);
|
|
871
961
|
// Drag-reorder columns → reorder the object's `fields` metadata (field display
|
|
872
962
|
// order follows metadata order), saved as a DRAFT. Published later via the
|
|
873
963
|
// package release — NOT auto-published per reorder as it used to be.
|
|
@@ -902,12 +992,15 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
902
992
|
}
|
|
903
993
|
}, [client, current, objDraft, onDraftSaved]);
|
|
904
994
|
const inspector = getMetadataInspector('object');
|
|
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) })),
|
|
995
|
+
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) })), savedAt && !dirty && (_jsx("span", { className: "ml-auto text-[11px] text-muted-foreground", "data-testid": "data-saved-at", children: tFormat('engine.studio.data.lastSaved', locale, {
|
|
996
|
+
time: savedAt.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }),
|
|
997
|
+
}) })), _jsxs("button", { onClick: doSave, disabled: !current || !dirty || !!saving || readOnly, title: readOnly ? t('engine.studio.pkg.readonlyHint', locale) : undefined, className: (savedAt && !dirty ? '' : 'ml-auto ') +
|
|
998
|
+
'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
999
|
.filter((o) => !query.trim() ||
|
|
907
1000
|
o.label.toLowerCase().includes(query.trim().toLowerCase()) ||
|
|
908
1001
|
o.name.toLowerCase().includes(query.trim().toLowerCase()))
|
|
909
1002
|
.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) => {
|
|
1003
|
+
(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: readOnly ? (_jsxs("p", { title: t('engine.studio.pkg.readonlyHint', locale), className: "flex items-center gap-1.5 px-2 py-1.5 text-[11px] text-muted-foreground", children: [_jsx(Lock, { className: "h-3 w-3" }), " ", t('engine.studio.pkg.readonly', locale)] })) : creating ? (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("input", { autoFocus: true, value: newLabel, onChange: (e) => {
|
|
911
1004
|
setNewLabel(e.target.value);
|
|
912
1005
|
if (!nameTouched)
|
|
913
1006
|
setNewName(toFieldNameLoose(e.target.value));
|
|
@@ -936,12 +1029,19 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
936
1029
|
? t('engine.studio.data.badge.settings', locale)
|
|
937
1030
|
: formMode === 'layout'
|
|
938
1031
|
? 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: () => {
|
|
1032
|
+
: t('engine.studio.data.badge.formPreview', locale)] }), (viewMode === 'grid' || viewMode === 'form') && !readOnly && (_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, disabled: readOnly })) : viewMode === 'settings' ? (_jsx(ObjectSettingsPanel, { name: current.name, draft: objDraft, onPatch: onPatch, locale: locale, disabled: readOnly })) : 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
1033
|
setViewMode('form');
|
|
941
1034
|
setFormMode('layout');
|
|
942
1035
|
}, 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
|
-
|
|
944
|
-
|
|
1036
|
+
// Read-only package: keep the per-column inspector (view props)
|
|
1037
|
+
// but drop add-column and drag-reorder — both are doomed writes.
|
|
1038
|
+
...(readOnly
|
|
1039
|
+
? {}
|
|
1040
|
+
: {
|
|
1041
|
+
onAddColumn: addField,
|
|
1042
|
+
addColumnLabel: t('engine.studio.data.addField', locale),
|
|
1043
|
+
onReorderFields: doReorderFields,
|
|
1044
|
+
}),
|
|
945
1045
|
onEditColumn: (fieldName) => {
|
|
946
1046
|
// ignore non-field columns (e.g. the row-actions column)
|
|
947
1047
|
if (readFields(objDraft.fields).entries.some((e) => e.name === fieldName)) {
|
|
@@ -949,7 +1049,6 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
949
1049
|
}
|
|
950
1050
|
},
|
|
951
1051
|
editColumnLabel: t('engine.studio.data.editFieldProps', locale),
|
|
952
|
-
onReorderFields: doReorderFields,
|
|
953
1052
|
}, children: _jsx(SchemaRendererProvider, { dataSource: adapter, children: _jsx(PluginObjectView, { schema: {
|
|
954
1053
|
type: 'object-view',
|
|
955
1054
|
objectName: current.name,
|
|
@@ -960,11 +1059,15 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
960
1059
|
table: {
|
|
961
1060
|
fields: readFields(objDraft.fields)
|
|
962
1061
|
.entries.map((e) => e.name)
|
|
963
|
-
|
|
1062
|
+
// Also drop a field named `actions`: the grid always pins
|
|
1063
|
+
// its own row-actions column headed "Actions", so a data
|
|
1064
|
+
// column of the same name reads as a duplicated column.
|
|
1065
|
+
// The field stays editable in the form designer.
|
|
1066
|
+
.filter((n) => !STUDIO_SYSTEM_FIELD_NAMES.has(n) && n !== 'actions'),
|
|
964
1067
|
},
|
|
965
1068
|
}, 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
1069
|
(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: `
|
|
1070
|
+
(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, readOnly: readOnly })) : !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
1071
|
/* Design preview: the form is a click-to-select canvas, not a data-entry
|
|
969
1072
|
* form. Disable interaction on field contents so a click anywhere on a
|
|
970
1073
|
* field routes to its [data-field] wrapper (→ select), and neutralize the
|
|
@@ -997,12 +1100,28 @@ function DataPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
997
1100
|
onPatch,
|
|
998
1101
|
onClearSelection: () => setFieldSel(null),
|
|
999
1102
|
onSelectionChange: setFieldSel,
|
|
1000
|
-
readOnly
|
|
1103
|
+
readOnly,
|
|
1001
1104
|
locale,
|
|
1002
1105
|
}) })] }))] })] }));
|
|
1003
1106
|
}
|
|
1004
|
-
/**
|
|
1005
|
-
|
|
1107
|
+
/**
|
|
1108
|
+
* A flow's live status in the Automations rail: a colored dot + On/Off, from the
|
|
1109
|
+
* engine's runtime state (persisted `status` is intent; this is what's actually
|
|
1110
|
+
* live). Renders nothing for a flow the engine doesn't know yet (never published)
|
|
1111
|
+
* — the amber "unpublished draft" chip already covers that case.
|
|
1112
|
+
*/
|
|
1113
|
+
export function FlowStatusDot({ state, locale }) {
|
|
1114
|
+
if (!state)
|
|
1115
|
+
return null;
|
|
1116
|
+
const { enabled, bound } = state;
|
|
1117
|
+
const title = enabled
|
|
1118
|
+
? bound
|
|
1119
|
+
? t('engine.studio.auto.onBound', locale)
|
|
1120
|
+
: t('engine.studio.auto.onUnbound', locale)
|
|
1121
|
+
: t('engine.studio.auto.offTitle', locale);
|
|
1122
|
+
return (_jsxs("span", { title: title, className: "inline-flex shrink-0 items-center gap-1", children: [_jsx("span", { className: 'h-1.5 w-1.5 rounded-full ' + (enabled ? 'bg-emerald-500' : 'bg-muted-foreground/40') }), _jsx("span", { className: 'text-[10px] ' + (enabled ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'), children: enabled ? t('engine.studio.auto.on', locale) : t('engine.studio.auto.off', locale) })] }));
|
|
1123
|
+
}
|
|
1124
|
+
function AutomationsPillar({ packageId, publishNonce = 0, onDraftSaved, readOnly = false, }) {
|
|
1006
1125
|
const client = useMetadataClient();
|
|
1007
1126
|
const locale = useMetadataLocale();
|
|
1008
1127
|
const [flows, setFlows] = React.useState([]);
|
|
@@ -1025,16 +1144,50 @@ function AutomationsPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
1025
1144
|
const Preview = getMetadataPreview(current?.type ?? '');
|
|
1026
1145
|
const inspector = getMetadataInspector('flow');
|
|
1027
1146
|
const isEditable = !!Preview;
|
|
1147
|
+
// Runtime enable/bound state per flow (GET /automation/_status). Persisted
|
|
1148
|
+
// `status` is intent; this is what's actually live in the engine — the truth
|
|
1149
|
+
// behind the rail's status dots. Refetched after a publish (publishNonce);
|
|
1150
|
+
// degrades silently on an older backend / offline (dots just don't render).
|
|
1151
|
+
const [flowStatus, setFlowStatus] = React.useState({});
|
|
1028
1152
|
React.useEffect(() => {
|
|
1029
1153
|
let cancelled = false;
|
|
1030
1154
|
(async () => {
|
|
1031
1155
|
try {
|
|
1032
|
-
const
|
|
1156
|
+
const res = await fetch('/api/v1/automation/_status', { credentials: 'include', headers: { Accept: 'application/json' } });
|
|
1157
|
+
if (!res.ok)
|
|
1158
|
+
return;
|
|
1159
|
+
const payload = (await res.json().catch(() => null));
|
|
1160
|
+
const list = payload?.data?.flows ?? payload?.flows ?? [];
|
|
1161
|
+
if (cancelled || !Array.isArray(list))
|
|
1162
|
+
return;
|
|
1163
|
+
const map = {};
|
|
1164
|
+
for (const s of list)
|
|
1165
|
+
if (s?.name)
|
|
1166
|
+
map[s.name] = { enabled: s.enabled !== false, bound: !!s.bound };
|
|
1167
|
+
setFlowStatus(map);
|
|
1168
|
+
}
|
|
1169
|
+
catch {
|
|
1170
|
+
/* offline / older backend → no dots */
|
|
1171
|
+
}
|
|
1172
|
+
})();
|
|
1173
|
+
return () => {
|
|
1174
|
+
cancelled = true;
|
|
1175
|
+
};
|
|
1176
|
+
}, [publishNonce]);
|
|
1177
|
+
React.useEffect(() => {
|
|
1178
|
+
let cancelled = false;
|
|
1179
|
+
(async () => {
|
|
1180
|
+
try {
|
|
1181
|
+
// Published flows ∪ pending DRAFT flows — `list()` only sees
|
|
1182
|
+
// published/active metadata, so a just-authored flow that hasn't been
|
|
1183
|
+
// published yet (or a fresh writable-base package whose flows are all
|
|
1184
|
+
// drafts) would render an empty rail even though "Changes · N" shows the
|
|
1185
|
+
// draft exists. Mirrors the Data / Interfaces / Access pillars, which all
|
|
1186
|
+
// merge their drafts. Keyed on `publishNonce` too so drafts that go live
|
|
1187
|
+
// collapse back into the published rail after a package publish.
|
|
1188
|
+
const items = await loadPackageSurfaces(client, 'flow', packageId);
|
|
1033
1189
|
if (cancelled)
|
|
1034
1190
|
return;
|
|
1035
|
-
const items = (list || [])
|
|
1036
|
-
.map((f) => ({ type: 'flow', name: String(f.name ?? ''), label: String(f.label ?? f.name ?? '') }))
|
|
1037
|
-
.filter((f) => f.name);
|
|
1038
1191
|
setFlows(items);
|
|
1039
1192
|
setCurrent((c) => c ?? items[0] ?? null);
|
|
1040
1193
|
}
|
|
@@ -1050,7 +1203,7 @@ function AutomationsPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
1050
1203
|
return () => {
|
|
1051
1204
|
cancelled = true;
|
|
1052
1205
|
};
|
|
1053
|
-
}, [client, packageId]);
|
|
1206
|
+
}, [client, packageId, publishNonce]);
|
|
1054
1207
|
const doCreateFlow = React.useCallback(async () => {
|
|
1055
1208
|
const label = newLabel.trim();
|
|
1056
1209
|
const name = toFieldName(newName.trim() || label);
|
|
@@ -1061,16 +1214,7 @@ function AutomationsPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
1061
1214
|
try {
|
|
1062
1215
|
// Minimal valid, autolaunched skeleton: start → end. The designer fills in
|
|
1063
1216
|
// 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
|
-
};
|
|
1217
|
+
const skeleton = buildFlowSkeleton(name, label, t('engine.studio.auto.nodeStart', locale), t('engine.studio.auto.nodeEnd', locale));
|
|
1074
1218
|
await client.save('flow', name, skeleton, { mode: 'draft', packageId });
|
|
1075
1219
|
const item = { type: 'flow', name, label };
|
|
1076
1220
|
setFlows((fs) => [...fs.filter((f) => f.name !== name), item]);
|
|
@@ -1141,9 +1285,35 @@ function AutomationsPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
1141
1285
|
setSaving(false);
|
|
1142
1286
|
}
|
|
1143
1287
|
}, [client, current, draft, onDraftSaved]);
|
|
1144
|
-
|
|
1288
|
+
// Enable/disable persists via the flow's deployment `status` (active = on,
|
|
1289
|
+
// obsolete = off) — the engine honors it on the next publish. The switch flips
|
|
1290
|
+
// it and saves the draft immediately; the change goes live when the package is
|
|
1291
|
+
// published (so "review before enabling" is preserved).
|
|
1292
|
+
const flowEnabled = draft.status !== 'obsolete' && draft.status !== 'invalid';
|
|
1293
|
+
const toggleEnabled = React.useCallback(async () => {
|
|
1294
|
+
if (!current)
|
|
1295
|
+
return;
|
|
1296
|
+
const next = !(draft.status !== 'obsolete' && draft.status !== 'invalid');
|
|
1297
|
+
const nextDraft = { ...draft, status: next ? 'active' : 'obsolete' };
|
|
1298
|
+
setDraft(nextDraft);
|
|
1299
|
+
setSaving('draft');
|
|
1300
|
+
setError(null);
|
|
1301
|
+
try {
|
|
1302
|
+
await client.save('flow', current.name, nextDraft, { mode: 'draft', packageId });
|
|
1303
|
+
setHasDraft(true);
|
|
1304
|
+
onDraftSaved?.();
|
|
1305
|
+
toast.success(next ? t('engine.studio.auto.enabledToast', locale) : t('engine.studio.auto.disabledToast', locale));
|
|
1306
|
+
}
|
|
1307
|
+
catch (e) {
|
|
1308
|
+
setError(formatMetadataError(e));
|
|
1309
|
+
}
|
|
1310
|
+
finally {
|
|
1311
|
+
setSaving(false);
|
|
1312
|
+
}
|
|
1313
|
+
}, [client, current, draft, packageId, onDraftSaved, locale]);
|
|
1314
|
+
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) })), current && (_jsxs("button", { type: "button", role: "switch", "aria-checked": flowEnabled, onClick: toggleEnabled, disabled: !isEditable || !!saving, title: flowEnabled ? t('engine.studio.auto.disableTitle', locale) : t('engine.studio.auto.enableTitle', locale), className: "inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-[11px] hover:bg-muted disabled:opacity-50", children: [_jsx("span", { className: 'relative inline-flex h-3.5 w-6 shrink-0 items-center rounded-full transition-colors ' + (flowEnabled ? 'bg-emerald-500' : 'bg-muted-foreground/40'), children: _jsx("span", { className: 'inline-block h-2.5 w-2.5 rounded-full bg-white transition-transform ' + (flowEnabled ? 'translate-x-3' : 'translate-x-0.5') }) }), flowEnabled ? t('engine.studio.auto.enabled', locale) : t('engine.studio.auto.disabled', locale)] })), _jsxs("button", { onClick: doSave, disabled: !current || !isEditable || !!saving || readOnly, title: readOnly ? t('engine.studio.pkg.readonlyHint', locale) : undefined, 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) }), !readOnly && (_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
1315
|
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) => {
|
|
1316
|
+
(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 }), _jsx(FlowStatusDot, { state: flowStatus[f.name], locale: locale })] }, 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
1317
|
if (e.key === 'Enter')
|
|
1148
1318
|
void doCreateFlow();
|
|
1149
1319
|
if (e.key === 'Escape')
|
|
@@ -1189,7 +1359,7 @@ function AutomationsPillar({ packageId, publishNonce = 0, onDraftSaved, }) {
|
|
|
1189
1359
|
* leaving other packages' contributed rows untouched (P0). Save writes a package
|
|
1190
1360
|
* DRAFT and publishes with the whole package via the top-bar Publish (P2, D6).
|
|
1191
1361
|
*/
|
|
1192
|
-
function AccessPillar({ packageId, publishNonce, onDraftSaved, }) {
|
|
1362
|
+
function AccessPillar({ packageId, publishNonce, onDraftSaved, readOnly = false, }) {
|
|
1193
1363
|
const client = useMetadataClient();
|
|
1194
1364
|
const locale = useMetadataLocale();
|
|
1195
1365
|
const [perms, setPerms] = React.useState([]);
|
|
@@ -1263,7 +1433,7 @@ function AccessPillar({ packageId, publishNonce, onDraftSaved, }) {
|
|
|
1263
1433
|
try {
|
|
1264
1434
|
// Package door → create as a DRAFT stamped with this package (D6/D7),
|
|
1265
1435
|
// published atomically with the rest of the package.
|
|
1266
|
-
await client.save('permission', name,
|
|
1436
|
+
await client.save('permission', name, buildPermissionSkeleton(name, label), { mode: 'draft', packageId });
|
|
1267
1437
|
toast.success(tFormat('engine.studio.access.created', locale, { label }));
|
|
1268
1438
|
setCreating(false);
|
|
1269
1439
|
setNewLabel('');
|
|
@@ -1301,6 +1471,6 @@ function AccessPillar({ packageId, publishNonce, onDraftSaved, }) {
|
|
|
1301
1471
|
void doCreate();
|
|
1302
1472
|
if (e.key === 'Escape')
|
|
1303
1473
|
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) })) })] })] }));
|
|
1474
|
+
}, 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) })] })] })) : readOnly ? (_jsxs("p", { title: t('engine.studio.pkg.readonlyHint', locale), className: "flex items-center gap-1.5 px-2 py-1.5 text-[11px] text-muted-foreground", children: [_jsx(Lock, { className: "h-3 w-3" }), " ", t('engine.studio.pkg.readonly', 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) })) })] })] }));
|
|
1305
1475
|
}
|
|
1306
1476
|
export default StudioDesignSurface;
|