@object-ui/app-shell 11.3.0 → 11.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +700 -0
- package/README.md +32 -0
- package/dist/chrome/KeyboardShortcutsDialog.js +2 -1
- package/dist/console/AppContent.js +145 -26
- package/dist/console/ConsoleShell.js +25 -3
- 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/context/CommandPaletteProvider.js +2 -1
- package/dist/hooks/useConsoleActionRuntime.js +32 -3
- package/dist/hooks/useObjectActions.js +16 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/layout/AppHeader.js +13 -5
- package/dist/layout/AppSidebar.js +10 -4
- package/dist/observability/sentry.d.ts +5 -0
- package/dist/observability/sentry.js +6 -1
- package/dist/preview/DraftChangesPanel.d.ts +31 -1
- package/dist/preview/DraftChangesPanel.js +146 -18
- package/dist/urlParams.d.ts +68 -0
- package/dist/urlParams.js +76 -0
- package/dist/utils/appRoute.d.ts +15 -0
- package/dist/utils/appRoute.js +22 -0
- package/dist/utils/deriveRelatedLists.d.ts +20 -5
- package/dist/utils/deriveRelatedLists.js +31 -13
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/pageTabsUrlSync.d.ts +32 -0
- package/dist/utils/pageTabsUrlSync.js +43 -0
- package/dist/utils/recordFormNavigation.d.ts +40 -0
- package/dist/utils/recordFormNavigation.js +30 -0
- package/dist/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/InterfaceListPage.d.ts +1 -0
- package/dist/views/InterfaceListPage.js +7 -5
- package/dist/views/ObjectDataPage.d.ts +29 -0
- package/dist/views/ObjectDataPage.js +227 -0
- package/dist/views/ObjectView.js +65 -13
- package/dist/views/RecordDetailView.js +191 -123
- package/dist/views/RecordFormPage.js +7 -1
- package/dist/views/RelatedRecordActionsBridge.d.ts +33 -0
- package/dist/views/RelatedRecordActionsBridge.js +147 -0
- 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/ResourceEditPage.js +39 -0
- package/dist/views/metadata-admin/i18n.d.ts +12 -21
- package/dist/views/metadata-admin/i18n.js +557 -6
- package/dist/views/metadata-admin/inspectors/AppNavInspector.d.ts +11 -4
- package/dist/views/metadata-admin/inspectors/AppNavInspector.js +141 -7
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.d.ts +14 -0
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.js +76 -5
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +60 -30
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +8 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +3 -2
- package/dist/views/metadata-admin/inspectors/nav-target.d.ts +52 -0
- package/dist/views/metadata-admin/inspectors/nav-target.js +149 -0
- package/dist/views/metadata-admin/nav-selection.d.ts +20 -0
- package/dist/views/metadata-admin/nav-selection.js +81 -0
- package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
- package/dist/views/metadata-admin/permission-slice.js +70 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.js +19 -7
- package/dist/views/metadata-admin/previews/AppPreview.js +4 -2
- package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
- package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
- package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
- package/dist/views/studio-design/BuilderLanding.js +126 -0
- package/dist/views/studio-design/ObjectFormDesigner.d.ts +33 -0
- package/dist/views/studio-design/ObjectFormDesigner.js +231 -0
- package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectSettingsPanel.js +46 -0
- package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectValidationsPanel.js +80 -0
- package/dist/views/studio-design/PackageIdInput.d.ts +31 -0
- package/dist/views/studio-design/PackageIdInput.js +40 -0
- package/dist/views/studio-design/StudioDesignSurface.d.ts +13 -0
- package/dist/views/studio-design/StudioDesignSurface.js +962 -145
- package/dist/views/studio-design/metadataError.d.ts +23 -0
- package/dist/views/studio-design/metadataError.js +44 -0
- package/dist/views/studio-design/packageSurfaces.d.ts +49 -0
- package/dist/views/studio-design/packageSurfaces.js +34 -0
- package/dist/views/studio-design/packages-io.d.ts +38 -0
- package/dist/views/studio-design/packages-io.js +73 -0
- package/dist/views/studio-design/skeletons.d.ts +16 -0
- package/dist/views/studio-design/skeletons.js +51 -0
- package/package.json +42 -39
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nav-item target resolution (#2245) — the pure logic behind
|
|
3
|
+
* AppNavInspector's type + per-type target editing.
|
|
4
|
+
*
|
|
5
|
+
* The nav contract is a discriminated union on `type`; each type carries its
|
|
6
|
+
* own typed target field. `object` items additionally have FOUR mutually
|
|
7
|
+
* exclusive landing modes matching resolveHref's precedence
|
|
8
|
+
* (`recordId` → `filters` → `viewName` → bare default). The mode is NEVER
|
|
9
|
+
* persisted — it is derived from which fields are present — and switching
|
|
10
|
+
* type/mode explicitly clears the other target fields plus the legacy
|
|
11
|
+
* off-spec keys (`path` / `kind` / aliases), so every edit normalizes the
|
|
12
|
+
* item to spec shape ("edit is the migration").
|
|
13
|
+
*/
|
|
14
|
+
/** Nav item types the inspector offers (spec union minus separator/action). */
|
|
15
|
+
export declare const NAV_ITEM_TYPES: readonly ["object", "page", "dashboard", "report", "url", "group"];
|
|
16
|
+
export type NavItemType = (typeof NAV_ITEM_TYPES)[number];
|
|
17
|
+
/** Landing modes for `type: 'object'`, in resolveHref precedence order. */
|
|
18
|
+
export declare const OBJECT_TARGET_MODES: readonly ["default", "view", "record", "filters"];
|
|
19
|
+
export type ObjectTargetMode = (typeof OBJECT_TARGET_MODES)[number];
|
|
20
|
+
/**
|
|
21
|
+
* Per-type target descriptor: which field the picker writes and which
|
|
22
|
+
* metadata list feeds its options (`client.list(metaType)`); free-text
|
|
23
|
+
* types have no metaType.
|
|
24
|
+
*/
|
|
25
|
+
export declare const NAV_TYPE_TARGETS: Record<NavItemType, {
|
|
26
|
+
targetKey?: string;
|
|
27
|
+
metaType?: string;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Infer the effective type of a (possibly legacy) nav node for display:
|
|
31
|
+
* spec `type` wins; else legacy `kind`; else the presence of typed or
|
|
32
|
+
* legacy target fields; else children ⇒ group; else null (unset).
|
|
33
|
+
*/
|
|
34
|
+
export declare function inferNavItemType(node: Record<string, unknown>): NavItemType | null;
|
|
35
|
+
/**
|
|
36
|
+
* Derive an object item's landing mode from field presence, following
|
|
37
|
+
* resolveHref's precedence — the mode is a projection, never stored.
|
|
38
|
+
*/
|
|
39
|
+
export declare function deriveObjectTargetMode(node: Record<string, unknown>): ObjectTargetMode;
|
|
40
|
+
/**
|
|
41
|
+
* The patch that clears everything EXCEPT the fields the given type+mode
|
|
42
|
+
* legitimately owns. Always includes the legacy keys. Spread this before
|
|
43
|
+
* the fields being set so a type/mode switch leaves no stale target behind.
|
|
44
|
+
*/
|
|
45
|
+
export declare function clearedTargetPatch(keep?: ReadonlyArray<string>): Record<string, undefined>;
|
|
46
|
+
/** Fields each object landing mode owns (besides objectName). */
|
|
47
|
+
export declare const OBJECT_MODE_FIELDS: Record<ObjectTargetMode, ReadonlyArray<string>>;
|
|
48
|
+
/**
|
|
49
|
+
* Ensure a spec-valid snake_case `id`. Existing ids are kept; otherwise one
|
|
50
|
+
* is derived from the target/label and uniqued against sibling ids.
|
|
51
|
+
*/
|
|
52
|
+
export declare function ensureNavId(node: Record<string, unknown>, siblings: ReadonlyArray<Record<string, unknown>>, seed?: string): string;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Nav-item target resolution (#2245) — the pure logic behind
|
|
4
|
+
* AppNavInspector's type + per-type target editing.
|
|
5
|
+
*
|
|
6
|
+
* The nav contract is a discriminated union on `type`; each type carries its
|
|
7
|
+
* own typed target field. `object` items additionally have FOUR mutually
|
|
8
|
+
* exclusive landing modes matching resolveHref's precedence
|
|
9
|
+
* (`recordId` → `filters` → `viewName` → bare default). The mode is NEVER
|
|
10
|
+
* persisted — it is derived from which fields are present — and switching
|
|
11
|
+
* type/mode explicitly clears the other target fields plus the legacy
|
|
12
|
+
* off-spec keys (`path` / `kind` / aliases), so every edit normalizes the
|
|
13
|
+
* item to spec shape ("edit is the migration").
|
|
14
|
+
*/
|
|
15
|
+
/** Nav item types the inspector offers (spec union minus separator/action). */
|
|
16
|
+
export const NAV_ITEM_TYPES = ['object', 'page', 'dashboard', 'report', 'url', 'group'];
|
|
17
|
+
/** Landing modes for `type: 'object'`, in resolveHref precedence order. */
|
|
18
|
+
export const OBJECT_TARGET_MODES = ['default', 'view', 'record', 'filters'];
|
|
19
|
+
/**
|
|
20
|
+
* Per-type target descriptor: which field the picker writes and which
|
|
21
|
+
* metadata list feeds its options (`client.list(metaType)`); free-text
|
|
22
|
+
* types have no metaType.
|
|
23
|
+
*/
|
|
24
|
+
export const NAV_TYPE_TARGETS = {
|
|
25
|
+
object: { targetKey: 'objectName', metaType: 'object' },
|
|
26
|
+
page: { targetKey: 'pageName', metaType: 'page' },
|
|
27
|
+
dashboard: { targetKey: 'dashboardName', metaType: 'dashboard' },
|
|
28
|
+
report: { targetKey: 'reportName', metaType: 'report' },
|
|
29
|
+
url: { targetKey: 'url' },
|
|
30
|
+
group: {},
|
|
31
|
+
};
|
|
32
|
+
/** Typed target fields across the whole union. */
|
|
33
|
+
const TYPED_TARGET_FIELDS = [
|
|
34
|
+
'objectName',
|
|
35
|
+
'viewName',
|
|
36
|
+
'recordId',
|
|
37
|
+
'recordMode',
|
|
38
|
+
'filters',
|
|
39
|
+
'pageName',
|
|
40
|
+
'dashboardName',
|
|
41
|
+
'reportName',
|
|
42
|
+
'url',
|
|
43
|
+
'target',
|
|
44
|
+
'params',
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Legacy / off-spec keys that runtime resolution ignores and save-time
|
|
48
|
+
* validation rejects. Cleared on EVERY inspector edit so stale keys never
|
|
49
|
+
* hijack behavior or fail validation (`navigation.0: Invalid input`).
|
|
50
|
+
*/
|
|
51
|
+
const LEGACY_KEYS = ['path', 'kind', 'href', 'route', 'object', 'page', 'dashboard', 'report'];
|
|
52
|
+
/**
|
|
53
|
+
* Map a legacy `kind` value (the old inspector's vocabulary) to the spec
|
|
54
|
+
* type; `link` was never a spec member — it maps to `url`.
|
|
55
|
+
*/
|
|
56
|
+
const LEGACY_KIND_TO_TYPE = {
|
|
57
|
+
object: 'object',
|
|
58
|
+
page: 'page',
|
|
59
|
+
dashboard: 'dashboard',
|
|
60
|
+
report: 'report',
|
|
61
|
+
link: 'url',
|
|
62
|
+
url: 'url',
|
|
63
|
+
group: 'group',
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Infer the effective type of a (possibly legacy) nav node for display:
|
|
67
|
+
* spec `type` wins; else legacy `kind`; else the presence of typed or
|
|
68
|
+
* legacy target fields; else children ⇒ group; else null (unset).
|
|
69
|
+
*/
|
|
70
|
+
export function inferNavItemType(node) {
|
|
71
|
+
const t = node.type;
|
|
72
|
+
if (typeof t === 'string' && NAV_ITEM_TYPES.includes(t)) {
|
|
73
|
+
return t;
|
|
74
|
+
}
|
|
75
|
+
const kind = node.kind;
|
|
76
|
+
if (typeof kind === 'string' && LEGACY_KIND_TO_TYPE[kind])
|
|
77
|
+
return LEGACY_KIND_TO_TYPE[kind];
|
|
78
|
+
if (node.objectName || node.object)
|
|
79
|
+
return 'object';
|
|
80
|
+
if (node.pageName || node.page)
|
|
81
|
+
return 'page';
|
|
82
|
+
if (node.dashboardName || node.dashboard)
|
|
83
|
+
return 'dashboard';
|
|
84
|
+
if (node.reportName || node.report)
|
|
85
|
+
return 'report';
|
|
86
|
+
if (node.url || node.href)
|
|
87
|
+
return 'url';
|
|
88
|
+
if (Array.isArray(node.children) && node.children.length > 0)
|
|
89
|
+
return 'group';
|
|
90
|
+
const path = node.path;
|
|
91
|
+
if (typeof path === 'string' && /^https?:/i.test(path))
|
|
92
|
+
return 'url';
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Derive an object item's landing mode from field presence, following
|
|
97
|
+
* resolveHref's precedence — the mode is a projection, never stored.
|
|
98
|
+
*/
|
|
99
|
+
export function deriveObjectTargetMode(node) {
|
|
100
|
+
if (node.recordId)
|
|
101
|
+
return 'record';
|
|
102
|
+
const filters = node.filters;
|
|
103
|
+
if (filters && typeof filters === 'object' && !Array.isArray(filters))
|
|
104
|
+
return 'filters';
|
|
105
|
+
if (node.viewName)
|
|
106
|
+
return 'view';
|
|
107
|
+
return 'default';
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The patch that clears everything EXCEPT the fields the given type+mode
|
|
111
|
+
* legitimately owns. Always includes the legacy keys. Spread this before
|
|
112
|
+
* the fields being set so a type/mode switch leaves no stale target behind.
|
|
113
|
+
*/
|
|
114
|
+
export function clearedTargetPatch(keep = []) {
|
|
115
|
+
const patch = {};
|
|
116
|
+
for (const key of [...TYPED_TARGET_FIELDS, ...LEGACY_KEYS]) {
|
|
117
|
+
if (!keep.includes(key))
|
|
118
|
+
patch[key] = undefined;
|
|
119
|
+
}
|
|
120
|
+
return patch;
|
|
121
|
+
}
|
|
122
|
+
/** Fields each object landing mode owns (besides objectName). */
|
|
123
|
+
export const OBJECT_MODE_FIELDS = {
|
|
124
|
+
default: ['objectName'],
|
|
125
|
+
view: ['objectName', 'viewName'],
|
|
126
|
+
record: ['objectName', 'recordId', 'recordMode'],
|
|
127
|
+
filters: ['objectName', 'filters'],
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Ensure a spec-valid snake_case `id`. Existing ids are kept; otherwise one
|
|
131
|
+
* is derived from the target/label and uniqued against sibling ids.
|
|
132
|
+
*/
|
|
133
|
+
export function ensureNavId(node, siblings, seed) {
|
|
134
|
+
const existing = node.id;
|
|
135
|
+
if (typeof existing === 'string' && existing)
|
|
136
|
+
return existing;
|
|
137
|
+
const raw = (seed ?? String(node.objectName ?? node.label ?? 'item'))
|
|
138
|
+
.toLowerCase()
|
|
139
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
140
|
+
.replace(/^_+|_+$/g, '') || 'item';
|
|
141
|
+
const base = `nav_${raw}`;
|
|
142
|
+
const taken = new Set(siblings.map((s) => (typeof s.id === 'string' ? s.id : '')).filter(Boolean));
|
|
143
|
+
if (!taken.has(base))
|
|
144
|
+
return base;
|
|
145
|
+
let n = 2;
|
|
146
|
+
while (taken.has(`${base}_${n}`))
|
|
147
|
+
n++;
|
|
148
|
+
return `${base}_${n}`;
|
|
149
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Search param carrying the designer's selected element. */
|
|
2
|
+
export declare const DESIGNER_SEL_PARAM = "sel";
|
|
3
|
+
/** Parse a `sel` param value; returns the nav item id for `nav:<id>`. */
|
|
4
|
+
export declare function parseNavSelParam(value: string | null | undefined): string | null;
|
|
5
|
+
export declare function formatNavSelParam(navId: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Locate a nav item by its `id` across all accepted root keys, returning
|
|
8
|
+
* the positional selection id the canvas/inspector pair uses
|
|
9
|
+
* (`<rootKey>[i]` / `<rootKey>[i].children[j]`), or null when absent.
|
|
10
|
+
*/
|
|
11
|
+
export declare function findNavPositionById(draft: Record<string, unknown>, navId: string): {
|
|
12
|
+
selectionId: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
} | null;
|
|
15
|
+
/**
|
|
16
|
+
* Read the nav item `id` at a positional selection id (the inverse of
|
|
17
|
+
* {@link findNavPositionById}); null when the path is invalid or the node
|
|
18
|
+
* has no id.
|
|
19
|
+
*/
|
|
20
|
+
export declare function navIdAtPosition(draft: Record<string, unknown>, positionalId: string): string | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Nav-item deep-link selection (#2272).
|
|
4
|
+
*
|
|
5
|
+
* The designer's internal selection ids are POSITIONAL (`navigation[2]`,
|
|
6
|
+
* `nav[0].children[1]`) — cheap for canvas/inspector wiring but unstable:
|
|
7
|
+
* they drift on reorder and mean nothing outside one editing session. The
|
|
8
|
+
* EXTERNAL contract is the nav item's spec-required snake_case `id`,
|
|
9
|
+
* carried in the URL as `?sel=nav:<id>`. These helpers translate between
|
|
10
|
+
* the two at the designer boundary; positions never leave component state.
|
|
11
|
+
*/
|
|
12
|
+
import { APP_NAV_ROOT_KEYS } from './inspectors/AppNavInspector';
|
|
13
|
+
/** Search param carrying the designer's selected element. */
|
|
14
|
+
export const DESIGNER_SEL_PARAM = 'sel';
|
|
15
|
+
/** Parse a `sel` param value; returns the nav item id for `nav:<id>`. */
|
|
16
|
+
export function parseNavSelParam(value) {
|
|
17
|
+
if (!value || !value.startsWith('nav:'))
|
|
18
|
+
return null;
|
|
19
|
+
const id = value.slice(4);
|
|
20
|
+
return id.length > 0 ? id : null;
|
|
21
|
+
}
|
|
22
|
+
export function formatNavSelParam(navId) {
|
|
23
|
+
return `nav:${navId}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Locate a nav item by its `id` across all accepted root keys, returning
|
|
27
|
+
* the positional selection id the canvas/inspector pair uses
|
|
28
|
+
* (`<rootKey>[i]` / `<rootKey>[i].children[j]`), or null when absent.
|
|
29
|
+
*/
|
|
30
|
+
export function findNavPositionById(draft, navId) {
|
|
31
|
+
const walk = (nodes, prefix) => {
|
|
32
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
33
|
+
const node = nodes[i];
|
|
34
|
+
if (!node || typeof node !== 'object')
|
|
35
|
+
continue;
|
|
36
|
+
const pos = `${prefix}[${i}]`;
|
|
37
|
+
if (node.id === navId) {
|
|
38
|
+
const label = node.label ?? node.title ?? node.name;
|
|
39
|
+
return { selectionId: pos, label: typeof label === 'string' ? label : undefined };
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(node.children)) {
|
|
42
|
+
const hit = walk(node.children, `${pos}.children`);
|
|
43
|
+
if (hit)
|
|
44
|
+
return hit;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
for (const rootKey of APP_NAV_ROOT_KEYS) {
|
|
50
|
+
const arr = draft[rootKey];
|
|
51
|
+
if (!Array.isArray(arr))
|
|
52
|
+
continue;
|
|
53
|
+
const hit = walk(arr, rootKey);
|
|
54
|
+
if (hit)
|
|
55
|
+
return hit;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read the nav item `id` at a positional selection id (the inverse of
|
|
61
|
+
* {@link findNavPositionById}); null when the path is invalid or the node
|
|
62
|
+
* has no id.
|
|
63
|
+
*/
|
|
64
|
+
export function navIdAtPosition(draft, positionalId) {
|
|
65
|
+
const segs = positionalId.split('.');
|
|
66
|
+
let node;
|
|
67
|
+
for (let s = 0; s < segs.length; s++) {
|
|
68
|
+
const m = /^([a-zA-Z_]\w*)\[(\d+)\]$/.exec(segs[s]);
|
|
69
|
+
if (!m)
|
|
70
|
+
return null;
|
|
71
|
+
const key = m[1];
|
|
72
|
+
const index = Number(m[2]);
|
|
73
|
+
const arr = s === 0 ? draft[key] : node?.[key];
|
|
74
|
+
if (!Array.isArray(arr))
|
|
75
|
+
return null;
|
|
76
|
+
node = arr[index];
|
|
77
|
+
if (!node || typeof node !== 'object')
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return typeof node?.id === 'string' && node.id ? node.id : null;
|
|
81
|
+
}
|
|
@@ -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,24 @@ 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
|
+
// Spec invariants from birth (#2245): a snake_case `id` and a `type`
|
|
121
|
+
// (object is the 80% case per the app-composition guide) — never the
|
|
122
|
+
// old `{label, path:''}` placeholder that failed save validation. The
|
|
123
|
+
// item completes once the inspector's object picker fills `objectName`.
|
|
124
|
+
const taken = new Set(items.map((it) => (typeof it.id === 'string' ? it.id : '')).filter(Boolean));
|
|
125
|
+
let navId = `nav_item_${items.length + 1}`;
|
|
126
|
+
for (let n = items.length + 2; taken.has(navId); n++)
|
|
127
|
+
navId = `nav_item_${n}`;
|
|
128
|
+
const newItem = { id: navId, type: 'object', label: newLabel };
|
|
118
129
|
const next = appendArray(items, newItem);
|
|
119
130
|
setItems(next);
|
|
120
131
|
onSelectionChange?.({
|
|
121
132
|
kind: 'nav',
|
|
122
133
|
id: `${rootKey}[${next.length - 1}]`,
|
|
123
|
-
label:
|
|
134
|
+
label: newLabel,
|
|
124
135
|
});
|
|
125
|
-
}, [onPatch, items, setItems, rootKey, onSelectionChange]);
|
|
136
|
+
}, [onPatch, items, setItems, rootKey, onSelectionChange, locale]);
|
|
126
137
|
const removeItem = React.useCallback((index) => {
|
|
127
138
|
if (!onPatch)
|
|
128
139
|
return;
|
|
@@ -158,7 +169,7 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
|
|
|
158
169
|
label: navLabel(next[to] ?? {}, to),
|
|
159
170
|
});
|
|
160
171
|
}, [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-
|
|
172
|
+
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
173
|
if (!onPatch)
|
|
163
174
|
return;
|
|
164
175
|
if (!e.dataTransfer.types.includes(DND_MIME))
|
|
@@ -176,8 +187,8 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
|
|
|
176
187
|
moveItem(dragIndex, items.length);
|
|
177
188
|
setDragIndex(null);
|
|
178
189
|
}, 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
|
-
: '
|
|
190
|
+
? t('engine.appNav.empty', locale)
|
|
191
|
+
: 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
192
|
if (dragIndex == null)
|
|
182
193
|
return;
|
|
183
194
|
moveItem(dragIndex, i);
|
|
@@ -192,6 +203,7 @@ function NavCardTree({ item, index, depth, path, selectedId, canEdit, onClick, o
|
|
|
192
203
|
})] }));
|
|
193
204
|
}
|
|
194
205
|
function NavCard({ item, index, depth, path, isSelected, canEdit, onClick, onRename, onRemove, onDragStart, onDragEnd, onDropBefore, }) {
|
|
206
|
+
const locale = useMetadataLocale();
|
|
195
207
|
const kind = inferKind(item);
|
|
196
208
|
const Icon = kindIcon(kind);
|
|
197
209
|
const tone = kindTone(kind);
|
|
@@ -256,5 +268,5 @@ function NavCard({ item, index, depth, path, isSelected, canEdit, onClick, onRen
|
|
|
256
268
|
e.stopPropagation();
|
|
257
269
|
onRemove();
|
|
258
270
|
}
|
|
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":
|
|
271
|
+
}, 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
272
|
}
|
|
@@ -80,8 +80,10 @@ export function AppPreview({ name, draft, editing, selection, onSelectionChange,
|
|
|
80
80
|
}
|
|
81
81
|
return { rootKey: null, navItems: [] };
|
|
82
82
|
}, [draft]);
|
|
83
|
-
// For Add we need a root key even when empty — default to
|
|
84
|
-
|
|
83
|
+
// For Add we need a root key even when empty — default to `navigation`,
|
|
84
|
+
// the only root key the spec (AppSchema) actually accepts; `nav` /
|
|
85
|
+
// `tabs` / `items` are read-back tolerances, not write targets (#2245).
|
|
86
|
+
const addRootKey = rootKey ?? 'navigation';
|
|
85
87
|
const designMode = !!(editing && onSelectionChange);
|
|
86
88
|
const canEdit = designMode && !!onPatch;
|
|
87
89
|
const selectedId = selection && selection.kind === 'nav' ? selection.id : null;
|
|
@@ -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([]);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BuilderLanding — the application builder's front door.
|
|
3
|
+
*
|
|
4
|
+
* The journey from login: Home → Studio app → the App Builder landing (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;
|