@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +700 -0
  2. package/README.md +32 -0
  3. package/dist/chrome/KeyboardShortcutsDialog.js +2 -1
  4. package/dist/console/AppContent.js +145 -26
  5. package/dist/console/ConsoleShell.js +25 -3
  6. package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
  7. package/dist/console/home/CloudOnboardingNext.js +14 -4
  8. package/dist/console/home/HomePage.js +34 -7
  9. package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
  10. package/dist/console/organizations/OrganizationsPage.js +16 -7
  11. package/dist/context/CommandPaletteProvider.js +2 -1
  12. package/dist/hooks/useConsoleActionRuntime.js +32 -3
  13. package/dist/hooks/useObjectActions.js +16 -4
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +3 -0
  16. package/dist/layout/AppHeader.js +13 -5
  17. package/dist/layout/AppSidebar.js +10 -4
  18. package/dist/observability/sentry.d.ts +5 -0
  19. package/dist/observability/sentry.js +6 -1
  20. package/dist/preview/DraftChangesPanel.d.ts +31 -1
  21. package/dist/preview/DraftChangesPanel.js +146 -18
  22. package/dist/urlParams.d.ts +68 -0
  23. package/dist/urlParams.js +76 -0
  24. package/dist/utils/appRoute.d.ts +15 -0
  25. package/dist/utils/appRoute.js +22 -0
  26. package/dist/utils/deriveRelatedLists.d.ts +20 -5
  27. package/dist/utils/deriveRelatedLists.js +31 -13
  28. package/dist/utils/index.d.ts +1 -1
  29. package/dist/utils/index.js +1 -1
  30. package/dist/utils/pageTabsUrlSync.d.ts +32 -0
  31. package/dist/utils/pageTabsUrlSync.js +43 -0
  32. package/dist/utils/recordFormNavigation.d.ts +40 -0
  33. package/dist/utils/recordFormNavigation.js +30 -0
  34. package/dist/utils/resolveViewId.d.ts +23 -0
  35. package/dist/utils/resolveViewId.js +37 -0
  36. package/dist/utils/warnSuppressedListNav.d.ts +10 -0
  37. package/dist/utils/warnSuppressedListNav.js +40 -0
  38. package/dist/views/InterfaceListPage.d.ts +1 -0
  39. package/dist/views/InterfaceListPage.js +7 -5
  40. package/dist/views/ObjectDataPage.d.ts +29 -0
  41. package/dist/views/ObjectDataPage.js +227 -0
  42. package/dist/views/ObjectView.js +65 -13
  43. package/dist/views/RecordDetailView.js +191 -123
  44. package/dist/views/RecordFormPage.js +7 -1
  45. package/dist/views/RelatedRecordActionsBridge.d.ts +33 -0
  46. package/dist/views/RelatedRecordActionsBridge.js +147 -0
  47. package/dist/views/metadata-admin/PackagesPage.js +18 -7
  48. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
  49. package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
  50. package/dist/views/metadata-admin/ResourceEditPage.js +39 -0
  51. package/dist/views/metadata-admin/i18n.d.ts +12 -21
  52. package/dist/views/metadata-admin/i18n.js +557 -6
  53. package/dist/views/metadata-admin/inspectors/AppNavInspector.d.ts +11 -4
  54. package/dist/views/metadata-admin/inspectors/AppNavInspector.js +141 -7
  55. package/dist/views/metadata-admin/inspectors/FlowReferenceField.d.ts +14 -0
  56. package/dist/views/metadata-admin/inspectors/FlowReferenceField.js +76 -5
  57. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +60 -30
  58. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +8 -1
  59. package/dist/views/metadata-admin/inspectors/flow-node-config.js +3 -2
  60. package/dist/views/metadata-admin/inspectors/nav-target.d.ts +52 -0
  61. package/dist/views/metadata-admin/inspectors/nav-target.js +149 -0
  62. package/dist/views/metadata-admin/nav-selection.d.ts +20 -0
  63. package/dist/views/metadata-admin/nav-selection.js +81 -0
  64. package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
  65. package/dist/views/metadata-admin/permission-slice.js +70 -0
  66. package/dist/views/metadata-admin/previews/AppNavCanvas.js +19 -7
  67. package/dist/views/metadata-admin/previews/AppPreview.js +4 -2
  68. package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
  69. package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
  70. package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
  71. package/dist/views/studio-design/BuilderLanding.js +126 -0
  72. package/dist/views/studio-design/ObjectFormDesigner.d.ts +33 -0
  73. package/dist/views/studio-design/ObjectFormDesigner.js +231 -0
  74. package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
  75. package/dist/views/studio-design/ObjectSettingsPanel.js +46 -0
  76. package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
  77. package/dist/views/studio-design/ObjectValidationsPanel.js +80 -0
  78. package/dist/views/studio-design/PackageIdInput.d.ts +31 -0
  79. package/dist/views/studio-design/PackageIdInput.js +40 -0
  80. package/dist/views/studio-design/StudioDesignSurface.d.ts +13 -0
  81. package/dist/views/studio-design/StudioDesignSurface.js +962 -145
  82. package/dist/views/studio-design/metadataError.d.ts +23 -0
  83. package/dist/views/studio-design/metadataError.js +44 -0
  84. package/dist/views/studio-design/packageSurfaces.d.ts +49 -0
  85. package/dist/views/studio-design/packageSurfaces.js +34 -0
  86. package/dist/views/studio-design/packages-io.d.ts +38 -0
  87. package/dist/views/studio-design/packages-io.js +73 -0
  88. package/dist/views/studio-design/skeletons.d.ts +16 -0
  89. package/dist/views/studio-design/skeletons.js +51 -0
  90. 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 newItem = { label: 'New item', path: '' };
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: 'New item',
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-mono uppercase tracking-wide text-muted-foreground", children: rootKey }), _jsxs(Badge, { variant: "outline", className: "text-[10px]", children: [items.length, " ", items.length === 1 ? 'item' : 'items'] })] }), 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" }), " Add nav item"] }))] }), _jsx("div", { className: "space-y-1.5 p-2", onDragOver: (e) => {
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
- ? 'Empty — click "Add nav item" to start'
180
- : 'No top-level nav items yet' })) : (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: () => {
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": "Remove nav item", children: _jsx(Trash2, { className: "h-3 w-3" }) }))] })] }));
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 "nav".
84
- const addRootKey = rootKey ?? 'nav';
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
- 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) })), step.error?.message && (_jsx("span", { className: "min-w-0 truncate text-[9px] text-rose-600", title: step.error.message, children: step.error.message }))] }));
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
- 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}`] }), run.error?.message && (_jsx("div", { className: "pb-1 text-[10px] text-rose-600", children: run.error.message })), 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}`))) }))] }))] }));
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;