@object-ui/app-shell 7.0.0 → 7.1.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 +281 -0
- package/dist/console/AppContent.js +14 -2
- package/dist/console/ai/AiChatPage.js +11 -7
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.js +6 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +25 -8
- package/dist/layout/ContextSelectors.js +59 -35
- package/dist/layout/agentPicker.d.ts +56 -0
- package/dist/layout/agentPicker.js +40 -0
- package/dist/preview/CommitTimeline.d.ts +15 -0
- package/dist/preview/CommitTimeline.js +82 -0
- package/dist/preview/UnpublishedAppBar.js +11 -7
- package/dist/preview/commitHistory.d.ts +28 -0
- package/dist/preview/commitHistory.js +48 -0
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/DirectoryPage.js +2 -14
- package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
- package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
- package/dist/views/metadata-admin/PackagesPage.js +9 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +47 -20
- package/dist/views/metadata-admin/ResourceListPage.js +8 -16
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- package/dist/views/metadata-admin/i18n.js +88 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +2 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +122 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +84 -3
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +67 -2
- package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
- package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
- package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +97 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +46 -1
- package/dist/views/metadata-admin/issuePath.d.ts +22 -0
- package/dist/views/metadata-admin/issuePath.js +65 -0
- package/dist/views/metadata-admin/package-scope.d.ts +26 -0
- package/dist/views/metadata-admin/package-scope.js +43 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +7 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +104 -16
- package/dist/views/metadata-admin/previews/FlowPreview.js +31 -3
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
- package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +21 -6
- package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
- package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
- package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
- package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +11 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +72 -0
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
- package/package.json +38 -38
package/dist/views/FlowRunner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* FlowRunner — renders the interactive `screen` of a paused screen-flow run
|
|
4
4
|
* (framework screen-flow runtime, ADR-0019) and resumes it with the collected
|
|
@@ -10,18 +10,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
* On submit it POSTs `/api/v1/automation/{flow}/runs/{runId}/resume` with the
|
|
11
11
|
* field values as `inputs`; a `paused` response renders the next screen
|
|
12
12
|
* (multi-screen wizards), a terminal response closes and refreshes the view.
|
|
13
|
+
*
|
|
14
|
+
* The screen BODY (flat fields / object-form) is rendered by the shared
|
|
15
|
+
* {@link ScreenView} — the same renderer the Studio design preview reuses, so
|
|
16
|
+
* the two can never drift (cf. #1927).
|
|
13
17
|
*/
|
|
14
18
|
import { useEffect, useState } from 'react';
|
|
15
|
-
import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, Button,
|
|
16
|
-
import { ObjectForm } from '@object-ui/plugin-form';
|
|
19
|
+
import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, Button, } from '@object-ui/components';
|
|
17
20
|
import { toast } from 'sonner';
|
|
18
|
-
|
|
19
|
-
const v = {};
|
|
20
|
-
for (const f of screen.fields)
|
|
21
|
-
if (f.defaultValue !== undefined)
|
|
22
|
-
v[f.name] = f.defaultValue;
|
|
23
|
-
return v;
|
|
24
|
-
}
|
|
21
|
+
import { ScreenView, isObjectFormScreen, initialScreenValues } from './ScreenView';
|
|
25
22
|
export function FlowRunner({ state, authFetch, baseUrl, onClose, onComplete, dataSource, objects }) {
|
|
26
23
|
const [screen, setScreen] = useState(null);
|
|
27
24
|
const [runId, setRunId] = useState('');
|
|
@@ -33,7 +30,7 @@ export function FlowRunner({ state, authFetch, baseUrl, onClose, onComplete, dat
|
|
|
33
30
|
setScreen(state.screen);
|
|
34
31
|
setRunId(state.runId);
|
|
35
32
|
setFlowName(state.flowName);
|
|
36
|
-
setValues(
|
|
33
|
+
setValues(initialScreenValues(state.screen));
|
|
37
34
|
}
|
|
38
35
|
}, [state]);
|
|
39
36
|
if (!state || !screen)
|
|
@@ -67,7 +64,7 @@ export function FlowRunner({ state, authFetch, baseUrl, onClose, onComplete, dat
|
|
|
67
64
|
if (data.status === 'paused' && data.screen) {
|
|
68
65
|
setScreen(data.screen);
|
|
69
66
|
setRunId(data.runId || runId);
|
|
70
|
-
setValues(
|
|
67
|
+
setValues(initialScreenValues(data.screen));
|
|
71
68
|
toast.success('Saved — next step');
|
|
72
69
|
}
|
|
73
70
|
else {
|
|
@@ -111,43 +108,14 @@ export function FlowRunner({ state, authFetch, baseUrl, onClose, onComplete, dat
|
|
|
111
108
|
setSubmitting(false);
|
|
112
109
|
}
|
|
113
110
|
};
|
|
114
|
-
const isObjectForm = screen
|
|
115
|
-
const objectDef = isObjectForm && Array.isArray(objects)
|
|
116
|
-
? objects.find((o) => o?.name === screen.objectName)
|
|
117
|
-
: undefined;
|
|
118
|
-
const subforms = objectDef
|
|
119
|
-
? (objectDef.form?.subforms ?? objectDef.formViews?.default?.subforms)
|
|
120
|
-
: undefined;
|
|
111
|
+
const isObjectForm = isObjectFormScreen(screen);
|
|
121
112
|
return (_jsx(Dialog, { open: true, onOpenChange: (o) => { if (!o && !submitting)
|
|
122
|
-
onClose(); }, children: _jsxs(DialogContent, { className: isObjectForm ? 'sm:max-w-3xl max-h-[90vh] overflow-y-auto' : 'sm:max-w-md', children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: screen.title || 'Input' }), screen.description && _jsx(DialogDescription, { children: screen.description })] }),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
subforms,
|
|
131
|
-
onSuccess: onObjectFormSaved,
|
|
132
|
-
onCancel: onClose,
|
|
133
|
-
showSubmit: true,
|
|
134
|
-
showCancel: true,
|
|
135
|
-
submitText: 'Save & Continue',
|
|
136
|
-
cancelText: 'Cancel',
|
|
137
|
-
}, dataSource: dataSource }, screen.nodeId)) : (_jsx("div", { className: "text-sm text-destructive py-4", children: "This step renders an object form but no data source is available." })) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "space-y-4 py-2", children: screen.fields.map((f) => (_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: `ff-${f.name}`, className: "text-sm", children: [f.label || f.name, f.required && _jsx("span", { className: "text-destructive", children: " *" })] }), _jsx(FieldInput, { field: f, value: values[f.name], onChange: (v) => setVal(f.name, v) })] }, f.name))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: submitting, children: "Cancel" }), _jsx(Button, { onClick: submit, disabled: submitting, children: submitting ? 'Submitting…' : 'Submit' })] })] }))] }) }));
|
|
138
|
-
}
|
|
139
|
-
function FieldInput({ field, value, onChange }) {
|
|
140
|
-
const id = `ff-${field.name}`;
|
|
141
|
-
const t = (field.type || 'text').toLowerCase();
|
|
142
|
-
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
143
|
-
return (_jsxs(Select, { value: value != null ? String(value) : undefined, onValueChange: (v) => onChange(v), children: [_jsx(SelectTrigger, { id: id, children: _jsx(SelectValue, { placeholder: field.placeholder || 'Select…' }) }), _jsx(SelectContent, { children: field.options.map((o, i) => (_jsx(SelectItem, { value: String(o.value), children: o.label }, i))) })] }));
|
|
144
|
-
}
|
|
145
|
-
if (t === 'boolean' || t === 'checkbox') {
|
|
146
|
-
return _jsx(Checkbox, { id: id, checked: value === true, onCheckedChange: (c) => onChange(c === true) });
|
|
147
|
-
}
|
|
148
|
-
if (t === 'textarea' || t === 'markdown') {
|
|
149
|
-
return _jsx(Textarea, { id: id, value: value ?? '', placeholder: field.placeholder, onChange: (e) => onChange(e.target.value) });
|
|
150
|
-
}
|
|
151
|
-
const htmlType = t === 'number' || t === 'currency' ? 'number' : t === 'email' ? 'email' : t === 'date' ? 'date' : 'text';
|
|
152
|
-
return (_jsx(Input, { id: id, type: htmlType, value: value ?? '', placeholder: field.placeholder, onChange: (e) => onChange(htmlType === 'number' ? (e.target.value === '' ? undefined : Number(e.target.value)) : e.target.value) }));
|
|
113
|
+
onClose(); }, children: _jsxs(DialogContent, { className: isObjectForm ? 'sm:max-w-3xl max-h-[90vh] overflow-y-auto' : 'sm:max-w-md', children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: screen.title || 'Input' }), screen.description && _jsx(DialogDescription, { children: screen.description })] }), _jsx(ScreenView, { screen: screen, values: values, onValueChange: setVal, dataSource: dataSource, objects: objects, objectForm: {
|
|
114
|
+
onSuccess: onObjectFormSaved,
|
|
115
|
+
onCancel: onClose,
|
|
116
|
+
showSubmit: true,
|
|
117
|
+
showCancel: true,
|
|
118
|
+
submitText: 'Save & Continue',
|
|
119
|
+
cancelText: 'Cancel',
|
|
120
|
+
} }), !isObjectForm && (_jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: submitting, children: "Cancel" }), _jsx(Button, { onClick: submit, disabled: submitting, children: submitting ? 'Submitting…' : 'Submit' })] }))] }) }));
|
|
153
121
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface ScreenFieldSpec {
|
|
2
|
+
name: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
type?: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
options?: Array<{
|
|
7
|
+
value: unknown;
|
|
8
|
+
label: string;
|
|
9
|
+
}>;
|
|
10
|
+
defaultValue?: unknown;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ScreenSpec {
|
|
14
|
+
nodeId: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
fields: ScreenFieldSpec[];
|
|
18
|
+
/**
|
|
19
|
+
* `'object-form'` renders the named object's FULL create/edit form — incl.
|
|
20
|
+
* inline master-detail child grids — as a wizard step (vs. the flat `fields`
|
|
21
|
+
* list). The form persists the record (and its children, atomically) itself,
|
|
22
|
+
* then resumes the run with the saved id bound to `idVariable`.
|
|
23
|
+
*/
|
|
24
|
+
kind?: 'fields' | 'object-form';
|
|
25
|
+
objectName?: string;
|
|
26
|
+
mode?: 'create' | 'edit';
|
|
27
|
+
recordId?: string;
|
|
28
|
+
defaults?: Record<string, unknown>;
|
|
29
|
+
idVariable?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Whether a screen renders the object-form body rather than the flat fields. */
|
|
32
|
+
export declare function isObjectFormScreen(screen: ScreenSpec): boolean;
|
|
33
|
+
/** Seed flat-field values from each field's `defaultValue`. */
|
|
34
|
+
export declare function initialScreenValues(screen: ScreenSpec): Record<string, unknown>;
|
|
35
|
+
/** Submit/cancel wiring for the object-form body — runtime persists & resumes;
|
|
36
|
+
* the design preview hides the bar (`showSubmit`/`showCancel` false). */
|
|
37
|
+
export interface ScreenObjectFormActions {
|
|
38
|
+
showSubmit?: boolean;
|
|
39
|
+
showCancel?: boolean;
|
|
40
|
+
submitText?: string;
|
|
41
|
+
cancelText?: string;
|
|
42
|
+
onSuccess?: (saved: any) => void;
|
|
43
|
+
onCancel?: () => void;
|
|
44
|
+
/** Overrides the "no data source" copy (the preview phrases it for authors). */
|
|
45
|
+
noDataSourceMessage?: React.ReactNode;
|
|
46
|
+
}
|
|
47
|
+
export interface ScreenViewProps {
|
|
48
|
+
screen: ScreenSpec;
|
|
49
|
+
/** Controlled values for the flat-fields body. */
|
|
50
|
+
values: Record<string, unknown>;
|
|
51
|
+
onValueChange: (name: string, value: unknown) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Data source — required to render the `object-form` body. ObjectForm fetches
|
|
54
|
+
* the object schema (and persists) through this adapter.
|
|
55
|
+
*/
|
|
56
|
+
dataSource?: any;
|
|
57
|
+
/**
|
|
58
|
+
* Object definitions — used to derive an `object-form` step's inline
|
|
59
|
+
* master-detail `subforms` (mirrors RecordFormPage's create form).
|
|
60
|
+
*/
|
|
61
|
+
objects?: any[];
|
|
62
|
+
objectForm?: ScreenObjectFormActions;
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function ScreenView({ screen, values, onValueChange, dataSource, objects, objectForm, className }: ScreenViewProps): import("react").JSX.Element;
|
|
66
|
+
export declare function FieldInput({ field, value, onChange }: {
|
|
67
|
+
field: ScreenFieldSpec;
|
|
68
|
+
value: unknown;
|
|
69
|
+
onChange: (v: unknown) => void;
|
|
70
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
3
|
+
/**
|
|
4
|
+
* ScreenView — the presentational body of a flow `screen` (framework
|
|
5
|
+
* screen-flow runtime, ADR-0019): the flat input-field list OR the named
|
|
6
|
+
* object's full create/edit form.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from {@link FlowRunner} so the exact same renderer drives both the
|
|
9
|
+
* runtime (paused screen-flow → collect input → resume) and the Studio design
|
|
10
|
+
* preview ({@link ScreenPreview}). Keeping ONE renderer is deliberate: a
|
|
11
|
+
* separate preview reimplementation would drift from runtime — the
|
|
12
|
+
* simulator-vs-engine divergence fixed in #1927.
|
|
13
|
+
*
|
|
14
|
+
* It owns no submit/resume behaviour and no Dialog chrome — the caller frames
|
|
15
|
+
* it (the runtime wraps it in a Dialog + footer and resumes the run; the
|
|
16
|
+
* preview wraps it in a card and hides the persist bar).
|
|
17
|
+
*/
|
|
18
|
+
import { Input, Label, Textarea, Checkbox, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, cn, } from '@object-ui/components';
|
|
19
|
+
import { ObjectForm } from '@object-ui/plugin-form';
|
|
20
|
+
/** Whether a screen renders the object-form body rather than the flat fields. */
|
|
21
|
+
export function isObjectFormScreen(screen) {
|
|
22
|
+
return screen.kind === 'object-form' && !!screen.objectName;
|
|
23
|
+
}
|
|
24
|
+
/** Seed flat-field values from each field's `defaultValue`. */
|
|
25
|
+
export function initialScreenValues(screen) {
|
|
26
|
+
const v = {};
|
|
27
|
+
for (const f of screen.fields)
|
|
28
|
+
if (f.defaultValue !== undefined)
|
|
29
|
+
v[f.name] = f.defaultValue;
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
export function ScreenView({ screen, values, onValueChange, dataSource, objects, objectForm, className }) {
|
|
33
|
+
if (isObjectFormScreen(screen)) {
|
|
34
|
+
const objectDef = Array.isArray(objects) ? objects.find((o) => o?.name === screen.objectName) : undefined;
|
|
35
|
+
const subforms = objectDef
|
|
36
|
+
? (objectDef.form?.subforms ?? objectDef.formViews?.default?.subforms)
|
|
37
|
+
: undefined;
|
|
38
|
+
// Full object create/edit form (with inline master-detail grids). At runtime
|
|
39
|
+
// the form owns its own Save/Cancel bar; the preview hides it.
|
|
40
|
+
return (_jsx("div", { className: cn('py-2', className), children: dataSource ? (_jsx(ObjectForm, { schema: {
|
|
41
|
+
type: 'object-form',
|
|
42
|
+
formType: 'simple',
|
|
43
|
+
objectName: screen.objectName,
|
|
44
|
+
mode: screen.mode === 'edit' ? 'edit' : 'create',
|
|
45
|
+
recordId: screen.mode === 'edit' ? screen.recordId : undefined,
|
|
46
|
+
...(screen.defaults ? { initialValues: screen.defaults } : {}),
|
|
47
|
+
layout: 'vertical',
|
|
48
|
+
subforms,
|
|
49
|
+
onSuccess: objectForm?.onSuccess,
|
|
50
|
+
onCancel: objectForm?.onCancel,
|
|
51
|
+
showSubmit: objectForm?.showSubmit ?? true,
|
|
52
|
+
showCancel: objectForm?.showCancel ?? true,
|
|
53
|
+
submitText: objectForm?.submitText ?? 'Save & Continue',
|
|
54
|
+
cancelText: objectForm?.cancelText ?? 'Cancel',
|
|
55
|
+
}, dataSource: dataSource }, screen.nodeId)) : (_jsx("div", { className: "text-sm text-destructive py-4", children: objectForm?.noDataSourceMessage ?? 'This step renders an object form but no data source is available.' })) }));
|
|
56
|
+
}
|
|
57
|
+
return (_jsx("div", { className: cn('space-y-4 py-2', className), children: screen.fields.map((f) => (_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { htmlFor: `ff-${f.name}`, className: "text-sm", children: [f.label || f.name, f.required && _jsx("span", { className: "text-destructive", children: " *" })] }), _jsx(FieldInput, { field: f, value: values[f.name], onChange: (v) => onValueChange(f.name, v) })] }, f.name))) }));
|
|
58
|
+
}
|
|
59
|
+
export function FieldInput({ field, value, onChange }) {
|
|
60
|
+
const id = `ff-${field.name}`;
|
|
61
|
+
const t = (field.type || 'text').toLowerCase();
|
|
62
|
+
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
63
|
+
return (_jsxs(Select, { value: value != null ? String(value) : undefined, onValueChange: (v) => onChange(v), children: [_jsx(SelectTrigger, { id: id, children: _jsx(SelectValue, { placeholder: field.placeholder || 'Select…' }) }), _jsx(SelectContent, { children: field.options.map((o, i) => (_jsx(SelectItem, { value: String(o.value), children: o.label }, i))) })] }));
|
|
64
|
+
}
|
|
65
|
+
if (t === 'boolean' || t === 'checkbox') {
|
|
66
|
+
return _jsx(Checkbox, { id: id, checked: value === true, onCheckedChange: (c) => onChange(c === true) });
|
|
67
|
+
}
|
|
68
|
+
if (t === 'textarea' || t === 'markdown') {
|
|
69
|
+
return _jsx(Textarea, { id: id, value: value ?? '', placeholder: field.placeholder, onChange: (e) => onChange(e.target.value) });
|
|
70
|
+
}
|
|
71
|
+
const htmlType = t === 'number' || t === 'currency' ? 'number' : t === 'email' ? 'email' : t === 'date' ? 'date' : 'text';
|
|
72
|
+
return (_jsx(Input, { id: id, type: htmlType, value: value ?? '', placeholder: field.placeholder, onChange: (e) => onChange(htmlType === 'number' ? (e.target.value === '' ? undefined : Number(e.target.value)) : e.target.value) }));
|
|
73
|
+
}
|
|
@@ -25,6 +25,7 @@ import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
|
25
25
|
import { useMetadataClient, useMetadataTypes, useGlobalDiagnostics, } from './useMetadata';
|
|
26
26
|
import { MetadataQuickFind } from './QuickFind';
|
|
27
27
|
import { translateMetadataType, translateMetadataDomain, t, tFormat, detectLocale, } from './i18n';
|
|
28
|
+
import { buildPackageScopeOptions } from './package-scope';
|
|
28
29
|
const DOMAIN_ICONS = {
|
|
29
30
|
data: Database,
|
|
30
31
|
ui: Layers,
|
|
@@ -79,20 +80,7 @@ export function MetadataDirectoryPage() {
|
|
|
79
80
|
const list = await client.list('package');
|
|
80
81
|
if (cancelled)
|
|
81
82
|
return;
|
|
82
|
-
|
|
83
|
-
const rows = (list ?? [])
|
|
84
|
-
.map((raw) => {
|
|
85
|
-
const item = raw && typeof raw === 'object' && 'item' in raw ? raw.item : raw;
|
|
86
|
-
const m = (item?.manifest ?? item ?? {});
|
|
87
|
-
return {
|
|
88
|
-
id: m.id,
|
|
89
|
-
scope: m.scope,
|
|
90
|
-
name: m.name || m.id,
|
|
91
|
-
};
|
|
92
|
-
})
|
|
93
|
-
.filter((p) => p.id && !SYSTEM_SCOPES.has(p.scope));
|
|
94
|
-
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
95
|
-
setProjectPackages(rows.map((p) => ({ id: p.id, name: p.name })));
|
|
83
|
+
setProjectPackages(buildPackageScopeOptions(list));
|
|
96
84
|
}
|
|
97
85
|
catch {
|
|
98
86
|
if (!cancelled)
|
|
@@ -32,6 +32,8 @@ export interface JsonSourceEditorProps {
|
|
|
32
32
|
issues?: JsonIssue[];
|
|
33
33
|
/** Pixel or CSS-length height. Defaults to `60vh`. */
|
|
34
34
|
height?: string | number;
|
|
35
|
+
/** Grace period (ms) before the textarea fallback engages. Test-tunable. */
|
|
36
|
+
fallbackDelayMs?: number;
|
|
35
37
|
}
|
|
36
|
-
export declare function JsonSourceEditor({ value, onChange, readOnly, issues, height, }: JsonSourceEditorProps): React.JSX.Element;
|
|
38
|
+
export declare function JsonSourceEditor({ value, onChange, readOnly, issues, height, fallbackDelayMs, }: JsonSourceEditorProps): React.JSX.Element;
|
|
37
39
|
export default JsonSourceEditor;
|
|
@@ -45,7 +45,7 @@ function splitPath(p) {
|
|
|
45
45
|
return Number.isInteger(n) && String(n) === seg ? n : seg;
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
-
export function JsonSourceEditor({ value, onChange, readOnly, issues, height = '60vh', }) {
|
|
48
|
+
export function JsonSourceEditor({ value, onChange, readOnly, issues, height = '60vh', fallbackDelayMs = 4000, }) {
|
|
49
49
|
const locale = React.useMemo(() => detectLocale(), []);
|
|
50
50
|
const [text, setText] = React.useState(() => stringify(value));
|
|
51
51
|
const [parseError, setParseError] = React.useState(null);
|
|
@@ -54,6 +54,24 @@ export function JsonSourceEditor({ value, onChange, readOnly, issues, height = '
|
|
|
54
54
|
// when either the source text or the issues prop changes.
|
|
55
55
|
const editorRef = React.useRef(null);
|
|
56
56
|
const monacoRef = React.useRef(null);
|
|
57
|
+
// Monaco's core is fetched lazily and, by default, from a public CDN, and it
|
|
58
|
+
// also spins up web workers. When any of that is blocked — offline /
|
|
59
|
+
// air-gapped / CSP-restricted installs — the editor mounts an empty shell
|
|
60
|
+
// with no error and the Source tab looks blank. Detect "nothing actually
|
|
61
|
+
// painted" via the rendered `.view-line` rows and fall back to a plain
|
|
62
|
+
// textarea so the source is always readable and editable.
|
|
63
|
+
const containerRef = React.useRef(null);
|
|
64
|
+
const [monacoUnavailable, setMonacoUnavailable] = React.useState(false);
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
if (monacoUnavailable)
|
|
67
|
+
return;
|
|
68
|
+
const id = setTimeout(() => {
|
|
69
|
+
const el = containerRef.current;
|
|
70
|
+
if (!el || !el.querySelector('.view-line'))
|
|
71
|
+
setMonacoUnavailable(true);
|
|
72
|
+
}, fallbackDelayMs);
|
|
73
|
+
return () => clearTimeout(id);
|
|
74
|
+
}, [monacoUnavailable, fallbackDelayMs]);
|
|
57
75
|
// Match against the dark class our app-shell toggles on <html>; pick
|
|
58
76
|
// a Monaco theme that doesn't fight the rest of the chrome.
|
|
59
77
|
const [theme, setTheme] = React.useState(() => {
|
|
@@ -161,7 +179,7 @@ export function JsonSourceEditor({ value, onChange, readOnly, issues, height = '
|
|
|
161
179
|
// Defer one tick so the model has settled before the first paint.
|
|
162
180
|
setTimeout(applyMarkers, 0);
|
|
163
181
|
};
|
|
164
|
-
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("div", { className: "border rounded overflow-hidden bg-background", style: { height: typeof height === 'number' ? `${height}px` : height }, children: _jsx(React.Suspense, { fallback: _jsx(Skeleton, { className: "w-full h-full" }), children: _jsx(LazyMonaco, { value: text, language: "json", theme: theme, onChange: handleChange, onMount: handleMount, options: {
|
|
182
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("div", { ref: containerRef, "data-testid": "source-editor", className: "border rounded overflow-hidden bg-background", style: { height: typeof height === 'number' ? `${height}px` : height }, children: monacoUnavailable ? (_jsx("textarea", { value: text, onChange: (e) => handleChange(e.target.value), readOnly: readOnly, spellCheck: false, "aria-label": "JSON source", className: "w-full h-full resize-none bg-background p-3 font-mono text-xs leading-relaxed outline-none" })) : (_jsx(React.Suspense, { fallback: _jsx(Skeleton, { className: "w-full h-full" }), children: _jsx(LazyMonaco, { value: text, language: "json", theme: theme, onChange: handleChange, onMount: handleMount, options: {
|
|
165
183
|
readOnly,
|
|
166
184
|
minimap: { enabled: false },
|
|
167
185
|
fontSize: 12,
|
|
@@ -173,6 +191,6 @@ export function JsonSourceEditor({ value, onChange, readOnly, issues, height = '
|
|
|
173
191
|
tabSize: 2,
|
|
174
192
|
renderLineHighlight: 'line',
|
|
175
193
|
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
|
|
176
|
-
} }) }) }), parseError && (_jsxs("div", { className: "text-xs text-destructive flex items-start gap-1.5", children: [_jsx("span", { "aria-hidden": true, children: "\u26A0" }), _jsx("span", { children: parseError })] }))] }));
|
|
194
|
+
} }) })) }), parseError && (_jsxs("div", { className: "text-xs text-destructive flex items-start gap-1.5", children: [_jsx("span", { "aria-hidden": true, children: "\u26A0" }), _jsx("span", { children: parseError })] }))] }));
|
|
177
195
|
}
|
|
178
196
|
export default JsonSourceEditor;
|
|
@@ -104,6 +104,14 @@ function CreatePackageDialog({ open, onOpenChange, onCreated, }) {
|
|
|
104
104
|
}),
|
|
105
105
|
});
|
|
106
106
|
onCreated(id.trim());
|
|
107
|
+
// Let context selectors (e.g. the sidebar package switcher) pick up the
|
|
108
|
+
// new package without a full page reload.
|
|
109
|
+
try {
|
|
110
|
+
window.dispatchEvent(new CustomEvent('objectui:packages-changed'));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* non-DOM env */
|
|
114
|
+
}
|
|
107
115
|
onOpenChange(false);
|
|
108
116
|
}
|
|
109
117
|
catch (e) {
|
|
@@ -113,7 +121,7 @@ function CreatePackageDialog({ open, onOpenChange, onCreated, }) {
|
|
|
113
121
|
setBusy(false);
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('engine.packages.create.title', locale) }), _jsx(DialogDescription, { children: tFormat('engine.packages.create.description', locale, { example: 'com.acme.crm' }) })] }), _jsxs("div", { className: "space-y-4 py-2", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-id", children: t('engine.packages.create.id', locale) }), _jsx(Input, { id: "pkg-id", placeholder: "com.acme.crm", value: id, onChange: (e) => setId(e.target.value), "aria-invalid": !!id && !idValid }), !!id && !idValid && (_jsx("p", { className: "text-xs text-destructive", children: t('engine.packages.create.idInvalid', locale) }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-name", children: t('engine.packages.create.name', locale) }), _jsx(Input, { id: "pkg-name", placeholder: "Acme CRM", value: name, onChange: (e) => setName(e.target.value) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-version", children: t('engine.packages.create.version', locale) }), _jsx(Input, { id: "pkg-version", placeholder: "0.1.0", value: version, onChange: (e) => setVersion(e.target.value), "aria-invalid": !!version && !versionValid }), !!version && !versionValid && (_jsx("p", { className: "text-xs text-destructive", children: t('engine.packages.create.versionInvalid', locale) }))] }), error && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-2 text-sm text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsx("span", { children: error })] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: busy, children: t('engine.cancel', locale) }), _jsx(Button, { onClick: submit, disabled: !canSubmit, children: busy ? t('engine.packages.create.creating', locale) : t('engine.packages.create.submit', locale) })] })] }) }));
|
|
124
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('engine.packages.create.title', locale) }), _jsx(DialogDescription, { children: tFormat('engine.packages.create.description', locale, { example: 'com.acme.crm' }) })] }), _jsxs("div", { className: "space-y-4 py-2", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-id", children: t('engine.packages.create.id', locale) }), _jsx(Input, { id: "pkg-id", "data-testid": "package-id-input", placeholder: "com.acme.crm", value: id, onChange: (e) => setId(e.target.value), "aria-invalid": !!id && !idValid }), !!id && !idValid && (_jsx("p", { className: "text-xs text-destructive", children: t('engine.packages.create.idInvalid', locale) }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-name", children: t('engine.packages.create.name', locale) }), _jsx(Input, { id: "pkg-name", "data-testid": "package-name-input", placeholder: "Acme CRM", value: name, onChange: (e) => setName(e.target.value) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-version", children: t('engine.packages.create.version', locale) }), _jsx(Input, { id: "pkg-version", placeholder: "0.1.0", value: version, onChange: (e) => setVersion(e.target.value), "aria-invalid": !!version && !versionValid }), !!version && !versionValid && (_jsx("p", { className: "text-xs text-destructive", children: t('engine.packages.create.versionInvalid', locale) }))] }), error && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-2 text-sm text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsx("span", { children: error })] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: busy, children: t('engine.cancel', locale) }), _jsx(Button, { onClick: submit, disabled: !canSubmit, children: busy ? t('engine.packages.create.creating', locale) : t('engine.packages.create.submit', locale) })] })] }) }));
|
|
117
125
|
}
|
|
118
126
|
/* -------------------------------------------------------------------------- */
|
|
119
127
|
/* Detail sheet — manifest + lifecycle actions */
|
|
@@ -48,6 +48,7 @@ import { getMetadataDefaultInspector } from './default-inspector-registry';
|
|
|
48
48
|
import { detectLocale, t, tFormat, translateValidationMessage } from './i18n';
|
|
49
49
|
import { JsonSourceEditor } from './JsonSourceEditor';
|
|
50
50
|
import { validateMetadataDraft, hasClientValidator } from './clientValidation';
|
|
51
|
+
import { describeIssuePath } from './issuePath';
|
|
51
52
|
// react-resizable-panels' `direction` prop type does not always narrow
|
|
52
53
|
// cleanly in our TS config; cast at the boundary (precedent:
|
|
53
54
|
// packages/components/src/custom/navigation-overlay.tsx).
|
|
@@ -59,7 +60,7 @@ const PanelGroup = ResizablePanelGroup;
|
|
|
59
60
|
* editable via the no-selection default inspector. Other types keep
|
|
60
61
|
* the conventional "name it first, design after save" create flow.
|
|
61
62
|
*/
|
|
62
|
-
const CREATE_MODE_CANVAS_TYPES = new Set(['object']);
|
|
63
|
+
const CREATE_MODE_CANVAS_TYPES = new Set(['object', 'report', 'dataset']);
|
|
63
64
|
/**
|
|
64
65
|
* Top-level metadata keys that a type's canvas PreviewComponent owns and
|
|
65
66
|
* edits visually (e.g. the object designer owns `fields` + `fieldGroups`).
|
|
@@ -749,7 +750,10 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
749
750
|
const PC = getMetadataPreview(type);
|
|
750
751
|
if (!PC)
|
|
751
752
|
return;
|
|
752
|
-
|
|
753
|
+
// See `isArtifactItem` below — a `sys_metadata`-tagged code layer is a
|
|
754
|
+
// published org object, NOT a packaged artifact, so it stays editable.
|
|
755
|
+
const isArtifact = layered?.code != null
|
|
756
|
+
&& layered.code?._packageId !== 'sys_metadata';
|
|
753
757
|
const cw = isArtifact
|
|
754
758
|
? !!entry?.allowOrgOverride
|
|
755
759
|
: !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate);
|
|
@@ -815,22 +819,29 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
815
819
|
const key = path.split('.')[0];
|
|
816
820
|
if (!key)
|
|
817
821
|
return path;
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
const
|
|
822
|
-
for (const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
822
|
+
// Resolve the human label for the HEAD segment from the form/schema.
|
|
823
|
+
const headLabel = (() => {
|
|
824
|
+
const formForLabels = (createMode && config.createSchema ? undefined : entry?.form);
|
|
825
|
+
const sections = Array.isArray(formForLabels?.sections) ? formForLabels.sections : [];
|
|
826
|
+
for (const section of sections) {
|
|
827
|
+
const fields = Array.isArray(section?.fields) ? section.fields : [];
|
|
828
|
+
for (const field of fields) {
|
|
829
|
+
if (typeof field === 'string') {
|
|
830
|
+
if (field === key)
|
|
831
|
+
return field;
|
|
832
|
+
}
|
|
833
|
+
else if (field?.field === key) {
|
|
834
|
+
return String(field.label ?? key);
|
|
835
|
+
}
|
|
829
836
|
}
|
|
830
837
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
838
|
+
const props = (schema?.properties ?? {});
|
|
839
|
+
return String(props[key]?.title ?? key);
|
|
840
|
+
})();
|
|
841
|
+
// For a NESTED path (e.g. `widgets.2.layout`) append a readable trail naming
|
|
842
|
+
// the offending element + sub-field, so a terse "Widgets: Invalid input"
|
|
843
|
+
// becomes "Widgets → priority_split → layout".
|
|
844
|
+
return describeIssuePath(headLabel, path, draft);
|
|
834
845
|
}
|
|
835
846
|
async function doSave(force) {
|
|
836
847
|
setSaving(true);
|
|
@@ -929,7 +940,15 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
929
940
|
if (!createMode && !stayInEditing)
|
|
930
941
|
setEditing(false);
|
|
931
942
|
if (createMode) {
|
|
932
|
-
|
|
943
|
+
// Preserve the active query string (notably `?package=…`) so the
|
|
944
|
+
// post-create navigation lands on the item in the SAME package the
|
|
945
|
+
// author was working in. Without this the param is dropped and the
|
|
946
|
+
// editor falls back to the user's default package, where the freshly
|
|
947
|
+
// saved draft doesn't exist — so it reloads a blank form.
|
|
948
|
+
const qs = searchParams.toString();
|
|
949
|
+
navigate(`../${encodeURIComponent(savedName)}${qs ? `?${qs}` : ''}`, {
|
|
950
|
+
relative: 'path',
|
|
951
|
+
});
|
|
933
952
|
}
|
|
934
953
|
}
|
|
935
954
|
catch (err) {
|
|
@@ -1101,7 +1120,15 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
1101
1120
|
// - artifact-backed items (layered.code != null) need allowOrgOverride
|
|
1102
1121
|
// - DB-only items (no artifact) need allowOrgOverride OR allowRuntimeCreate
|
|
1103
1122
|
// - createMode is always writable (the server will gate on intent)
|
|
1104
|
-
|
|
1123
|
+
// A non-null `code` layer alone is NOT proof of a code (artifact) package:
|
|
1124
|
+
// a published org object also surfaces its active version in `code`, but
|
|
1125
|
+
// tagged with the `sys_metadata` provenance sentinel. Mirror the server's
|
|
1126
|
+
// `isArtifactBacked` (which excludes `_packageId === 'sys_metadata'`) so an
|
|
1127
|
+
// org-authored object stays editable after publish instead of being mis-read
|
|
1128
|
+
// as a read-only packaged item.
|
|
1129
|
+
const isArtifactItem = !createMode
|
|
1130
|
+
&& layered?.code != null
|
|
1131
|
+
&& layered.code?._packageId !== 'sys_metadata';
|
|
1105
1132
|
// ADR-0010 — server-computed lock flags. undefined means "no opinion"
|
|
1106
1133
|
// (older server / non-lockable item) → preserve legacy behaviour.
|
|
1107
1134
|
const lockEditable = layered?.editable !== false;
|
|
@@ -1320,7 +1347,7 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
1320
1347
|
? 'flex h-full min-h-0 flex-col'
|
|
1321
1348
|
: 'p-6 space-y-6 max-w-7xl', children: [(error || readOnly || hasDraft || isLocked) && (_jsxs("div", { className: PreviewComponent
|
|
1322
1349
|
? 'px-6 pt-4 space-y-3'
|
|
1323
|
-
: 'space-y-3', children: [error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), isLocked && (_jsxs("div", { className: "text-xs text-amber-900 border border-amber-300/70 bg-amber-50/70 rounded-md px-3 py-2.5 dark:text-amber-200 dark:border-amber-700/40 dark:bg-amber-950/20 flex items-start gap-2.5", children: [_jsx(Lock, { className: "h-3.5 w-3.5 mt-0.5 shrink-0 opacity-80" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "font-medium", children: [layered?.lock === 'full' && t('engine.edit.lockFull', locale), layered?.lock === 'no-overlay' && t('engine.edit.lockNoOverlay', locale), layered?.lock === 'no-delete' && t('engine.edit.lockNoDelete', locale)] }), lockReason && _jsx("div", { className: "mt-0.5 opacity-90", children: lockReason }), layered?.lockDocsUrl && (_jsxs("a", { href: layered.lockDocsUrl, target: "_blank", rel: "noopener noreferrer", className: "mt-1 inline-flex items-center gap-1 text-amber-800 underline hover:text-amber-900 dark:text-amber-200 dark:hover:text-amber-100", children: [locale === 'zh-CN' ? '查看文档' : 'View docs', " \u2192"] })), layered?.packageId && (_jsx("div", { className: "mt-0.5 text-amber-700 dark:text-amber-300/80", children: _jsxs("code", { className: "font-mono", children: [layered.packageId, layered.packageVersion ? `@${layered.packageVersion}` : ''] }) }))] }), showArtifactLockedBanner && (_jsx(Button, { size: "sm", variant: "outline", className: "shrink-0 h-7 bg-background/60", onClick: () => navigate(`../new`, { relative: 'path' }), children: t('engine.list.create', locale) }))] })), hasDraft && !createMode && (_jsxs("div", { className: "text-xs text-emerald-900 border border-emerald-300 bg-emerald-50 rounded p-3 dark:text-emerald-200 dark:border-emerald-700/50 dark:bg-emerald-950/30 flex items-center gap-3", children: [_jsx(Send, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1", children: t('engine.edit.draftPending', locale) }), canWrite && (_jsxs(_Fragment, { children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: doDiscardDraft, disabled: saving || publishing, className: "h-7", children: t('engine.edit.discardDraft', locale) }), _jsx(Button, { size: "sm", onClick: doPublish, disabled: saving || publishing || isDirty, className: "h-7 bg-emerald-600 hover:bg-emerald-700 text-emerald-50", children: publishing ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (t('engine.edit.publish', locale)) })] }))] })), readOnly && !isLocked && (_jsxs("div", { className: "text-xs text-amber-800 border border-amber-300/70 bg-amber-50/70 rounded-md px-3 py-2.5 dark:text-amber-200 dark:border-amber-700/40 dark:bg-amber-950/20 flex items-start gap-3", children: [_jsx("div", { className: "flex-1", children: showArtifactLockedBanner ? (
|
|
1350
|
+
: 'space-y-3', children: [error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), isLocked && (_jsxs("div", { className: "text-xs text-amber-900 border border-amber-300/70 bg-amber-50/70 rounded-md px-3 py-2.5 dark:text-amber-200 dark:border-amber-700/40 dark:bg-amber-950/20 flex items-start gap-2.5", children: [_jsx(Lock, { className: "h-3.5 w-3.5 mt-0.5 shrink-0 opacity-80" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "font-medium", children: [layered?.lock === 'full' && t('engine.edit.lockFull', locale), layered?.lock === 'no-overlay' && t('engine.edit.lockNoOverlay', locale), layered?.lock === 'no-delete' && t('engine.edit.lockNoDelete', locale)] }), lockReason && _jsx("div", { className: "mt-0.5 opacity-90", children: lockReason }), layered?.lockDocsUrl && (_jsxs("a", { href: layered.lockDocsUrl, target: "_blank", rel: "noopener noreferrer", className: "mt-1 inline-flex items-center gap-1 text-amber-800 underline hover:text-amber-900 dark:text-amber-200 dark:hover:text-amber-100", children: [locale === 'zh-CN' ? '查看文档' : 'View docs', " \u2192"] })), layered?.packageId && (_jsx("div", { className: "mt-0.5 text-amber-700 dark:text-amber-300/80", children: _jsxs("code", { className: "font-mono", children: [layered.packageId, layered.packageVersion ? `@${layered.packageVersion}` : ''] }) }))] }), showArtifactLockedBanner && (_jsx(Button, { size: "sm", variant: "outline", className: "shrink-0 h-7 bg-background/60", onClick: () => navigate(`../new`, { relative: 'path' }), children: t('engine.list.create', locale) }))] })), hasDraft && !createMode && (_jsxs("div", { className: "text-xs text-emerald-900 border border-emerald-300 bg-emerald-50 rounded p-3 dark:text-emerald-200 dark:border-emerald-700/50 dark:bg-emerald-950/30 flex items-center gap-3", children: [_jsx(Send, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1", children: t('engine.edit.draftPending', locale) }), canWrite && (_jsxs(_Fragment, { children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: doDiscardDraft, disabled: saving || publishing, className: "h-7", children: t('engine.edit.discardDraft', locale) }), _jsx(Button, { size: "sm", onClick: doPublish, disabled: saving || publishing || isDirty, className: "h-7 bg-emerald-600 hover:bg-emerald-700 text-emerald-50", children: publishing ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (t('engine.edit.publish', locale)) })] }))] })), readOnly && !isLocked && (_jsxs("div", { "data-testid": "readonly-banner", className: "text-xs text-amber-800 border border-amber-300/70 bg-amber-50/70 rounded-md px-3 py-2.5 dark:text-amber-200 dark:border-amber-700/40 dark:bg-amber-950/20 flex items-start gap-3", children: [_jsx("div", { className: "flex-1", children: showArtifactLockedBanner ? (
|
|
1324
1351
|
/* Type allows runtime-create but THIS item ships from
|
|
1325
1352
|
a code package. Tell the user clearly and provide
|
|
1326
1353
|
a CTA to author their own. */
|
|
@@ -1404,7 +1431,7 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
1404
1431
|
const titleKey = kind === 'error'
|
|
1405
1432
|
? 'engine.edit.diagnostics.title'
|
|
1406
1433
|
: 'engine.edit.diagnostics.warnTitle';
|
|
1407
|
-
return (_jsxs("div", { className: `flex items-start gap-2 text-xs border rounded p-2.5 ${cls}`, children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium", children: tFormat(titleKey, locale, { count: items.length }) }), _jsxs("ul", { className: "mt-1 space-y-0.5 font-mono text-[11px]", children: [head.map((e, i) => (_jsxs("li", { className: "truncate", children: [_jsx("span", { className: "opacity-70", children: e.path ? labelForIssuePath(e.path) : '(root)' }), ": ", e.message] }, i))), rest > 0 && (_jsx("li", { className: "opacity-70", children: tFormat('engine.edit.diagnostics.more', locale, { count: rest }) }))] })] })] }, kind));
|
|
1434
|
+
return (_jsxs("div", { "data-testid": kind === 'error' ? 'metadata-validation-banner' : undefined, className: `flex items-start gap-2 text-xs border rounded p-2.5 ${cls}`, children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium", children: tFormat(titleKey, locale, { count: items.length }) }), _jsxs("ul", { className: "mt-1 space-y-0.5 font-mono text-[11px]", children: [head.map((e, i) => (_jsxs("li", { className: "truncate", children: [_jsx("span", { className: "opacity-70", children: e.path ? labelForIssuePath(e.path) : '(root)' }), ": ", e.message] }, i))), rest > 0 && (_jsx("li", { className: "opacity-70", children: tFormat('engine.edit.diagnostics.more', locale, { count: rest }) }))] })] })] }, kind));
|
|
1408
1435
|
};
|
|
1409
1436
|
return (_jsxs("div", { className: "space-y-2", children: [hasErrs && renderBlock('error', errs), hasWarns && renderBlock('warning', warns)] }));
|
|
1410
1437
|
})(), PreviewComponent ? (_jsx("div", { className: isFullscreen
|
|
@@ -24,6 +24,7 @@ import { MetadataTypeActions } from './MetadataTypeActions';
|
|
|
24
24
|
import { useMetadataClient, useMetadataTypes, matchesQuery, } from './useMetadata';
|
|
25
25
|
import { getMetadataResource, resolveResourceConfig, } from './registry';
|
|
26
26
|
import { t, tFormat, translateMetadataType, detectLocale } from './i18n';
|
|
27
|
+
import { buildPackageScopeOptions, LOCAL_PACKAGE_ID } from './package-scope';
|
|
27
28
|
/**
|
|
28
29
|
* Derive provenance from item._packageId. The `loadMetaFromDb` path
|
|
29
30
|
* tags objects with the synthetic packageId 'sys_metadata' (see
|
|
@@ -82,20 +83,7 @@ function DefaultMetadataList({ type, appName }) {
|
|
|
82
83
|
const list = await client.list('package');
|
|
83
84
|
if (cancelled)
|
|
84
85
|
return;
|
|
85
|
-
|
|
86
|
-
const rows = (list ?? [])
|
|
87
|
-
.map((raw) => {
|
|
88
|
-
const item = raw && typeof raw === 'object' && 'item' in raw ? raw.item : raw;
|
|
89
|
-
const m = (item?.manifest ?? item ?? {});
|
|
90
|
-
return {
|
|
91
|
-
id: m.id,
|
|
92
|
-
scope: m.scope,
|
|
93
|
-
name: m.name || m.id,
|
|
94
|
-
};
|
|
95
|
-
})
|
|
96
|
-
.filter((p) => p.id && !SYSTEM_SCOPES.has(p.scope));
|
|
97
|
-
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
98
|
-
setProjectPackages(rows.map((p) => ({ id: p.id, name: p.name })));
|
|
86
|
+
setProjectPackages(buildPackageScopeOptions(list));
|
|
99
87
|
}
|
|
100
88
|
catch {
|
|
101
89
|
if (!cancelled)
|
|
@@ -249,8 +237,12 @@ function DefaultMetadataList({ type, appName }) {
|
|
|
249
237
|
if (!activePackage)
|
|
250
238
|
return false;
|
|
251
239
|
const pkg = row.item?._packageId;
|
|
252
|
-
const
|
|
253
|
-
|
|
240
|
+
const isLocal = !pkg || pkg === LOCAL_PACKAGE_ID;
|
|
241
|
+
// Local/Custom scope surfaces this environment's runtime-authored items
|
|
242
|
+
// (untagged / `sys_metadata` provenance); a code package shows its own.
|
|
243
|
+
if (activePackage === LOCAL_PACKAGE_ID)
|
|
244
|
+
return isLocal;
|
|
245
|
+
return !isLocal && pkg === activePackage;
|
|
254
246
|
}), [items, activePackage, config]);
|
|
255
247
|
// User-driven filters (search query + source provenance) on top of scope.
|
|
256
248
|
const filtered = scopedItems.filter((row) => {
|
|
@@ -28,6 +28,7 @@ import { useRecentItems } from '../../context/RecentItemsProvider';
|
|
|
28
28
|
import { useMetadataClient, useMetadataTypes, useGlobalDiagnostics, } from './useMetadata';
|
|
29
29
|
import { MetadataQuickFind } from './QuickFind';
|
|
30
30
|
import { translateMetadataType, translateMetadataDomain, t, tFormat, detectLocale, } from './i18n';
|
|
31
|
+
import { buildPackageScopeOptions, LOCAL_PACKAGE_ID } from './package-scope';
|
|
31
32
|
const HIDDEN_TYPES = new Set(['field', 'package']);
|
|
32
33
|
const DOMAIN_ICONS = {
|
|
33
34
|
data: Database,
|
|
@@ -104,20 +105,7 @@ export function StudioHomePage() {
|
|
|
104
105
|
const list = await client.list('package');
|
|
105
106
|
if (cancelled)
|
|
106
107
|
return;
|
|
107
|
-
|
|
108
|
-
const rows = (list ?? [])
|
|
109
|
-
.map((raw) => {
|
|
110
|
-
const item = raw && typeof raw === 'object' && 'item' in raw ? raw.item : raw;
|
|
111
|
-
const m = (item?.manifest ?? item ?? {});
|
|
112
|
-
return {
|
|
113
|
-
id: m.id,
|
|
114
|
-
scope: m.scope,
|
|
115
|
-
name: m.name || m.id,
|
|
116
|
-
};
|
|
117
|
-
})
|
|
118
|
-
.filter((p) => p.id && !SYSTEM_SCOPES.has(p.scope));
|
|
119
|
-
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
120
|
-
setProjectPackages(rows.map((p) => ({ id: p.id, name: p.name })));
|
|
108
|
+
setProjectPackages(buildPackageScopeOptions(list));
|
|
121
109
|
}
|
|
122
110
|
catch {
|
|
123
111
|
if (!cancelled)
|
|
@@ -155,6 +143,10 @@ export function StudioHomePage() {
|
|
|
155
143
|
return false;
|
|
156
144
|
if (!activePackage)
|
|
157
145
|
return false;
|
|
146
|
+
// Local/Custom scope: show every runtime-creatable type so the user can
|
|
147
|
+
// start authoring any kind of metadata here, even with zero items yet.
|
|
148
|
+
if (activePackage === LOCAL_PACKAGE_ID)
|
|
149
|
+
return e.allowOrgOverride || e.allowRuntimeCreate;
|
|
158
150
|
return (packagesByType[e.type] ?? []).includes(activePackage);
|
|
159
151
|
}), [activePackage, entries, packagesByType]);
|
|
160
152
|
const writable = React.useMemo(() => visible.filter((e) => e.allowOrgOverride || e.allowRuntimeCreate), [visible]);
|