@ankhorage/studio 0.15.0 → 0.17.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/README.md +1 -1
- package/dist/contractGuards.d.ts.map +1 -1
- package/dist/contractGuards.js +4 -2
- package/dist/contractGuards.js.map +1 -1
- package/dist/core/StudioProvider.d.ts.map +1 -1
- package/dist/core/StudioProvider.js +87 -10
- package/dist/core/StudioProvider.js.map +1 -1
- package/dist/host/layout/auth/resolveAuthLayoutPlan.js +2 -2
- package/dist/host/layout/auth/resolveAuthLayoutPlan.js.map +1 -1
- package/dist/host/layout/layoutGenerator.js +3 -3
- package/dist/host/layout/layoutGenerator.js.map +1 -1
- package/dist/host/layout/templates/navigation.js +4 -10
- package/dist/host/layout/templates/navigation.js.map +1 -1
- package/dist/host/layout/templates/rootLayout.d.ts.map +1 -1
- package/dist/host/layout/templates/rootLayout.js +13 -8
- package/dist/host/layout/templates/rootLayout.js.map +1 -1
- package/dist/host/orchestrator/templates.d.ts.map +1 -1
- package/dist/host/orchestrator/templates.js +1 -11
- package/dist/host/orchestrator/templates.js.map +1 -1
- package/dist/host/templateRegistry/index.d.ts.map +1 -1
- package/dist/host/templateRegistry/index.js +2 -17
- package/dist/host/templateRegistry/index.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/manifestState.d.ts +59 -2
- package/dist/manifestState.d.ts.map +1 -1
- package/dist/manifestState.js +336 -51
- package/dist/manifestState.js.map +1 -1
- package/dist/routeUtils.d.ts +1 -2
- package/dist/routeUtils.d.ts.map +1 -1
- package/dist/routeUtils.js +6 -44
- package/dist/routeUtils.js.map +1 -1
- package/dist/runtime/index.d.ts +0 -2
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +0 -2
- package/dist/runtime/index.js.map +1 -1
- package/dist/studioAdminRouteModel.d.ts +7 -1
- package/dist/studioAdminRouteModel.d.ts.map +1 -1
- package/dist/studioAdminRouteModel.js +54 -0
- package/dist/studioAdminRouteModel.js.map +1 -1
- package/dist/ui/admin/AnkhAdminPage.d.ts.map +1 -1
- package/dist/ui/admin/AnkhAdminPage.js +5 -1
- package/dist/ui/admin/AnkhAdminPage.js.map +1 -1
- package/dist/ui/admin/AnkhAdminShell.d.ts.map +1 -1
- package/dist/ui/admin/AnkhAdminShell.js +5 -2
- package/dist/ui/admin/AnkhAdminShell.js.map +1 -1
- package/dist/ui/admin/pages/ScreenDetailAdminPage.d.ts +6 -0
- package/dist/ui/admin/pages/ScreenDetailAdminPage.d.ts.map +1 -0
- package/dist/ui/admin/pages/ScreenDetailAdminPage.js +86 -0
- package/dist/ui/admin/pages/ScreenDetailAdminPage.js.map +1 -0
- package/dist/ui/admin/pages/ScreensAdminPage.d.ts +3 -0
- package/dist/ui/admin/pages/ScreensAdminPage.d.ts.map +1 -0
- package/dist/ui/admin/pages/ScreensAdminPage.js +99 -0
- package/dist/ui/admin/pages/ScreensAdminPage.js.map +1 -0
- package/dist/ui/admin/pages/screens/screensAdminActions.d.ts +28 -0
- package/dist/ui/admin/pages/screens/screensAdminActions.d.ts.map +1 -0
- package/dist/ui/admin/pages/screens/screensAdminActions.js +22 -0
- package/dist/ui/admin/pages/screens/screensAdminActions.js.map +1 -0
- package/dist/ui/studioAppBarModel.d.ts +10 -0
- package/dist/ui/studioAppBarModel.d.ts.map +1 -1
- package/dist/ui/studioAppBarModel.js +16 -1
- package/dist/ui/studioAppBarModel.js.map +1 -1
- package/dist/ui/useStudioAppBarAugmentation.d.ts.map +1 -1
- package/dist/ui/useStudioAppBarAugmentation.js +21 -3
- package/dist/ui/useStudioAppBarAugmentation.js.map +1 -1
- package/package.json +3 -15
- package/dist/manifestNavigatorPreviewModel.d.ts +0 -40
- package/dist/manifestNavigatorPreviewModel.d.ts.map +0 -1
- package/dist/manifestNavigatorPreviewModel.js +0 -191
- package/dist/manifestNavigatorPreviewModel.js.map +0 -1
- package/dist/runtime/previewRegistry.d.ts +0 -7
- package/dist/runtime/previewRegistry.d.ts.map +0 -1
- package/dist/runtime/previewRegistry.js +0 -6
- package/dist/runtime/previewRegistry.js.map +0 -1
- package/dist/runtime/previewRuntimeConfig.d.ts +0 -10
- package/dist/runtime/previewRuntimeConfig.d.ts.map +0 -1
- package/dist/runtime/previewRuntimeConfig.js +0 -13
- package/dist/runtime/previewRuntimeConfig.js.map +0 -1
package/dist/manifestState.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NAVIGATOR_TYPES } from '@ankhorage/contracts';
|
|
1
2
|
import { insertNodeAtPlacement, moveNodeToPlacement } from './index';
|
|
2
3
|
export const DEFAULT_STUDIO_SCREEN_TEMPLATE = {
|
|
3
4
|
id: 'tpl-screen-empty',
|
|
@@ -104,6 +105,215 @@ export function groupScreenRouteEntries(entries) {
|
|
|
104
105
|
}
|
|
105
106
|
return Array.from(groups.values());
|
|
106
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Verifies the Studio manifest invariant that route registry keys and stable
|
|
110
|
+
* ScreenSpec identities are the same unique value.
|
|
111
|
+
*/
|
|
112
|
+
export function hasCanonicalStudioScreenRegistryIdentity(screens) {
|
|
113
|
+
const screenIds = new Set();
|
|
114
|
+
for (const [registryKey, screen] of Object.entries(screens)) {
|
|
115
|
+
if (registryKey !== screen.id || screenIds.has(screen.id))
|
|
116
|
+
return false;
|
|
117
|
+
screenIds.add(screen.id);
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
function resolveCanonicalStudioScreen(manifest, screenId) {
|
|
122
|
+
if (!hasCanonicalStudioScreenRegistryIdentity(manifest.screens))
|
|
123
|
+
return undefined;
|
|
124
|
+
return manifest.screens[screenId];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Derives the complete package-neutral screen/navigation authoring model from
|
|
128
|
+
* the manifest without flattening its navigator tree.
|
|
129
|
+
*/
|
|
130
|
+
export function deriveStudioScreenNavigationModel(manifest) {
|
|
131
|
+
const primaryNavigatorPath = getPrimaryNavigatorPath(manifest.navigator.routes);
|
|
132
|
+
const primaryNavigator = findNavigatorAtPath(manifest.navigator, primaryNavigatorPath) ?? manifest.navigator;
|
|
133
|
+
const diagnostics = [];
|
|
134
|
+
const referencesByRegistryKey = new Map();
|
|
135
|
+
collectNavigationModelReferences({
|
|
136
|
+
navigator: manifest.navigator,
|
|
137
|
+
screens: manifest.screens,
|
|
138
|
+
primaryNavigatorPath,
|
|
139
|
+
parentPath: [],
|
|
140
|
+
routePathPrefix: [],
|
|
141
|
+
runtimePathPrefix: [],
|
|
142
|
+
diagnostics,
|
|
143
|
+
referencesByRegistryKey,
|
|
144
|
+
});
|
|
145
|
+
const screenIdCounts = new Map();
|
|
146
|
+
for (const screen of Object.values(manifest.screens)) {
|
|
147
|
+
screenIdCounts.set(screen.id, (screenIdCounts.get(screen.id) ?? 0) + 1);
|
|
148
|
+
}
|
|
149
|
+
const reportedDuplicateScreenIds = new Set();
|
|
150
|
+
const screens = Object.entries(manifest.screens).map(([registryKey, screen]) => {
|
|
151
|
+
const screenId = screen.id;
|
|
152
|
+
const routeReferences = referencesByRegistryKey.get(registryKey) ?? [];
|
|
153
|
+
if (registryKey !== screenId) {
|
|
154
|
+
diagnostics.push({
|
|
155
|
+
code: 'screen-registry-key-mismatch',
|
|
156
|
+
message: `Screen registry key "${registryKey}" does not match stable ScreenSpec.id "${screenId}".`,
|
|
157
|
+
parentPath: [],
|
|
158
|
+
screenId,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if ((screenIdCounts.get(screenId) ?? 0) > 1 && !reportedDuplicateScreenIds.has(screenId)) {
|
|
162
|
+
reportedDuplicateScreenIds.add(screenId);
|
|
163
|
+
diagnostics.push({
|
|
164
|
+
code: 'duplicate-screen-id',
|
|
165
|
+
message: `Stable ScreenSpec.id "${screenId}" is used by multiple screen registry entries.`,
|
|
166
|
+
parentPath: [],
|
|
167
|
+
screenId,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (routeReferences.length > 1) {
|
|
171
|
+
diagnostics.push({
|
|
172
|
+
code: 'ambiguous-screen-route-reference',
|
|
173
|
+
message: `Screen "${screenId}" is referenced by ${routeReferences.length} routes.`,
|
|
174
|
+
parentPath: [],
|
|
175
|
+
screenId,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return { screenId, screen, routeReferences };
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
primaryNavigatorPath,
|
|
182
|
+
primaryNavigator,
|
|
183
|
+
screens,
|
|
184
|
+
diagnostics,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Returns a concrete canonical app pathname only when a screen has one route
|
|
189
|
+
* reference and that route does not require runtime parameters.
|
|
190
|
+
*/
|
|
191
|
+
export function resolveStudioScreenAppPath(model, screenId) {
|
|
192
|
+
const matchingEntries = model.screens.filter((candidate) => candidate.screenId === screenId);
|
|
193
|
+
if (matchingEntries.length !== 1)
|
|
194
|
+
return null;
|
|
195
|
+
const [entry] = matchingEntries;
|
|
196
|
+
if (!entry)
|
|
197
|
+
return null;
|
|
198
|
+
if (entry.routeReferences.length !== 1)
|
|
199
|
+
return null;
|
|
200
|
+
const [reference] = entry.routeReferences;
|
|
201
|
+
if (!reference)
|
|
202
|
+
return null;
|
|
203
|
+
const hasDynamicSegment = reference.pathnamePattern
|
|
204
|
+
.split('/')
|
|
205
|
+
.some((segment) => segment.startsWith(':') || /^\[.*\]$/u.test(segment));
|
|
206
|
+
if (hasDynamicSegment)
|
|
207
|
+
return null;
|
|
208
|
+
const matchingReferences = model.screens.flatMap((candidate) => candidate.routeReferences.filter((candidateReference) => candidateReference.pathnamePattern === reference.pathnamePattern));
|
|
209
|
+
return matchingReferences.length === 1 ? reference.pathnamePattern : null;
|
|
210
|
+
}
|
|
211
|
+
function collectNavigationModelReferences(args) {
|
|
212
|
+
const { navigator, screens, primaryNavigatorPath, parentPath, routePathPrefix, runtimePathPrefix, diagnostics, referencesByRegistryKey, } = args;
|
|
213
|
+
const siblingNameCounts = new Map();
|
|
214
|
+
for (const route of navigator.routes) {
|
|
215
|
+
siblingNameCounts.set(route.name, (siblingNameCounts.get(route.name) ?? 0) + 1);
|
|
216
|
+
}
|
|
217
|
+
if (navigator.routes.length === 0) {
|
|
218
|
+
diagnostics.push({
|
|
219
|
+
code: 'empty-navigator',
|
|
220
|
+
message: `Navigator at "${formatParentPath(parentPath)}" has no routes.`,
|
|
221
|
+
parentPath,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (navigator.initialRouteName !== undefined &&
|
|
225
|
+
siblingNameCounts.get(navigator.initialRouteName) !== 1) {
|
|
226
|
+
diagnostics.push({
|
|
227
|
+
code: 'invalid-initial-route',
|
|
228
|
+
message: `Initial route "${navigator.initialRouteName}" is not one unambiguous direct route of "${formatParentPath(parentPath)}".`,
|
|
229
|
+
parentPath,
|
|
230
|
+
routeName: navigator.initialRouteName,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
for (const [siblingIndex, route] of navigator.routes.entries()) {
|
|
234
|
+
const routePath = [...routePathPrefix, route.name];
|
|
235
|
+
const runtimePath = appendRuntimeRoutePath(runtimePathPrefix, route);
|
|
236
|
+
const isPrimaryNavigatorMember = pathsEqual(parentPath, primaryNavigatorPath);
|
|
237
|
+
if ((siblingNameCounts.get(route.name) ?? 0) > 1 &&
|
|
238
|
+
navigator.routes.findIndex((candidate) => candidate.name === route.name) === siblingIndex) {
|
|
239
|
+
diagnostics.push({
|
|
240
|
+
code: 'duplicate-sibling-route-name',
|
|
241
|
+
message: `Route name "${route.name}" is duplicated under "${formatParentPath(parentPath)}".`,
|
|
242
|
+
parentPath,
|
|
243
|
+
routeName: route.name,
|
|
244
|
+
screenId: route.screenId,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (!route.screenId && !route.navigator) {
|
|
248
|
+
diagnostics.push({
|
|
249
|
+
code: 'missing-route-target',
|
|
250
|
+
message: `Route "${route.name}" has neither a screen nor a nested navigator.`,
|
|
251
|
+
parentPath,
|
|
252
|
+
routeName: route.name,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
else if (route.screenId && route.navigator) {
|
|
256
|
+
diagnostics.push({
|
|
257
|
+
code: 'ambiguous-route-target',
|
|
258
|
+
message: `Route "${route.name}" targets both a screen and a nested navigator.`,
|
|
259
|
+
parentPath,
|
|
260
|
+
routeName: route.name,
|
|
261
|
+
screenId: route.screenId,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (route.screenId) {
|
|
265
|
+
if (!screens[route.screenId]) {
|
|
266
|
+
diagnostics.push({
|
|
267
|
+
code: 'missing-screen-reference',
|
|
268
|
+
message: `Route "${route.name}" references missing screen "${route.screenId}".`,
|
|
269
|
+
parentPath,
|
|
270
|
+
routeName: route.name,
|
|
271
|
+
screenId: route.screenId,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const reference = {
|
|
276
|
+
route,
|
|
277
|
+
parentPath,
|
|
278
|
+
routePath,
|
|
279
|
+
pathnamePattern: toCanonicalRoutePattern(runtimePath),
|
|
280
|
+
siblingIndex,
|
|
281
|
+
navigatorType: navigator.type,
|
|
282
|
+
isPrimaryNavigatorMember,
|
|
283
|
+
showInPrimaryNavigation: route.showInPrimaryNavigation !== false,
|
|
284
|
+
isPrimaryInitialRoute: isPrimaryNavigatorMember && navigator.initialRouteName === route.name,
|
|
285
|
+
};
|
|
286
|
+
const currentReferences = referencesByRegistryKey.get(route.screenId) ?? [];
|
|
287
|
+
currentReferences.push(reference);
|
|
288
|
+
referencesByRegistryKey.set(route.screenId, currentReferences);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (route.navigator) {
|
|
292
|
+
collectNavigationModelReferences({
|
|
293
|
+
navigator: route.navigator,
|
|
294
|
+
screens,
|
|
295
|
+
primaryNavigatorPath,
|
|
296
|
+
parentPath: routePath,
|
|
297
|
+
routePathPrefix: routePath,
|
|
298
|
+
runtimePathPrefix: runtimePath,
|
|
299
|
+
diagnostics,
|
|
300
|
+
referencesByRegistryKey,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function appendRuntimeRoutePath(runtimePathPrefix, route) {
|
|
306
|
+
const explicitPath = route.path?.trim();
|
|
307
|
+
const segmentPath = explicitPath ?? route.name;
|
|
308
|
+
const segments = segmentPath.split('/').filter(Boolean);
|
|
309
|
+
return explicitPath?.startsWith('/') ? segments : [...runtimePathPrefix, ...segments];
|
|
310
|
+
}
|
|
311
|
+
function pathsEqual(first, second) {
|
|
312
|
+
return first.length === second.length && first.every((value, index) => value === second[index]);
|
|
313
|
+
}
|
|
314
|
+
function formatParentPath(parentPath) {
|
|
315
|
+
return parentPath.length === 0 ? '<root>' : parentPath.join('/');
|
|
316
|
+
}
|
|
107
317
|
export function listScreenIdsInRouteOrder(routes) {
|
|
108
318
|
return collectScreenRouteEntries(routes).map((entry) => entry.screenId);
|
|
109
319
|
}
|
|
@@ -113,6 +323,8 @@ export function listScreenIdsInRouteOrder(routes) {
|
|
|
113
323
|
* back to the first route that reaches an available screen.
|
|
114
324
|
*/
|
|
115
325
|
export function resolveInitialScreenId(navigator, screens) {
|
|
326
|
+
if (screens && !hasCanonicalStudioScreenRegistryIdentity(screens))
|
|
327
|
+
return null;
|
|
116
328
|
const initialRoute = navigator.initialRouteName
|
|
117
329
|
? navigator.routes.find((route) => route.name === navigator.initialRouteName)
|
|
118
330
|
: undefined;
|
|
@@ -126,22 +338,22 @@ export function resolveInitialScreenId(navigator, screens) {
|
|
|
126
338
|
return nestedScreenId;
|
|
127
339
|
}
|
|
128
340
|
if (route.screenId && (!screens || screens[route.screenId])) {
|
|
129
|
-
return route.screenId;
|
|
341
|
+
return screens ? (screens[route.screenId]?.id ?? null) : route.screenId;
|
|
130
342
|
}
|
|
131
343
|
}
|
|
132
344
|
return null;
|
|
133
345
|
}
|
|
134
346
|
export function resolveInitialActiveScreenId(manifest) {
|
|
135
|
-
if (!manifest)
|
|
347
|
+
if (!manifest || !hasCanonicalStudioScreenRegistryIdentity(manifest.screens))
|
|
136
348
|
return null;
|
|
137
349
|
const firstRoutedScreenId = resolveInitialScreenId(manifest.navigator, manifest.screens);
|
|
138
|
-
const [firstScreenId] = Object.
|
|
350
|
+
const [firstScreenId] = Object.values(manifest.screens).map((screen) => screen.id);
|
|
139
351
|
return firstRoutedScreenId ?? firstScreenId ?? null;
|
|
140
352
|
}
|
|
141
353
|
export function resolveActiveRootNode(manifest, activeScreenId) {
|
|
142
354
|
if (!manifest || !activeScreenId)
|
|
143
355
|
return null;
|
|
144
|
-
return manifest
|
|
356
|
+
return resolveCanonicalStudioScreen(manifest, activeScreenId)?.root ?? null;
|
|
145
357
|
}
|
|
146
358
|
export function findNodeInManifest(root, id) {
|
|
147
359
|
if (root.id === id)
|
|
@@ -159,9 +371,11 @@ export function resolveSafeSelectedNodeId(rootNode, selectedNodeId) {
|
|
|
159
371
|
return findNodeInManifest(rootNode, selectedNodeId) ? selectedNodeId : null;
|
|
160
372
|
}
|
|
161
373
|
export function findScreenIdForNode(manifest, nodeId) {
|
|
162
|
-
|
|
374
|
+
if (!hasCanonicalStudioScreenRegistryIdentity(manifest.screens))
|
|
375
|
+
return null;
|
|
376
|
+
for (const screen of Object.values(manifest.screens)) {
|
|
163
377
|
if (findNodeInManifest(screen.root, nodeId)) {
|
|
164
|
-
return
|
|
378
|
+
return screen.id;
|
|
165
379
|
}
|
|
166
380
|
}
|
|
167
381
|
return null;
|
|
@@ -169,7 +383,7 @@ export function findScreenIdForNode(manifest, nodeId) {
|
|
|
169
383
|
export function updateStudioManifestNode(manifest, activeScreenId, nodeId, newProps) {
|
|
170
384
|
if (!activeScreenId)
|
|
171
385
|
return manifest;
|
|
172
|
-
const screen = manifest
|
|
386
|
+
const screen = resolveCanonicalStudioScreen(manifest, activeScreenId);
|
|
173
387
|
if (!screen)
|
|
174
388
|
return manifest;
|
|
175
389
|
const newRoot = updateNodeInManifestTree(screen.root, nodeId, newProps);
|
|
@@ -189,7 +403,7 @@ export function updateStudioManifestNode(manifest, activeScreenId, nodeId, newPr
|
|
|
189
403
|
export function deleteStudioManifestNode(manifest, activeScreenId, nodeId) {
|
|
190
404
|
if (!activeScreenId)
|
|
191
405
|
return manifest;
|
|
192
|
-
const screen = manifest
|
|
406
|
+
const screen = resolveCanonicalStudioScreen(manifest, activeScreenId);
|
|
193
407
|
if (!screen || screen.root.id === nodeId)
|
|
194
408
|
return manifest;
|
|
195
409
|
const deletedNode = findNodeInManifest(screen.root, nodeId);
|
|
@@ -216,7 +430,7 @@ export function insertStudioManifestNodeAtPlacement(args) {
|
|
|
216
430
|
const { manifest, activeScreenId, placement, newNode, componentMeta } = args;
|
|
217
431
|
if (!activeScreenId)
|
|
218
432
|
return null;
|
|
219
|
-
const screen = manifest
|
|
433
|
+
const screen = resolveCanonicalStudioScreen(manifest, activeScreenId);
|
|
220
434
|
if (!screen)
|
|
221
435
|
return null;
|
|
222
436
|
const insertion = insertNodeAtPlacement({
|
|
@@ -245,7 +459,7 @@ export function moveStudioManifestNodeToPlacement(args) {
|
|
|
245
459
|
const { manifest, activeScreenId, nodeId, placement, componentMeta } = args;
|
|
246
460
|
if (!activeScreenId)
|
|
247
461
|
return null;
|
|
248
|
-
const screen = manifest
|
|
462
|
+
const screen = resolveCanonicalStudioScreen(manifest, activeScreenId);
|
|
249
463
|
if (!screen)
|
|
250
464
|
return null;
|
|
251
465
|
const movement = moveNodeToPlacement({
|
|
@@ -420,7 +634,10 @@ export function findRoutesAtParentPath(routes, parentPath) {
|
|
|
420
634
|
const [segment, ...rest] = parentPath;
|
|
421
635
|
if (!segment)
|
|
422
636
|
return null;
|
|
423
|
-
const
|
|
637
|
+
const matches = routes.filter((item) => item.name === segment);
|
|
638
|
+
if (matches.length !== 1)
|
|
639
|
+
return null;
|
|
640
|
+
const [route] = matches;
|
|
424
641
|
if (!route?.navigator?.routes)
|
|
425
642
|
return null;
|
|
426
643
|
return findRoutesAtParentPath(route.navigator.routes, rest);
|
|
@@ -429,8 +646,12 @@ export function insertRouteAtParentPath(routes, parentPath, newRoute) {
|
|
|
429
646
|
if (parentPath.length === 0)
|
|
430
647
|
return [...routes, newRoute];
|
|
431
648
|
const [segment, ...rest] = parentPath;
|
|
649
|
+
const matches = routes.filter((route) => route.name === segment && route.navigator);
|
|
650
|
+
if (matches.length !== 1)
|
|
651
|
+
return routes;
|
|
652
|
+
const [targetRoute] = matches;
|
|
432
653
|
return routes.map((route) => {
|
|
433
|
-
if (route
|
|
654
|
+
if (route !== targetRoute || !route.navigator?.routes)
|
|
434
655
|
return route;
|
|
435
656
|
return {
|
|
436
657
|
...route,
|
|
@@ -447,7 +668,10 @@ export function findNavigatorAtPath(navigator, parentPath) {
|
|
|
447
668
|
const [segment, ...rest] = parentPath;
|
|
448
669
|
if (!segment)
|
|
449
670
|
return null;
|
|
450
|
-
const
|
|
671
|
+
const matches = navigator.routes.filter((item) => item.name === segment);
|
|
672
|
+
if (matches.length !== 1)
|
|
673
|
+
return null;
|
|
674
|
+
const [route] = matches;
|
|
451
675
|
if (!route?.navigator)
|
|
452
676
|
return null;
|
|
453
677
|
return findNavigatorAtPath(route.navigator, rest);
|
|
@@ -456,10 +680,14 @@ export function updateNavigatorAtPath(navigator, parentPath, updater) {
|
|
|
456
680
|
if (parentPath.length === 0)
|
|
457
681
|
return updater(navigator);
|
|
458
682
|
const [segment, ...rest] = parentPath;
|
|
683
|
+
const matches = navigator.routes.filter((route) => route.name === segment && route.navigator);
|
|
684
|
+
if (matches.length !== 1)
|
|
685
|
+
return navigator;
|
|
686
|
+
const [targetRoute] = matches;
|
|
459
687
|
return {
|
|
460
688
|
...navigator,
|
|
461
689
|
routes: navigator.routes.map((route) => {
|
|
462
|
-
if (route
|
|
690
|
+
if (route !== targetRoute || !route.navigator)
|
|
463
691
|
return route;
|
|
464
692
|
return {
|
|
465
693
|
...route,
|
|
@@ -469,6 +697,8 @@ export function updateNavigatorAtPath(navigator, parentPath, updater) {
|
|
|
469
697
|
};
|
|
470
698
|
}
|
|
471
699
|
export function setStudioManifestNavigatorType(manifest, type) {
|
|
700
|
+
if (!NAVIGATOR_TYPES.includes(type))
|
|
701
|
+
return manifest;
|
|
472
702
|
const primaryNavigatorPath = getPrimaryNavigatorPath(manifest.navigator.routes);
|
|
473
703
|
const currentNavigator = findNavigatorAtPath(manifest.navigator, primaryNavigatorPath);
|
|
474
704
|
if (!currentNavigator || currentNavigator.type === type)
|
|
@@ -489,8 +719,9 @@ export function setStudioManifestNavigatorInitialRoute(manifest, routeName) {
|
|
|
489
719
|
const currentNavigator = findNavigatorAtPath(manifest.navigator, primaryNavigatorPath);
|
|
490
720
|
if (!currentNavigator)
|
|
491
721
|
return manifest;
|
|
492
|
-
if (
|
|
722
|
+
if (currentNavigator.routes.filter((route) => route.name === normalizedRoute).length !== 1) {
|
|
493
723
|
return manifest;
|
|
724
|
+
}
|
|
494
725
|
if (currentNavigator.initialRouteName === normalizedRoute)
|
|
495
726
|
return manifest;
|
|
496
727
|
return {
|
|
@@ -501,26 +732,82 @@ export function setStudioManifestNavigatorInitialRoute(manifest, routeName) {
|
|
|
501
732
|
})),
|
|
502
733
|
};
|
|
503
734
|
}
|
|
735
|
+
export function setStudioManifestRoutePrimaryNavigationVisibility(args) {
|
|
736
|
+
const { manifest, parentPath, routeName, showInPrimaryNavigation } = args;
|
|
737
|
+
const navigator = findNavigatorAtPath(manifest.navigator, parentPath);
|
|
738
|
+
if (!navigator)
|
|
739
|
+
return manifest;
|
|
740
|
+
const matches = navigator.routes.filter((route) => route.name === routeName);
|
|
741
|
+
if (matches.length !== 1)
|
|
742
|
+
return manifest;
|
|
743
|
+
const [matchedRoute] = matches;
|
|
744
|
+
if (!matchedRoute)
|
|
745
|
+
return manifest;
|
|
746
|
+
const alreadyCanonical = showInPrimaryNavigation
|
|
747
|
+
? matchedRoute.showInPrimaryNavigation === undefined
|
|
748
|
+
: matchedRoute.showInPrimaryNavigation === false;
|
|
749
|
+
if (alreadyCanonical)
|
|
750
|
+
return manifest;
|
|
751
|
+
return {
|
|
752
|
+
...manifest,
|
|
753
|
+
navigator: updateNavigatorAtPath(manifest.navigator, parentPath, (current) => ({
|
|
754
|
+
...current,
|
|
755
|
+
routes: current.routes.map((route) => {
|
|
756
|
+
if (route !== matchedRoute)
|
|
757
|
+
return route;
|
|
758
|
+
if (!showInPrimaryNavigation) {
|
|
759
|
+
return { ...route, showInPrimaryNavigation: false };
|
|
760
|
+
}
|
|
761
|
+
const { showInPrimaryNavigation: _visibility, ...visibleRoute } = route;
|
|
762
|
+
return visibleRoute;
|
|
763
|
+
}),
|
|
764
|
+
})),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
export function moveStudioManifestRoute(args) {
|
|
768
|
+
const { manifest, parentPath, routeName, toIndex } = args;
|
|
769
|
+
if (!Number.isInteger(toIndex))
|
|
770
|
+
return manifest;
|
|
771
|
+
const navigator = findNavigatorAtPath(manifest.navigator, parentPath);
|
|
772
|
+
if (!navigator || toIndex < 0 || toIndex >= navigator.routes.length)
|
|
773
|
+
return manifest;
|
|
774
|
+
const matchingIndexes = navigator.routes.flatMap((route, index) => route.name === routeName ? [index] : []);
|
|
775
|
+
if (matchingIndexes.length !== 1)
|
|
776
|
+
return manifest;
|
|
777
|
+
const [fromIndex] = matchingIndexes;
|
|
778
|
+
if (fromIndex === undefined || fromIndex === toIndex)
|
|
779
|
+
return manifest;
|
|
780
|
+
return {
|
|
781
|
+
...manifest,
|
|
782
|
+
navigator: updateNavigatorAtPath(manifest.navigator, parentPath, (current) => {
|
|
783
|
+
const routes = [...current.routes];
|
|
784
|
+
const [route] = routes.splice(fromIndex, 1);
|
|
785
|
+
if (!route)
|
|
786
|
+
return current;
|
|
787
|
+
routes.splice(toIndex, 0, route);
|
|
788
|
+
return { ...current, routes };
|
|
789
|
+
}),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
504
792
|
export function addStudioManifestScreen(args) {
|
|
505
793
|
const { manifest, activeScreenId, createId = generateManifestStateId } = args;
|
|
794
|
+
if (!hasCanonicalStudioScreenRegistryIdentity(manifest.screens)) {
|
|
795
|
+
return { manifest, activeScreenId };
|
|
796
|
+
}
|
|
506
797
|
const trimmedName = args.name.trim();
|
|
507
798
|
if (!trimmedName)
|
|
508
799
|
return { manifest, activeScreenId };
|
|
509
800
|
const baseRouteName = normalizeRouteName(trimmedName);
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
? findRoutesAtParentPath(manifest.navigator.routes, activeParentPath)
|
|
516
|
-
: null;
|
|
517
|
-
const parentPath = activeParentPath && activeParentRoutes ? activeParentPath : fallbackParentPath;
|
|
518
|
-
const siblingRoutes = findRoutesAtParentPath(manifest.navigator.routes, parentPath) ?? [];
|
|
801
|
+
const parentPath = args.parentPath ?? getPrimaryNavigatorPath(manifest.navigator.routes);
|
|
802
|
+
const siblingRoutes = findRoutesAtParentPath(manifest.navigator.routes, parentPath);
|
|
803
|
+
if (!siblingRoutes)
|
|
804
|
+
return { manifest, activeScreenId };
|
|
805
|
+
const existingScreenIds = new Set(Object.values(manifest.screens).map((screen) => screen.id));
|
|
519
806
|
let screenId = createId('Screen');
|
|
520
|
-
while (manifest.screens[screenId]) {
|
|
807
|
+
while (manifest.screens[screenId] || existingScreenIds.has(screenId)) {
|
|
521
808
|
screenId = createId('Screen');
|
|
522
809
|
}
|
|
523
|
-
const existingPatterns = new Set(
|
|
810
|
+
const existingPatterns = new Set(collectCanonicalRoutePatterns(manifest.navigator.routes));
|
|
524
811
|
const routeName = makeUniqueRouteNameForParent(baseRouteName, siblingRoutes, parentPath, existingPatterns);
|
|
525
812
|
const newScreen = {
|
|
526
813
|
id: screenId,
|
|
@@ -548,46 +835,33 @@ export function addStudioManifestScreen(args) {
|
|
|
548
835
|
};
|
|
549
836
|
}
|
|
550
837
|
export function deleteStudioManifestScreen(manifest, screenId, activeScreenId) {
|
|
838
|
+
if (!hasCanonicalStudioScreenRegistryIdentity(manifest.screens)) {
|
|
839
|
+
return { manifest, activeScreenId };
|
|
840
|
+
}
|
|
551
841
|
if (Object.keys(manifest.screens).length <= 1)
|
|
552
842
|
return { manifest, activeScreenId };
|
|
843
|
+
const deletedScreen = manifest.screens[screenId];
|
|
844
|
+
if (!deletedScreen)
|
|
845
|
+
return { manifest, activeScreenId };
|
|
553
846
|
const { [screenId]: _deletedScreen, ...remainingScreens } = manifest.screens;
|
|
554
847
|
const remainingScreenIds = Object.keys(remainingScreens);
|
|
555
|
-
|
|
556
|
-
if (safeRoutes.length === 0 && remainingScreenIds.length > 0) {
|
|
557
|
-
const [fallbackScreenId] = remainingScreenIds;
|
|
558
|
-
if (!fallbackScreenId)
|
|
559
|
-
return { manifest, activeScreenId };
|
|
560
|
-
const fallbackScreen = remainingScreens[fallbackScreenId];
|
|
561
|
-
safeRoutes = [
|
|
562
|
-
{
|
|
563
|
-
name: makeUniqueSiblingRouteName('screen', []),
|
|
564
|
-
label: fallbackScreen?.name ?? 'Screen',
|
|
565
|
-
screenId: fallbackScreenId,
|
|
566
|
-
},
|
|
567
|
-
];
|
|
568
|
-
}
|
|
848
|
+
const safeRoutes = removeScreenIdFromRoutes(manifest.navigator.routes, screenId);
|
|
569
849
|
const orderedScreenIds = listScreenIdsInRouteOrder(safeRoutes).filter((id) => !!remainingScreens[id]);
|
|
570
850
|
const nextActiveScreenId = !activeScreenId || activeScreenId === screenId || !remainingScreens[activeScreenId]
|
|
571
851
|
? (orderedScreenIds[0] ?? remainingScreenIds[0] ?? null)
|
|
572
852
|
: activeScreenId;
|
|
853
|
+
const deletedNodeIds = collectNodeIds(deletedScreen.root);
|
|
854
|
+
const dataBindings = Object.fromEntries(Object.entries(manifest.dataBindings ?? {}).filter(([componentId, binding]) => !deletedNodeIds.has(componentId) && !deletedNodeIds.has(binding.componentId)));
|
|
573
855
|
return {
|
|
574
856
|
activeScreenId: nextActiveScreenId,
|
|
575
857
|
manifest: {
|
|
576
858
|
...manifest,
|
|
859
|
+
dataBindings,
|
|
577
860
|
screens: remainingScreens,
|
|
578
|
-
navigator: {
|
|
861
|
+
navigator: normalizeNavigatorAfterRouteUpdate({
|
|
579
862
|
...manifest.navigator,
|
|
580
863
|
routes: safeRoutes,
|
|
581
|
-
},
|
|
582
|
-
},
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
export function reorderStudioManifestScreens(manifest, newRoutes) {
|
|
586
|
-
return {
|
|
587
|
-
...manifest,
|
|
588
|
-
navigator: {
|
|
589
|
-
...manifest.navigator,
|
|
590
|
-
routes: newRoutes,
|
|
864
|
+
}),
|
|
591
865
|
},
|
|
592
866
|
};
|
|
593
867
|
}
|
|
@@ -653,6 +927,17 @@ function normalizeRouteName(value) {
|
|
|
653
927
|
.replace(/[^a-z0-9]+/g, '-')
|
|
654
928
|
.replace(/^-+|-+$/g, '') || 'screen');
|
|
655
929
|
}
|
|
930
|
+
function collectCanonicalRoutePatterns(routes, runtimePathPrefix = []) {
|
|
931
|
+
const patterns = [];
|
|
932
|
+
for (const route of routes) {
|
|
933
|
+
const runtimePath = appendRuntimeRoutePath(runtimePathPrefix, route);
|
|
934
|
+
patterns.push(toCanonicalRoutePattern(runtimePath));
|
|
935
|
+
if (route.navigator) {
|
|
936
|
+
patterns.push(...collectCanonicalRoutePatterns(route.navigator.routes, runtimePath));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return patterns;
|
|
940
|
+
}
|
|
656
941
|
function cloneNodeWithNewIds(node, createId) {
|
|
657
942
|
const clonedNode = {
|
|
658
943
|
...node,
|