@object-ui/app-shell 11.2.0 → 11.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +562 -0
- package/README.md +23 -0
- package/dist/console/ConsoleShell.js +17 -2
- package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
- package/dist/console/home/CloudOnboardingNext.js +14 -4
- package/dist/console/home/HomePage.js +34 -7
- package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
- package/dist/console/organizations/OrganizationsPage.js +16 -7
- package/dist/hooks/useConsoleActionRuntime.js +32 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/preview/DraftChangesPanel.d.ts +3 -1
- package/dist/preview/DraftChangesPanel.js +6 -5
- package/dist/utils/deriveRelatedLists.d.ts +20 -5
- package/dist/utils/deriveRelatedLists.js +31 -13
- package/dist/utils/index.d.ts +2 -24
- package/dist/utils/index.js +14 -101
- package/dist/utils/resolveViewId.d.ts +23 -0
- package/dist/utils/resolveViewId.js +37 -0
- package/dist/utils/warnSuppressedListNav.d.ts +10 -0
- package/dist/utils/warnSuppressedListNav.js +40 -0
- package/dist/views/DashboardView.js +2 -3
- package/dist/views/InterfaceListPage.js +10 -5
- package/dist/views/ObjectView.js +65 -12
- package/dist/views/PageView.js +2 -3
- package/dist/views/RecordDetailView.js +131 -104
- package/dist/views/RecordFormPage.js +7 -1
- package/dist/views/RelatedRecordActionsBridge.d.ts +24 -0
- package/dist/views/RelatedRecordActionsBridge.js +114 -0
- package/dist/views/ReportView.js +2 -3
- package/dist/views/metadata-admin/PackagesPage.js +18 -7
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
- package/dist/views/metadata-admin/clientValidation.js +8 -2
- package/dist/views/metadata-admin/color-variant-field.d.ts +1 -12
- package/dist/views/metadata-admin/color-variant-field.js +11 -0
- package/dist/views/metadata-admin/i18n.d.ts +12 -21
- package/dist/views/metadata-admin/i18n.js +343 -2
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +25 -11
- package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
- package/dist/views/metadata-admin/permission-slice.js +70 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.js +11 -7
- package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
- package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
- package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +1 -13
- package/dist/views/metadata-admin/previews/OutlineStrip.js +12 -0
- package/dist/views/metadata-admin/previews/PagePreview.js +9 -0
- package/dist/views/metadata-admin/previews/SourcePageEditor.d.ts +28 -0
- package/dist/views/metadata-admin/previews/SourcePageEditor.js +83 -0
- package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
- package/dist/views/studio-design/BuilderLanding.js +133 -0
- package/dist/views/studio-design/ObjectFormDesigner.d.ts +31 -0
- package/dist/views/studio-design/ObjectFormDesigner.js +226 -0
- package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectSettingsPanel.js +45 -0
- package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectValidationsPanel.js +78 -0
- package/dist/views/studio-design/StudioDesignSurface.d.ts +20 -0
- package/dist/views/studio-design/StudioDesignSurface.js +1306 -0
- package/dist/views/studio-design/metadataError.d.ts +23 -0
- package/dist/views/studio-design/metadataError.js +44 -0
- package/dist/views/studio-design/packages-io.d.ts +27 -0
- package/dist/views/studio-design/packages-io.js +61 -0
- package/package.json +46 -43
|
@@ -340,19 +340,33 @@ function useObjectOptions() {
|
|
|
340
340
|
const [opts, setOpts] = React.useState([]);
|
|
341
341
|
React.useEffect(() => {
|
|
342
342
|
let cancelled = false;
|
|
343
|
-
|
|
344
|
-
.list('object')
|
|
345
|
-
|
|
343
|
+
Promise.all([
|
|
344
|
+
client.list('object'),
|
|
345
|
+
// Draft objects are not yet published, so `list('object')` can't see
|
|
346
|
+
// them. Include them so a lookup can target a SIBLING object being
|
|
347
|
+
// designed in the same authoring pass (before the package's first
|
|
348
|
+
// publish) instead of forcing the author to type an API name blind.
|
|
349
|
+
client.listDrafts({ type: 'object' }).catch(() => []),
|
|
350
|
+
])
|
|
351
|
+
.then(([published, drafts]) => {
|
|
346
352
|
if (cancelled)
|
|
347
353
|
return;
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
const byName = new Map();
|
|
355
|
+
for (const i of published ?? []) {
|
|
356
|
+
if (typeof i?.name === 'string' && i.name && !byName.has(i.name)) {
|
|
357
|
+
byName.set(i.name, {
|
|
358
|
+
value: i.name,
|
|
359
|
+
label: i.label ? `${i.label} (${i.name})` : i.name,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
for (const d of drafts ?? []) {
|
|
364
|
+
const name = d.name;
|
|
365
|
+
if (typeof name === 'string' && name && !byName.has(name)) {
|
|
366
|
+
byName.set(name, { value: name, label: `${name} (草稿)` });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
setOpts([...byName.values()].sort((a, b) => a.value.localeCompare(b.value)));
|
|
356
370
|
})
|
|
357
371
|
.catch(() => {
|
|
358
372
|
// Empty list — picker falls back to free-text. No banner needed.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-scoped slice merge for the permission matrix (ADR-0086 P0).
|
|
3
|
+
*
|
|
4
|
+
* A Permission Set / Profile is a single metadata record whose `objects` and
|
|
5
|
+
* `fields` maps accumulate authorization rows contributed by many packages.
|
|
6
|
+
* When the Access matrix is opened inside a package context it must:
|
|
7
|
+
*
|
|
8
|
+
* 1. show only the objects that package declares (scope), and
|
|
9
|
+
* 2. on Save write back ONLY that slice — leaving every row contributed by
|
|
10
|
+
* other packages byte-for-byte intact.
|
|
11
|
+
*
|
|
12
|
+
* Overwriting the whole record (the pre-P0 behavior) silently drops the rows
|
|
13
|
+
* other packages contributed. {@link mergePermissionSlice} rebuilds the record
|
|
14
|
+
* from a freshly-read base, keeping the set-level identity and every
|
|
15
|
+
* out-of-scope row, and overlaying only the in-scope rows the user edited.
|
|
16
|
+
*/
|
|
17
|
+
export interface ObjectPerm {
|
|
18
|
+
allowCreate?: boolean;
|
|
19
|
+
allowRead?: boolean;
|
|
20
|
+
allowEdit?: boolean;
|
|
21
|
+
allowDelete?: boolean;
|
|
22
|
+
allowTransfer?: boolean;
|
|
23
|
+
allowRestore?: boolean;
|
|
24
|
+
allowPurge?: boolean;
|
|
25
|
+
viewAllRecords?: boolean;
|
|
26
|
+
modifyAllRecords?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface FieldPerm {
|
|
29
|
+
readable?: boolean;
|
|
30
|
+
editable?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface PermissionSetDraft {
|
|
33
|
+
name: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
isProfile?: boolean;
|
|
36
|
+
objects: Record<string, ObjectPerm>;
|
|
37
|
+
fields?: Record<string, FieldPerm>;
|
|
38
|
+
systemPermissions?: string[];
|
|
39
|
+
tabPermissions?: Record<string, 'visible' | 'hidden' | 'default_on' | 'default_off'>;
|
|
40
|
+
[extra: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Object name embedded in a `${object}.${field}` field-permission key. Object
|
|
44
|
+
* and field names are field-name-safe (snake_case, no dots), so the object is
|
|
45
|
+
* everything up to the first dot.
|
|
46
|
+
*/
|
|
47
|
+
export declare function fieldKeyObject(key: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Narrow a permission set down to just the rows whose object is in `scope`.
|
|
50
|
+
* Used to drive the matrix display so a package panel lists only its own
|
|
51
|
+
* objects (and their field overrides), never the whole environment.
|
|
52
|
+
*/
|
|
53
|
+
export declare function scopePermissionSet(set: Pick<PermissionSetDraft, 'objects' | 'fields'>, scope: Iterable<string>): {
|
|
54
|
+
objects: Record<string, ObjectPerm>;
|
|
55
|
+
fields: Record<string, FieldPerm>;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Merge the edited in-scope slice back onto a freshly-read full `base`.
|
|
59
|
+
*
|
|
60
|
+
* Out-of-scope rows (other packages' contributions) are copied verbatim from
|
|
61
|
+
* `base`; in-scope rows are taken entirely from `edited` (so removing a grant
|
|
62
|
+
* in the package panel deletes only that package's row). Set-level identity and
|
|
63
|
+
* any extra keys (systemPermissions, tabPermissions, …) come from `base`, with
|
|
64
|
+
* name / label / isProfile taking the user's edits.
|
|
65
|
+
*/
|
|
66
|
+
export declare function mergePermissionSlice(base: PermissionSetDraft, edited: PermissionSetDraft, scope: Iterable<string>): PermissionSetDraft;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Object name embedded in a `${object}.${field}` field-permission key. Object
|
|
4
|
+
* and field names are field-name-safe (snake_case, no dots), so the object is
|
|
5
|
+
* everything up to the first dot.
|
|
6
|
+
*/
|
|
7
|
+
export function fieldKeyObject(key) {
|
|
8
|
+
const dot = key.indexOf('.');
|
|
9
|
+
return dot === -1 ? key : key.slice(0, dot);
|
|
10
|
+
}
|
|
11
|
+
function asScopeSet(scope) {
|
|
12
|
+
return scope instanceof Set ? scope : new Set(scope);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Narrow a permission set down to just the rows whose object is in `scope`.
|
|
16
|
+
* Used to drive the matrix display so a package panel lists only its own
|
|
17
|
+
* objects (and their field overrides), never the whole environment.
|
|
18
|
+
*/
|
|
19
|
+
export function scopePermissionSet(set, scope) {
|
|
20
|
+
const scopeSet = asScopeSet(scope);
|
|
21
|
+
const objects = {};
|
|
22
|
+
for (const [k, v] of Object.entries(set.objects ?? {})) {
|
|
23
|
+
if (scopeSet.has(k))
|
|
24
|
+
objects[k] = v;
|
|
25
|
+
}
|
|
26
|
+
const fields = {};
|
|
27
|
+
for (const [k, v] of Object.entries(set.fields ?? {})) {
|
|
28
|
+
if (scopeSet.has(fieldKeyObject(k)))
|
|
29
|
+
fields[k] = v;
|
|
30
|
+
}
|
|
31
|
+
return { objects, fields };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Merge the edited in-scope slice back onto a freshly-read full `base`.
|
|
35
|
+
*
|
|
36
|
+
* Out-of-scope rows (other packages' contributions) are copied verbatim from
|
|
37
|
+
* `base`; in-scope rows are taken entirely from `edited` (so removing a grant
|
|
38
|
+
* in the package panel deletes only that package's row). Set-level identity and
|
|
39
|
+
* any extra keys (systemPermissions, tabPermissions, …) come from `base`, with
|
|
40
|
+
* name / label / isProfile taking the user's edits.
|
|
41
|
+
*/
|
|
42
|
+
export function mergePermissionSlice(base, edited, scope) {
|
|
43
|
+
const scopeSet = asScopeSet(scope);
|
|
44
|
+
const objects = {};
|
|
45
|
+
for (const [k, v] of Object.entries(base.objects ?? {})) {
|
|
46
|
+
if (!scopeSet.has(k))
|
|
47
|
+
objects[k] = v; // preserve other packages' rows
|
|
48
|
+
}
|
|
49
|
+
for (const [k, v] of Object.entries(edited.objects ?? {})) {
|
|
50
|
+
if (scopeSet.has(k))
|
|
51
|
+
objects[k] = v; // write this package's slice
|
|
52
|
+
}
|
|
53
|
+
const fields = {};
|
|
54
|
+
for (const [k, v] of Object.entries(base.fields ?? {})) {
|
|
55
|
+
if (!scopeSet.has(fieldKeyObject(k)))
|
|
56
|
+
fields[k] = v;
|
|
57
|
+
}
|
|
58
|
+
for (const [k, v] of Object.entries(edited.fields ?? {})) {
|
|
59
|
+
if (scopeSet.has(fieldKeyObject(k)))
|
|
60
|
+
fields[k] = v;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
...base,
|
|
64
|
+
name: edited.name,
|
|
65
|
+
label: edited.label,
|
|
66
|
+
isProfile: edited.isProfile,
|
|
67
|
+
objects,
|
|
68
|
+
fields,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -19,6 +19,7 @@ import * as React from 'react';
|
|
|
19
19
|
import { BarChart3, Compass, Database, FileText, Folder, GripVertical, LayoutDashboard, Link as LinkIcon, Plus, Trash2, } from 'lucide-react';
|
|
20
20
|
import { Badge, cn } from '@object-ui/components';
|
|
21
21
|
import { appendArray, moveArray, spliceArray } from '../inspectors/_shared';
|
|
22
|
+
import { t, useMetadataLocale } from '../i18n';
|
|
22
23
|
const DND_MIME = 'text/x-objectui-nav';
|
|
23
24
|
function inferKind(it) {
|
|
24
25
|
if (it.kind)
|
|
@@ -100,6 +101,7 @@ function navPath(it) {
|
|
|
100
101
|
return typeof p === 'string' && p ? p : undefined;
|
|
101
102
|
}
|
|
102
103
|
export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionChange, }) {
|
|
104
|
+
const locale = useMetadataLocale();
|
|
103
105
|
const items = React.useMemo(() => {
|
|
104
106
|
const v = draft[rootKey];
|
|
105
107
|
return Array.isArray(v) ? v : [];
|
|
@@ -114,15 +116,16 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
|
|
|
114
116
|
const addItem = React.useCallback(() => {
|
|
115
117
|
if (!onPatch)
|
|
116
118
|
return;
|
|
117
|
-
const
|
|
119
|
+
const newLabel = t('engine.appNav.newItem', locale);
|
|
120
|
+
const newItem = { label: newLabel, path: '' };
|
|
118
121
|
const next = appendArray(items, newItem);
|
|
119
122
|
setItems(next);
|
|
120
123
|
onSelectionChange?.({
|
|
121
124
|
kind: 'nav',
|
|
122
125
|
id: `${rootKey}[${next.length - 1}]`,
|
|
123
|
-
label:
|
|
126
|
+
label: newLabel,
|
|
124
127
|
});
|
|
125
|
-
}, [onPatch, items, setItems, rootKey, onSelectionChange]);
|
|
128
|
+
}, [onPatch, items, setItems, rootKey, onSelectionChange, locale]);
|
|
126
129
|
const removeItem = React.useCallback((index) => {
|
|
127
130
|
if (!onPatch)
|
|
128
131
|
return;
|
|
@@ -158,7 +161,7 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
|
|
|
158
161
|
label: navLabel(next[to] ?? {}, to),
|
|
159
162
|
});
|
|
160
163
|
}, [onPatch, items, setItems, rootKey, onSelectionChange]);
|
|
161
|
-
return (_jsxs("div", { className: "rounded-md border bg-card/40", children: [_jsxs("div", { className: "flex items-center justify-between border-b px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { className: "font-
|
|
164
|
+
return (_jsxs("div", { className: "rounded-md border bg-card/40", children: [_jsxs("div", { className: "flex items-center justify-between border-b px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { className: "font-medium uppercase tracking-wide text-muted-foreground", children: t('engine.appNav.heading', locale) }), _jsxs(Badge, { variant: "outline", className: "text-[10px]", children: [items.length, " ", items.length === 1 ? t('engine.appNav.itemOne', locale) : t('engine.appNav.itemOther', locale)] })] }), onPatch && (_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded border border-dashed px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted/30 hover:text-foreground", onClick: addItem, children: [_jsx(Plus, { className: "h-3 w-3" }), " ", t('engine.appNav.addItem', locale)] }))] }), _jsx("div", { className: "space-y-1.5 p-2", onDragOver: (e) => {
|
|
162
165
|
if (!onPatch)
|
|
163
166
|
return;
|
|
164
167
|
if (!e.dataTransfer.types.includes(DND_MIME))
|
|
@@ -176,8 +179,8 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
|
|
|
176
179
|
moveItem(dragIndex, items.length);
|
|
177
180
|
setDragIndex(null);
|
|
178
181
|
}, children: items.length === 0 ? (_jsx("div", { className: "rounded border border-dashed px-3 py-4 text-center text-[11px] text-muted-foreground", children: onPatch
|
|
179
|
-
? '
|
|
180
|
-
: '
|
|
182
|
+
? t('engine.appNav.empty', locale)
|
|
183
|
+
: t('engine.appNav.emptyReadonly', locale) })) : (items.map((it, i) => (_jsx(NavCardTree, { item: it, index: i, depth: 0, path: `${rootKey}[${i}]`, selectedId: selectedId, canEdit: !!onPatch, onClick: (p, lbl) => onSelectionChange?.({ kind: 'nav', id: p, label: lbl }), onRename: (lbl) => renameItem(i, lbl), onRemove: () => removeItem(i), onDragStart: () => setDragIndex(i), onDragEnd: () => setDragIndex(null), onDropBefore: () => {
|
|
181
184
|
if (dragIndex == null)
|
|
182
185
|
return;
|
|
183
186
|
moveItem(dragIndex, i);
|
|
@@ -192,6 +195,7 @@ function NavCardTree({ item, index, depth, path, selectedId, canEdit, onClick, o
|
|
|
192
195
|
})] }));
|
|
193
196
|
}
|
|
194
197
|
function NavCard({ item, index, depth, path, isSelected, canEdit, onClick, onRename, onRemove, onDragStart, onDragEnd, onDropBefore, }) {
|
|
198
|
+
const locale = useMetadataLocale();
|
|
195
199
|
const kind = inferKind(item);
|
|
196
200
|
const Icon = kindIcon(kind);
|
|
197
201
|
const tone = kindTone(kind);
|
|
@@ -256,5 +260,5 @@ function NavCard({ item, index, depth, path, isSelected, canEdit, onClick, onRen
|
|
|
256
260
|
e.stopPropagation();
|
|
257
261
|
onRemove();
|
|
258
262
|
}
|
|
259
|
-
}, className: "inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive", "aria-label":
|
|
263
|
+
}, className: "inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive", "aria-label": t('engine.appNav.removeItem', locale), children: _jsx(Trash2, { className: "h-3 w-3" }) }))] })] }));
|
|
260
264
|
}
|
|
@@ -10,16 +10,20 @@
|
|
|
10
10
|
* designer.
|
|
11
11
|
*/
|
|
12
12
|
import * as React from 'react';
|
|
13
|
+
/** An error on a run/step. The engine sends the run-level `error` as a plain
|
|
14
|
+
* string (`ExecutionLog.error`) while a step-level error is a `{code,message}`
|
|
15
|
+
* object — the panel accepts either shape. */
|
|
16
|
+
type RunError = string | {
|
|
17
|
+
code?: string;
|
|
18
|
+
message?: string;
|
|
19
|
+
};
|
|
13
20
|
/** Step entry of a run log (spec `ExecutionStepLogSchema`, fields we render). */
|
|
14
21
|
interface RunStep {
|
|
15
22
|
nodeId: string;
|
|
16
23
|
nodeType?: string;
|
|
17
24
|
status: 'success' | 'failure' | 'skipped' | string;
|
|
18
25
|
durationMs?: number;
|
|
19
|
-
error?:
|
|
20
|
-
code?: string;
|
|
21
|
-
message?: string;
|
|
22
|
-
};
|
|
26
|
+
error?: RunError;
|
|
23
27
|
}
|
|
24
28
|
/** Run log entry (spec `ExecutionLogSchema`, fields we render). */
|
|
25
29
|
export interface FlowRun {
|
|
@@ -34,10 +38,15 @@ export interface FlowRun {
|
|
|
34
38
|
object?: string;
|
|
35
39
|
};
|
|
36
40
|
steps?: RunStep[];
|
|
37
|
-
error?:
|
|
38
|
-
message?: string;
|
|
39
|
-
};
|
|
41
|
+
error?: RunError;
|
|
40
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Normalize a run/step error to its human-readable message. The engine emits a
|
|
45
|
+
* run-level `error` as a plain string but a step-level error as `{code,message}`
|
|
46
|
+
* — reading `.message` off the string case silently dropped the run failure
|
|
47
|
+
* reason (the whole point of the Runs panel for a failed run), so accept both.
|
|
48
|
+
*/
|
|
49
|
+
export declare function errorText(e: RunError | undefined | null): string | undefined;
|
|
41
50
|
/** Fetch a flow's run history. Exposed for tests. */
|
|
42
51
|
export declare function fetchFlowRuns(flowName: string, signal?: AbortSignal): Promise<FlowRun[] | null>;
|
|
43
52
|
export declare function FlowRunsPanel({ flowName }: {
|
|
@@ -15,6 +15,20 @@ import * as React from 'react';
|
|
|
15
15
|
import { AlertCircle, CheckCircle2, ChevronDown, ChevronRight, Clock, Loader2, PauseCircle, RefreshCw, SkipForward } from 'lucide-react';
|
|
16
16
|
import { cn } from '@object-ui/components';
|
|
17
17
|
import { apiBase } from './useFlowNodePalette';
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a run/step error to its human-readable message. The engine emits a
|
|
20
|
+
* run-level `error` as a plain string but a step-level error as `{code,message}`
|
|
21
|
+
* — reading `.message` off the string case silently dropped the run failure
|
|
22
|
+
* reason (the whole point of the Runs panel for a failed run), so accept both.
|
|
23
|
+
*/
|
|
24
|
+
export function errorText(e) {
|
|
25
|
+
if (!e)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (typeof e === 'string')
|
|
28
|
+
return e || undefined;
|
|
29
|
+
const m = e.message;
|
|
30
|
+
return typeof m === 'string' && m ? m : undefined;
|
|
31
|
+
}
|
|
18
32
|
/** Fetch a flow's run history. Exposed for tests. */
|
|
19
33
|
export async function fetchFlowRuns(flowName, signal) {
|
|
20
34
|
try {
|
|
@@ -60,14 +74,16 @@ function StepRow({ step }) {
|
|
|
60
74
|
: step.status === 'failure'
|
|
61
75
|
? 'text-rose-600 dark:text-rose-400'
|
|
62
76
|
: 'text-muted-foreground';
|
|
63
|
-
|
|
77
|
+
const stepErr = errorText(step.error);
|
|
78
|
+
return (_jsxs("li", { className: "flex items-baseline gap-1.5 py-0.5", children: [_jsx("span", { className: cn('shrink-0 text-[9px] font-semibold uppercase', cls), children: step.status }), _jsx("span", { className: "truncate font-mono text-[10px]", title: step.nodeId, children: step.nodeId }), step.nodeType && _jsx("span", { className: "shrink-0 text-[9px] uppercase text-muted-foreground", children: step.nodeType }), fmtDuration(step.durationMs) && (_jsx("span", { className: "ml-auto shrink-0 text-[9px] text-muted-foreground", children: fmtDuration(step.durationMs) })), stepErr && (_jsx("span", { className: "min-w-0 truncate text-[9px] text-rose-600", title: stepErr, children: stepErr }))] }));
|
|
64
79
|
}
|
|
65
80
|
function RunRow({ run }) {
|
|
66
81
|
const [open, setOpen] = React.useState(false);
|
|
67
82
|
const meta = statusMeta(run.status);
|
|
68
83
|
const Icon = meta.icon;
|
|
69
84
|
const steps = Array.isArray(run.steps) ? run.steps : [];
|
|
70
|
-
|
|
85
|
+
const runErr = errorText(run.error);
|
|
86
|
+
return (_jsxs("li", { className: "rounded border bg-background", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "flex w-full items-center gap-1.5 p-1.5 text-left", "aria-expanded": open, children: [open ? (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-3 w-3 shrink-0 text-muted-foreground" })), _jsx(Icon, { className: cn('h-3.5 w-3.5 shrink-0', meta.cls, run.status === 'running' && 'animate-spin') }), _jsx("span", { className: cn('shrink-0 text-[10px] font-semibold', meta.cls), children: meta.label }), _jsx("span", { className: "min-w-0 truncate text-[10px] text-muted-foreground", title: run.id, children: fmtTime(run.startedAt) }), fmtDuration(run.durationMs) && (_jsx("span", { className: "ml-auto shrink-0 text-[10px] text-muted-foreground", children: fmtDuration(run.durationMs) }))] }), open && (_jsxs("div", { className: "border-t px-2 py-1.5", children: [_jsxs("div", { className: "pb-1 font-mono text-[9px] text-muted-foreground", title: run.id, children: ["run ", run.id, run.trigger?.type && ` · trigger ${run.trigger.type}`] }), runErr && (_jsx("div", { className: "pb-1 text-[10px] text-rose-600", children: runErr })), steps.length === 0 ? (_jsx("div", { className: "text-[10px] italic text-muted-foreground", children: "No step log recorded." })) : (_jsx("ul", { className: "divide-y divide-border/50", children: steps.map((s, i) => (_jsx(StepRow, { step: s }, `${s.nodeId}#${i}`))) }))] }))] }));
|
|
71
87
|
}
|
|
72
88
|
export function FlowRunsPanel({ flowName }) {
|
|
73
89
|
const [runs, setRuns] = React.useState([]);
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OutlineStrip — clickable chip strip that lets users select
|
|
3
|
-
* sub-elements inside a preview whose main canvas renders through a
|
|
4
|
-
* sealed external renderer (SchemaRenderer, ReportRenderer, …) and
|
|
5
|
-
* therefore can't intercept clicks directly.
|
|
6
|
-
*
|
|
7
|
-
* The strip sits above the canvas. Each chip emits a selection on
|
|
8
|
-
* click; the currently-selected one gets a ring. Empty list collapses
|
|
9
|
-
* the strip entirely so the canvas takes the full preview height
|
|
10
|
-
* outside design mode.
|
|
11
|
-
*/
|
|
12
|
-
import * as React from 'react';
|
|
13
1
|
export interface OutlineEntry {
|
|
14
2
|
/** Selection id to emit. */
|
|
15
3
|
id: string;
|
|
@@ -29,4 +17,4 @@ export declare function OutlineStrip({ title, entries, selectedId, onSelect, onA
|
|
|
29
17
|
onAdd?: () => void;
|
|
30
18
|
/** Tooltip / aria-label for the Add chip. Defaults to "Add". */
|
|
31
19
|
addLabel?: string;
|
|
32
|
-
}):
|
|
20
|
+
}): import("react").JSX.Element | null;
|
|
@@ -1,4 +1,16 @@
|
|
|
1
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
|
+
* OutlineStrip — clickable chip strip that lets users select
|
|
5
|
+
* sub-elements inside a preview whose main canvas renders through a
|
|
6
|
+
* sealed external renderer (SchemaRenderer, ReportRenderer, …) and
|
|
7
|
+
* therefore can't intercept clicks directly.
|
|
8
|
+
*
|
|
9
|
+
* The strip sits above the canvas. Each chip emits a selection on
|
|
10
|
+
* click; the currently-selected one gets a ring. Empty list collapses
|
|
11
|
+
* the strip entirely so the canvas takes the full preview height
|
|
12
|
+
* outside design mode.
|
|
13
|
+
*/
|
|
2
14
|
import { Plus } from 'lucide-react';
|
|
3
15
|
import { cn } from '@object-ui/components';
|
|
4
16
|
export function OutlineStrip({ title, entries, selectedId, onSelect, onAdd, addLabel, }) {
|
|
@@ -14,6 +14,7 @@ import { buildExpandFields } from '@object-ui/core';
|
|
|
14
14
|
import { buildDefaultPageSchema } from '@object-ui/plugin-detail';
|
|
15
15
|
import { PreviewShell, PreviewErrorBoundary, PreviewMessage } from './PreviewShell';
|
|
16
16
|
import { OutlineStrip } from './OutlineStrip';
|
|
17
|
+
import { SourcePageEditor } from './SourcePageEditor';
|
|
17
18
|
import { PageBlockCanvas } from './PageBlockCanvas';
|
|
18
19
|
import { InterfaceListPage } from '../../InterfaceListPage';
|
|
19
20
|
import { t as tr } from '../i18n';
|
|
@@ -205,6 +206,14 @@ export function PagePreview({ draft, editing, selection, onSelectionChange, onPa
|
|
|
205
206
|
if (isInterfacePage) {
|
|
206
207
|
return (_jsx(PreviewShell, { hint: "page \u00B7 interface", children: _jsx(PreviewErrorBoundary, { fallbackHint: "The interface page references a source object/view that isn't available.", children: _jsx(InterfaceListPage, { page: draft, onConfigChange: canEdit ? (patch) => onPatch({ interfaceConfig: { ...((draft.interfaceConfig) || {}), ...patch } }) : undefined }) }) }));
|
|
207
208
|
}
|
|
209
|
+
// Source page (ADR-0080/0081) → `kind:'html'`/`'react'` pages are a `source`
|
|
210
|
+
// string, not a region tree. The design canvas does not apply (and would
|
|
211
|
+
// choke on the absent regions/children), so edit the source directly with a
|
|
212
|
+
// live preview. This is the fix for the editor crashing on these pages.
|
|
213
|
+
const pageKind = draft.kind;
|
|
214
|
+
if (pageKind === 'react' || pageKind === 'html') {
|
|
215
|
+
return (_jsx(SourcePageEditor, { draft: draft, onPatch: canEdit ? onPatch : undefined, readOnly: !canEdit }));
|
|
216
|
+
}
|
|
208
217
|
// Empty draft → no preview; but if we're in design mode show the
|
|
209
218
|
// canvas so users can author from scratch.
|
|
210
219
|
if (!schema || Object.keys(schema).length <= 1) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*
|
|
8
|
+
* SourcePageEditor — the Studio editor surface for `kind:'html'` and
|
|
9
|
+
* `kind:'react'` pages (ADR-0080/0081). These pages ARE a `source` string
|
|
10
|
+
* (JSX/HTML or real React), not a region tree — so the structured design
|
|
11
|
+
* canvas does not apply (and would choke on the missing `regions`/`children`).
|
|
12
|
+
* Instead we present a code editor for the `source` field beside a live preview
|
|
13
|
+
* rendered through the runtime SchemaRenderer. Edits patch `draft.source`.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors JsonSourceEditor's Monaco + textarea-fallback + theme handling, but
|
|
16
|
+
* edits ONE field (the source) instead of the whole-record JSON, with a
|
|
17
|
+
* JSX/TSX language mode.
|
|
18
|
+
*/
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
export interface SourcePageEditorProps {
|
|
21
|
+
draft: Record<string, unknown>;
|
|
22
|
+
/** Patch the draft; undefined in read-only mode. */
|
|
23
|
+
onPatch?: (patch: Record<string, unknown>) => void;
|
|
24
|
+
readOnly?: boolean;
|
|
25
|
+
fallbackDelayMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function SourcePageEditor({ draft, onPatch, readOnly, fallbackDelayMs }: SourcePageEditorProps): React.JSX.Element;
|
|
28
|
+
export default SourcePageEditor;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ObjectUI
|
|
4
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* SourcePageEditor — the Studio editor surface for `kind:'html'` and
|
|
10
|
+
* `kind:'react'` pages (ADR-0080/0081). These pages ARE a `source` string
|
|
11
|
+
* (JSX/HTML or real React), not a region tree — so the structured design
|
|
12
|
+
* canvas does not apply (and would choke on the missing `regions`/`children`).
|
|
13
|
+
* Instead we present a code editor for the `source` field beside a live preview
|
|
14
|
+
* rendered through the runtime SchemaRenderer. Edits patch `draft.source`.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors JsonSourceEditor's Monaco + textarea-fallback + theme handling, but
|
|
17
|
+
* edits ONE field (the source) instead of the whole-record JSON, with a
|
|
18
|
+
* JSX/TSX language mode.
|
|
19
|
+
*/
|
|
20
|
+
import * as React from 'react';
|
|
21
|
+
import { Skeleton } from '@object-ui/components';
|
|
22
|
+
import { SchemaRenderer } from '@object-ui/react';
|
|
23
|
+
import { PreviewShell, PreviewErrorBoundary } from './PreviewShell';
|
|
24
|
+
const LazyMonaco = React.lazy(async () => {
|
|
25
|
+
const mod = await import('@monaco-editor/react');
|
|
26
|
+
return { default: mod.default };
|
|
27
|
+
});
|
|
28
|
+
export function SourcePageEditor({ draft, onPatch, readOnly, fallbackDelayMs = 4000 }) {
|
|
29
|
+
const kind = draft.kind === 'react' ? 'react' : 'html';
|
|
30
|
+
const source = typeof draft.source === 'string' ? draft.source : '';
|
|
31
|
+
const [text, setText] = React.useState(source);
|
|
32
|
+
const lastCommittedRef = React.useRef(source);
|
|
33
|
+
const containerRef = React.useRef(null);
|
|
34
|
+
// Sync from upstream (Reset / inspector edits) without clobbering keystrokes.
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
if (source !== lastCommittedRef.current) {
|
|
37
|
+
setText(source);
|
|
38
|
+
lastCommittedRef.current = source;
|
|
39
|
+
}
|
|
40
|
+
}, [source]);
|
|
41
|
+
// Monaco-unavailable fallback (headless / CSP) → plain textarea.
|
|
42
|
+
const [monacoUnavailable, setMonacoUnavailable] = React.useState(false);
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
if (monacoUnavailable)
|
|
45
|
+
return;
|
|
46
|
+
const id = setTimeout(() => {
|
|
47
|
+
const el = containerRef.current;
|
|
48
|
+
if (!el || !el.querySelector('.view-line'))
|
|
49
|
+
setMonacoUnavailable(true);
|
|
50
|
+
}, fallbackDelayMs);
|
|
51
|
+
return () => clearTimeout(id);
|
|
52
|
+
}, [monacoUnavailable, fallbackDelayMs]);
|
|
53
|
+
const [theme, setTheme] = React.useState(() => typeof document !== 'undefined' && document.documentElement.classList.contains('dark') ? 'vs-dark' : 'light');
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
if (typeof document === 'undefined')
|
|
56
|
+
return;
|
|
57
|
+
const root = document.documentElement;
|
|
58
|
+
const update = () => setTheme(root.classList.contains('dark') ? 'vs-dark' : 'light');
|
|
59
|
+
const obs = new MutationObserver(update);
|
|
60
|
+
obs.observe(root, { attributes: true, attributeFilter: ['class'] });
|
|
61
|
+
return () => obs.disconnect();
|
|
62
|
+
}, []);
|
|
63
|
+
const handleChange = (next) => {
|
|
64
|
+
const v = next ?? '';
|
|
65
|
+
setText(v);
|
|
66
|
+
lastCommittedRef.current = v;
|
|
67
|
+
onPatch?.({ source: v });
|
|
68
|
+
};
|
|
69
|
+
const previewSchema = React.useMemo(() => ({ ...draft, type: draft.type ?? 'page' }), [draft]);
|
|
70
|
+
return (_jsx(PreviewShell, { hint: `page · ${kind} source`, children: _jsxs("div", { className: "grid h-full grid-cols-1 divide-y divide-border lg:grid-cols-2 lg:divide-x lg:divide-y-0", children: [_jsx("div", { ref: containerRef, className: "h-full min-h-[260px] overflow-hidden bg-background", children: monacoUnavailable ? (_jsx("textarea", { value: text, onChange: (e) => handleChange(e.target.value), readOnly: readOnly, spellCheck: false, "aria-label": "Page source", className: "h-full w-full resize-none bg-background p-3 font-mono text-xs leading-relaxed outline-none" })) : (_jsx(React.Suspense, { fallback: _jsx(Skeleton, { className: "h-full w-full" }), children: _jsx(LazyMonaco, { value: text, language: "typescript", path: kind === 'react' ? 'page.tsx' : 'page.html.tsx', theme: theme, onChange: handleChange, options: {
|
|
71
|
+
readOnly,
|
|
72
|
+
minimap: { enabled: false },
|
|
73
|
+
fontSize: 12,
|
|
74
|
+
lineNumbers: 'on',
|
|
75
|
+
scrollBeyondLastLine: false,
|
|
76
|
+
automaticLayout: true,
|
|
77
|
+
folding: true,
|
|
78
|
+
wordWrap: 'on',
|
|
79
|
+
tabSize: 2,
|
|
80
|
+
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
|
|
81
|
+
} }) })) }), _jsx("div", { className: "h-full min-h-[260px] overflow-auto bg-muted/20", children: _jsx(PreviewErrorBoundary, { fallbackHint: "The page source threw while rendering \u2014 fix the code on the left.", children: _jsx(SchemaRenderer, { schema: previewSchema }) }) })] }) }));
|
|
82
|
+
}
|
|
83
|
+
export default SourcePageEditor;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BuilderLanding — the application builder's front door.
|
|
3
|
+
*
|
|
4
|
+
* The journey from login: Home → Studio app → 「应用构建」 (this page, embedded
|
|
5
|
+
* in the app chrome via the `studio:builder` component ref) → pick or create a
|
|
6
|
+
* writable base package → the full-screen pillar builder
|
|
7
|
+
* (`/studio/:packageId/:tab`). Also served standalone at bare `/studio` so the
|
|
8
|
+
* builder is bookmarkable.
|
|
9
|
+
*
|
|
10
|
+
* Writable bases (where authoring happens) lead; read-only code packages are
|
|
11
|
+
* listed secondary for browsing. Writability is the shared display heuristic
|
|
12
|
+
* from packages-io — the ADR-0070 D4 gate stays the server-side authority.
|
|
13
|
+
*/
|
|
14
|
+
import * as React from 'react';
|
|
15
|
+
export declare function BuilderLanding(): React.ReactElement;
|