@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/dist/console/AppContent.js +14 -2
  3. package/dist/console/ai/AiChatPage.js +11 -7
  4. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  5. package/dist/console/ai/LiveCanvas.js +6 -4
  6. package/dist/hooks/useChatConversation.d.ts +30 -0
  7. package/dist/hooks/useChatConversation.js +63 -0
  8. package/dist/hooks/useConsoleActionRuntime.js +6 -2
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +5 -1
  11. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  12. package/dist/layout/ConsoleFloatingChatbot.js +25 -8
  13. package/dist/layout/ContextSelectors.js +59 -35
  14. package/dist/layout/agentPicker.d.ts +56 -0
  15. package/dist/layout/agentPicker.js +40 -0
  16. package/dist/preview/CommitTimeline.d.ts +15 -0
  17. package/dist/preview/CommitTimeline.js +82 -0
  18. package/dist/preview/UnpublishedAppBar.js +11 -7
  19. package/dist/preview/commitHistory.d.ts +28 -0
  20. package/dist/preview/commitHistory.js +48 -0
  21. package/dist/providers/MetadataProvider.js +9 -0
  22. package/dist/views/FlowRunner.d.ts +2 -30
  23. package/dist/views/FlowRunner.js +18 -50
  24. package/dist/views/ScreenView.d.ts +70 -0
  25. package/dist/views/ScreenView.js +73 -0
  26. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  27. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  28. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  29. package/dist/views/metadata-admin/PackagesPage.js +9 -1
  30. package/dist/views/metadata-admin/ResourceEditPage.js +47 -20
  31. package/dist/views/metadata-admin/ResourceListPage.js +8 -16
  32. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  33. package/dist/views/metadata-admin/anchors.js +20 -2
  34. package/dist/views/metadata-admin/i18n.js +88 -2
  35. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +2 -2
  36. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +122 -8
  37. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +84 -3
  38. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +67 -2
  39. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  40. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  41. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  42. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  43. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  44. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  45. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  46. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +97 -0
  47. package/dist/views/metadata-admin/inspectors/flow-node-config.js +46 -1
  48. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  49. package/dist/views/metadata-admin/issuePath.js +65 -0
  50. package/dist/views/metadata-admin/package-scope.d.ts +26 -0
  51. package/dist/views/metadata-admin/package-scope.js +43 -0
  52. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  53. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +7 -1
  54. package/dist/views/metadata-admin/previews/FlowCanvas.js +104 -16
  55. package/dist/views/metadata-admin/previews/FlowPreview.js +31 -3
  56. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  57. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  58. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  59. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  60. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  61. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  62. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
  63. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +21 -6
  64. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  65. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  66. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  67. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  68. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +11 -0
  69. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  70. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +72 -0
  71. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  72. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  73. package/package.json +38 -38
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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, Input, Label, Textarea, Checkbox, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@object-ui/components';
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
- function initialValues(screen) {
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(initialValues(state.screen));
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(initialValues(data.screen));
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.kind === 'object-form' && !!screen.objectName;
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 })] }), isObjectForm ? (_jsx("div", { className: "py-2", children: dataSource ? (_jsx(ObjectForm, { schema: {
123
- type: 'object-form',
124
- formType: 'simple',
125
- objectName: screen.objectName,
126
- mode: screen.mode === 'edit' ? 'edit' : 'create',
127
- recordId: screen.mode === 'edit' ? screen.recordId : undefined,
128
- ...(screen.defaults ? { initialValues: screen.defaults } : {}),
129
- layout: 'vertical',
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
- const SYSTEM_SCOPES = new Set(['system', 'cloud']);
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
- const isArtifact = layered?.code != null;
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
- const formForLabels = (createMode && config.createSchema ? undefined : entry?.form);
819
- const sections = Array.isArray(formForLabels?.sections) ? formForLabels.sections : [];
820
- for (const section of sections) {
821
- const fields = Array.isArray(section?.fields) ? section.fields : [];
822
- for (const field of fields) {
823
- if (typeof field === 'string') {
824
- if (field === key)
825
- return field;
826
- }
827
- else if (field?.field === key) {
828
- return String(field.label ?? key);
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
- const props = (schema?.properties ?? {});
833
- return String(props[key]?.title ?? key);
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
- navigate(`../${encodeURIComponent(savedName)}`, { relative: 'path' });
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
- const isArtifactItem = !createMode && layered?.code != null;
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
- const SYSTEM_SCOPES = new Set(['system', 'cloud']);
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 effectivePkg = !pkg || pkg === 'sys_metadata' ? null : pkg;
253
- return effectivePkg === activePackage;
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
- const SYSTEM_SCOPES = new Set(['system', 'cloud']);
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]);