@mintlify/common 1.0.1024 → 1.0.1025
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/dist/navigation/scopeNavStateFlip.test.d.ts +1 -0
- package/dist/navigation/scopeNavStateFlip.test.js +567 -0
- package/dist/navigation/scopeNavToPath.js +51 -4
- package/dist/navigation/updateFirstHrefInDivision.d.ts +2 -1
- package/dist/navigation/updateFirstHrefInDivision.js +7 -2
- package/dist/navigation/updateFirstHrefInDivision.test.d.ts +1 -0
- package/dist/navigation/updateFirstHrefInDivision.test.js +52 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { isEqualIgnoringLeadingSlash } from '../paths/isEqualIgnoringLeadingSlash.js';
|
|
2
|
+
import { decoratedNavigationConfig } from './decoratedNavigationConfig.js';
|
|
3
|
+
import { filterDivisions } from './filterDivisions.js';
|
|
4
|
+
import { getAllPathsInDecoratedNav } from './getAllPathsInDecoratedNav.js';
|
|
5
|
+
import { getScopedNavForPath } from './scopeNavToPath.js';
|
|
6
|
+
function filterWith(decoratedNav, currentPath, currentVersion, currentLanguage, currentProduct) {
|
|
7
|
+
return filterDivisions({
|
|
8
|
+
currentPath,
|
|
9
|
+
decoratedNav,
|
|
10
|
+
userGroups: new Set(),
|
|
11
|
+
currentVersion,
|
|
12
|
+
currentLanguage,
|
|
13
|
+
currentProduct,
|
|
14
|
+
shouldUseDivisionMatch: true,
|
|
15
|
+
cache: { dropdownCache: new Map() },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function divisionSummary(result) {
|
|
19
|
+
return {
|
|
20
|
+
tabs: result.tabs.map(({ name, isActive }) => ({ name, isActive })),
|
|
21
|
+
anchors: result.anchors.map(({ name, isActive }) => ({ name, isActive })),
|
|
22
|
+
versions: result.versions.map(({ name, isActive }) => ({ name, isActive })),
|
|
23
|
+
languages: result.languages.map(({ name, isActive }) => ({ name, isActive })),
|
|
24
|
+
dropdowns: result.dropdowns.map(({ name, isActive }) => ({ name, isActive })),
|
|
25
|
+
products: result.products.map(({ name, isActive }) => ({ name, isActive })),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The correctness property behind same-url division switches: for every path
|
|
30
|
+
* and every selectable version/language, EITHER the precomputed jump target
|
|
31
|
+
* navigates away (the destination's own payload takes over) OR the switch
|
|
32
|
+
* keeps the url — in which case the client re-renders the scoped tree with
|
|
33
|
+
* flipped session state, and that render must match the full nav exactly.
|
|
34
|
+
*/
|
|
35
|
+
function assertStateFlipSafety(nav) {
|
|
36
|
+
const paths = getAllPathsInDecoratedNav(nav);
|
|
37
|
+
expect(paths.length).toBeGreaterThan(0);
|
|
38
|
+
for (const path of paths) {
|
|
39
|
+
const currentPath = `/${path.replace(/^\//, '')}`;
|
|
40
|
+
const scoped = getScopedNavForPath({ decoratedNav: nav, currentPath });
|
|
41
|
+
const full = filterWith(nav, currentPath, scoped.currentVersion, scoped.currentLanguage);
|
|
42
|
+
const rescoped = filterWith(scoped.scopedNav, currentPath, scoped.currentVersion, scoped.currentLanguage);
|
|
43
|
+
// the picker only offers divisions present in the chrome, so parity is
|
|
44
|
+
// asserted for those; full-nav maps can carry unreachable entries from
|
|
45
|
+
// other products' divisions that stubs rightly no longer contribute
|
|
46
|
+
const selectableVersions = new Set(full.versions.map(({ name }) => name));
|
|
47
|
+
const selectableLanguages = new Set(full.languages.map(({ language }) => language));
|
|
48
|
+
const selectable = (map, keys) => new Map([...map.entries()].filter(([key]) => keys.has(key)).map(([k, v]) => [k, v.href]));
|
|
49
|
+
expect(selectable(rescoped.firstHrefInVersion, selectableVersions), currentPath).toEqual(selectable(full.firstHrefInVersion, selectableVersions));
|
|
50
|
+
expect(selectable(rescoped.firstHrefInLanguage, selectableLanguages), currentPath).toEqual(selectable(full.firstHrefInLanguage, selectableLanguages));
|
|
51
|
+
for (const version of full.versions) {
|
|
52
|
+
const target = full.firstHrefInVersion.get(version.name);
|
|
53
|
+
const stays = target !== undefined && isEqualIgnoringLeadingSlash(target.href, currentPath);
|
|
54
|
+
if (!stays)
|
|
55
|
+
continue;
|
|
56
|
+
const label = `${currentPath} -> version ${version.name}`;
|
|
57
|
+
const flippedFull = filterWith(nav, currentPath, version.name, scoped.currentLanguage);
|
|
58
|
+
const flippedScoped = filterWith(scoped.scopedNav, currentPath, version.name, scoped.currentLanguage);
|
|
59
|
+
expect(flippedScoped.groupsOrPages, label).toEqual(flippedFull.groupsOrPages);
|
|
60
|
+
expect(divisionSummary(flippedScoped), label).toEqual(divisionSummary(flippedFull));
|
|
61
|
+
}
|
|
62
|
+
// deliberately unguarded (no stays check): even a flip to a stubbed,
|
|
63
|
+
// non-containing product must render identically, because the sidebar is
|
|
64
|
+
// path-driven and the selector chrome survives in product stubs
|
|
65
|
+
for (const product of full.products) {
|
|
66
|
+
const label = `${currentPath} -> product ${product.name}`;
|
|
67
|
+
const flippedFull = filterWith(nav, currentPath, scoped.currentVersion, scoped.currentLanguage, product.name);
|
|
68
|
+
const flippedScoped = filterWith(scoped.scopedNav, currentPath, scoped.currentVersion, scoped.currentLanguage, product.name);
|
|
69
|
+
expect(flippedScoped.groupsOrPages, label).toEqual(flippedFull.groupsOrPages);
|
|
70
|
+
expect(divisionSummary(flippedScoped), label).toEqual(divisionSummary(flippedFull));
|
|
71
|
+
}
|
|
72
|
+
for (const language of full.languages) {
|
|
73
|
+
const target = full.firstHrefInLanguage.get(language.language);
|
|
74
|
+
const stays = target !== undefined && isEqualIgnoringLeadingSlash(target.href, currentPath);
|
|
75
|
+
if (!stays)
|
|
76
|
+
continue;
|
|
77
|
+
const label = `${currentPath} -> language ${language.language}`;
|
|
78
|
+
const flippedFull = filterWith(nav, currentPath, scoped.currentVersion, language.language);
|
|
79
|
+
const flippedScoped = filterWith(scoped.scopedNav, currentPath, scoped.currentVersion, language.language);
|
|
80
|
+
expect(flippedScoped.groupsOrPages, label).toEqual(flippedFull.groupsOrPages);
|
|
81
|
+
expect(divisionSummary(flippedScoped), label).toEqual(divisionSummary(flippedFull));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function versionWithTabs(version, isDefault, sharedPages) {
|
|
86
|
+
return Object.assign(Object.assign({ version }, (isDefault ? { default: true } : {})), { tabs: [
|
|
87
|
+
{
|
|
88
|
+
tab: 'Docs',
|
|
89
|
+
pages: [
|
|
90
|
+
...sharedPages.map((href) => ({ title: `Shared ${href}`, href })),
|
|
91
|
+
{ title: 'Overview', href: `/docs/${version}/overview` },
|
|
92
|
+
{ title: 'Install', href: `/docs/${version}/install` },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
tab: 'Reference',
|
|
97
|
+
pages: [
|
|
98
|
+
{ title: 'API', href: `/docs/${version}/reference/api` },
|
|
99
|
+
{ title: 'SQL', href: `/docs/${version}/reference/sql` },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
] });
|
|
103
|
+
}
|
|
104
|
+
// cockroachlabs shape: products wrap version divisions, the docs home and a
|
|
105
|
+
// molt-style tool tab are duplicated into every version at identical hrefs,
|
|
106
|
+
// and a second product has no versions at all
|
|
107
|
+
const productSharedNav = {
|
|
108
|
+
products: [
|
|
109
|
+
{
|
|
110
|
+
product: 'Database',
|
|
111
|
+
versions: [
|
|
112
|
+
{
|
|
113
|
+
version: 'v26',
|
|
114
|
+
default: true,
|
|
115
|
+
tabs: [
|
|
116
|
+
{
|
|
117
|
+
tab: 'Docs',
|
|
118
|
+
pages: [
|
|
119
|
+
{ title: 'Home', href: '/docs' },
|
|
120
|
+
{ title: 'Overview', href: '/docs/v26/overview' },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
tab: 'Migrate',
|
|
125
|
+
pages: [
|
|
126
|
+
{ title: 'Migration Overview', href: '/docs/molt/overview' },
|
|
127
|
+
{ title: 'Bulk Load', href: '/docs/molt/bulk-load' },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
version: 'v25',
|
|
134
|
+
tabs: [
|
|
135
|
+
{
|
|
136
|
+
tab: 'Docs',
|
|
137
|
+
pages: [
|
|
138
|
+
{ title: 'Home', href: '/docs' },
|
|
139
|
+
{ title: 'Overview', href: '/docs/v25/overview' },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
tab: 'Migrate',
|
|
144
|
+
pages: [
|
|
145
|
+
{ title: 'Migration Overview', href: '/docs/molt/overview' },
|
|
146
|
+
{ title: 'Bulk Load', href: '/docs/molt/bulk-load' },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
product: 'Cloud',
|
|
155
|
+
tabs: [
|
|
156
|
+
{
|
|
157
|
+
tab: 'Deploy',
|
|
158
|
+
pages: [
|
|
159
|
+
{ title: 'Get Started', href: '/docs/cloud/get-started' },
|
|
160
|
+
{ title: 'Clusters', href: '/docs/cloud/clusters' },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
// the shared home exists in v26 and v25 but NOT v24: switching to v24 from it
|
|
168
|
+
// must navigate (into v24, per the unversioned-fallback fix) instead of
|
|
169
|
+
// staying on a url v24 cannot render
|
|
170
|
+
const partialSharedNav = {
|
|
171
|
+
versions: [
|
|
172
|
+
versionWithTabs('v26', true, ['/docs']),
|
|
173
|
+
versionWithTabs('v25', false, ['/docs']),
|
|
174
|
+
versionWithTabs('v24', false, []),
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
// pages that live OUTSIDE every version division: their sidebar never derives
|
|
178
|
+
// from a version division, so stubs are safe there — pinned by the harness
|
|
179
|
+
const unversionedPagesNav = {
|
|
180
|
+
global: {
|
|
181
|
+
anchors: [{ anchor: 'External', href: 'https://example.com' }],
|
|
182
|
+
},
|
|
183
|
+
tabs: [
|
|
184
|
+
{
|
|
185
|
+
tab: 'Company',
|
|
186
|
+
pages: [
|
|
187
|
+
{ title: 'About', href: '/docs/about' },
|
|
188
|
+
{ title: 'Pricing', href: '/docs/pricing' },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
tab: 'Product',
|
|
193
|
+
versions: [
|
|
194
|
+
versionWithTabs('v2', true, ['/docs/shared-note']),
|
|
195
|
+
versionWithTabs('v1', false, ['/docs/shared-note']),
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
// a page shared across language divisions at the same href — the language
|
|
201
|
+
// analog of the cockroachlabs home page
|
|
202
|
+
const languageSharedNav = {
|
|
203
|
+
languages: [
|
|
204
|
+
{
|
|
205
|
+
language: 'en',
|
|
206
|
+
groups: [
|
|
207
|
+
{
|
|
208
|
+
group: 'Guides',
|
|
209
|
+
pages: [
|
|
210
|
+
{ title: 'Legal', href: '/docs/legal' },
|
|
211
|
+
{ title: 'Overview', href: '/docs/en/overview' },
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
language: 'es',
|
|
218
|
+
groups: [
|
|
219
|
+
{
|
|
220
|
+
group: 'Guides',
|
|
221
|
+
pages: [
|
|
222
|
+
{ title: 'Legal', href: '/docs/legal' },
|
|
223
|
+
{ title: 'Overview', href: '/docs/es/overview' },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
// sharing at BOTH nesting levels: the home page exists in every version and,
|
|
231
|
+
// within each version, in every language
|
|
232
|
+
const nestedSharedNav = {
|
|
233
|
+
versions: [
|
|
234
|
+
{
|
|
235
|
+
version: 'v2',
|
|
236
|
+
default: true,
|
|
237
|
+
languages: [
|
|
238
|
+
{
|
|
239
|
+
language: 'en',
|
|
240
|
+
groups: [
|
|
241
|
+
{
|
|
242
|
+
group: 'Guides',
|
|
243
|
+
pages: [
|
|
244
|
+
{ title: 'Home', href: '/docs' },
|
|
245
|
+
{ title: 'Overview', href: '/docs/v2/en/overview' },
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
language: 'es',
|
|
252
|
+
groups: [
|
|
253
|
+
{
|
|
254
|
+
group: 'Guides',
|
|
255
|
+
pages: [
|
|
256
|
+
{ title: 'Home', href: '/docs' },
|
|
257
|
+
{ title: 'Overview', href: '/docs/v2/es/overview' },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
version: 'v1',
|
|
266
|
+
languages: [
|
|
267
|
+
{
|
|
268
|
+
language: 'en',
|
|
269
|
+
groups: [
|
|
270
|
+
{
|
|
271
|
+
group: 'Guides',
|
|
272
|
+
pages: [
|
|
273
|
+
{ title: 'Home', href: '/docs' },
|
|
274
|
+
{ title: 'Overview', href: '/docs/v1/en/overview' },
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
language: 'es',
|
|
281
|
+
groups: [
|
|
282
|
+
{
|
|
283
|
+
group: 'Guides',
|
|
284
|
+
pages: [
|
|
285
|
+
{ title: 'Home', href: '/docs' },
|
|
286
|
+
{ title: 'Overview', href: '/docs/v1/es/overview' },
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
};
|
|
295
|
+
// a tool page listed in BOTH products at the same href, plus product menus
|
|
296
|
+
const ambiguousProductNav = {
|
|
297
|
+
products: [
|
|
298
|
+
{
|
|
299
|
+
product: 'Server',
|
|
300
|
+
menu: [
|
|
301
|
+
{
|
|
302
|
+
item: 'Guides',
|
|
303
|
+
pages: [
|
|
304
|
+
{ title: 'Install', href: '/docs/server/install' },
|
|
305
|
+
{ title: 'Configure', href: '/docs/server/configure' },
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
tabs: [
|
|
310
|
+
{
|
|
311
|
+
tab: 'Docs',
|
|
312
|
+
pages: [
|
|
313
|
+
{ title: 'Shared Tool', href: '/docs/shared-tool' },
|
|
314
|
+
{ title: 'Install', href: '/docs/server/install' },
|
|
315
|
+
{ title: 'Configure', href: '/docs/server/configure' },
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
product: 'Cloud',
|
|
322
|
+
menu: [
|
|
323
|
+
{
|
|
324
|
+
item: 'Guides',
|
|
325
|
+
pages: [
|
|
326
|
+
{ title: 'Sign Up', href: '/docs/cloud/sign-up' },
|
|
327
|
+
{ title: 'Billing', href: '/docs/cloud/billing' },
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
tabs: [
|
|
332
|
+
{
|
|
333
|
+
tab: 'Docs',
|
|
334
|
+
pages: [
|
|
335
|
+
{ title: 'Shared Tool', href: '/docs/shared-tool' },
|
|
336
|
+
{ title: 'Sign Up', href: '/docs/cloud/sign-up' },
|
|
337
|
+
{ title: 'Billing', href: '/docs/cloud/billing' },
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
// pages beside the products array, outside every product
|
|
345
|
+
const outsideProductsNav = {
|
|
346
|
+
tabs: [
|
|
347
|
+
{
|
|
348
|
+
tab: 'Company',
|
|
349
|
+
pages: [
|
|
350
|
+
{ title: 'About', href: '/docs/about' },
|
|
351
|
+
{ title: 'Careers', href: '/docs/careers' },
|
|
352
|
+
],
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
tab: 'Product',
|
|
356
|
+
products: [
|
|
357
|
+
{
|
|
358
|
+
product: 'Server',
|
|
359
|
+
pages: [
|
|
360
|
+
{ title: 'Install', href: '/docs/server/install' },
|
|
361
|
+
{ title: 'Configure', href: '/docs/server/configure' },
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
product: 'Cloud',
|
|
366
|
+
pages: [
|
|
367
|
+
{ title: 'Sign Up', href: '/docs/cloud/sign-up' },
|
|
368
|
+
{ title: 'Billing', href: '/docs/cloud/billing' },
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
};
|
|
375
|
+
function tripleLanguage(product, version, language, shared) {
|
|
376
|
+
return {
|
|
377
|
+
language,
|
|
378
|
+
groups: [
|
|
379
|
+
{
|
|
380
|
+
group: 'Guides',
|
|
381
|
+
pages: [
|
|
382
|
+
...shared.map((href) => ({ title: `Shared ${href}`, href })),
|
|
383
|
+
{ title: 'Overview', href: `/docs/${product}/${version}/${language}/overview` },
|
|
384
|
+
{ title: 'Install', href: `/docs/${product}/${version}/${language}/install` },
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
// all three axes nested: products -> versions -> languages, with the home
|
|
391
|
+
// page shared across every version and language of the first product
|
|
392
|
+
const tripleNestedNav = {
|
|
393
|
+
products: [
|
|
394
|
+
{
|
|
395
|
+
product: 'Server',
|
|
396
|
+
versions: [
|
|
397
|
+
{
|
|
398
|
+
version: 'v2',
|
|
399
|
+
default: true,
|
|
400
|
+
languages: [
|
|
401
|
+
tripleLanguage('server', 'v2', 'en', ['/docs', '/docs/all-shared']),
|
|
402
|
+
tripleLanguage('server', 'v2', 'es', ['/docs', '/docs/all-shared']),
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
version: 'v1',
|
|
407
|
+
languages: [
|
|
408
|
+
tripleLanguage('server', 'v1', 'en', ['/docs', '/docs/all-shared']),
|
|
409
|
+
tripleLanguage('server', 'v1', 'es', ['/docs', '/docs/all-shared']),
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
product: 'Cloud',
|
|
416
|
+
versions: [
|
|
417
|
+
{
|
|
418
|
+
version: 'v2',
|
|
419
|
+
default: true,
|
|
420
|
+
languages: [
|
|
421
|
+
tripleLanguage('cloud', 'v2', 'en', ['/docs/all-shared']),
|
|
422
|
+
tripleLanguage('cloud', 'v2', 'es', ['/docs/all-shared']),
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
};
|
|
429
|
+
describe('state-flip safety under scoped nav', () => {
|
|
430
|
+
it('holds for the versioned fixture corpus', () => {
|
|
431
|
+
assertStateFlipSafety(decoratedNavigationConfig);
|
|
432
|
+
});
|
|
433
|
+
it('holds for products wrapping versions with shared home and tool pages', () => {
|
|
434
|
+
assertStateFlipSafety(productSharedNav);
|
|
435
|
+
});
|
|
436
|
+
it('holds when the shared page exists in only some versions', () => {
|
|
437
|
+
assertStateFlipSafety(partialSharedNav);
|
|
438
|
+
});
|
|
439
|
+
it('holds for pages outside every version division', () => {
|
|
440
|
+
assertStateFlipSafety(unversionedPagesNav);
|
|
441
|
+
});
|
|
442
|
+
it('holds for pages shared across language divisions', () => {
|
|
443
|
+
assertStateFlipSafety(languageSharedNav);
|
|
444
|
+
});
|
|
445
|
+
it('holds for a page shared across products', () => {
|
|
446
|
+
assertStateFlipSafety(ambiguousProductNav);
|
|
447
|
+
});
|
|
448
|
+
it('holds for pages outside every product', () => {
|
|
449
|
+
assertStateFlipSafety(outsideProductsNav);
|
|
450
|
+
});
|
|
451
|
+
it('holds for pages shared at both version and language level', () => {
|
|
452
|
+
assertStateFlipSafety(nestedSharedNav);
|
|
453
|
+
});
|
|
454
|
+
it('holds for products wrapping versions wrapping languages', () => {
|
|
455
|
+
assertStateFlipSafety(tripleNestedNav);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
describe('triple-nested stripping', () => {
|
|
459
|
+
it('strips each axis independently on an owned page', () => {
|
|
460
|
+
const scoped = getScopedNavForPath({
|
|
461
|
+
decoratedNav: tripleNestedNav,
|
|
462
|
+
currentPath: '/docs/server/v2/en/overview',
|
|
463
|
+
});
|
|
464
|
+
const serialized = JSON.stringify(scoped.scopedNav);
|
|
465
|
+
expect(serialized).toContain('/docs/server/v2/en/install');
|
|
466
|
+
expect(serialized).not.toContain('/docs/server/v2/es/install');
|
|
467
|
+
expect(serialized).not.toContain('/docs/server/v1/en/install');
|
|
468
|
+
expect(serialized).not.toContain('/docs/cloud/v2/en/install');
|
|
469
|
+
});
|
|
470
|
+
it('keeps every division on a page shared across products, versions, and languages at once', () => {
|
|
471
|
+
const scoped = getScopedNavForPath({
|
|
472
|
+
decoratedNav: tripleNestedNav,
|
|
473
|
+
currentPath: '/docs/all-shared',
|
|
474
|
+
});
|
|
475
|
+
const serialized = JSON.stringify(scoped.scopedNav);
|
|
476
|
+
for (const href of [
|
|
477
|
+
'/docs/server/v2/en/install',
|
|
478
|
+
'/docs/server/v2/es/install',
|
|
479
|
+
'/docs/server/v1/en/install',
|
|
480
|
+
'/docs/server/v1/es/install',
|
|
481
|
+
'/docs/cloud/v2/en/install',
|
|
482
|
+
'/docs/cloud/v2/es/install',
|
|
483
|
+
]) {
|
|
484
|
+
expect(serialized, href).toContain(href);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
it('keeps every version-language pair containing the shared home, still stubbing the other product', () => {
|
|
488
|
+
const scoped = getScopedNavForPath({ decoratedNav: tripleNestedNav, currentPath: '/docs' });
|
|
489
|
+
const serialized = JSON.stringify(scoped.scopedNav);
|
|
490
|
+
for (const href of [
|
|
491
|
+
'/docs/server/v2/en/install',
|
|
492
|
+
'/docs/server/v2/es/install',
|
|
493
|
+
'/docs/server/v1/en/install',
|
|
494
|
+
'/docs/server/v1/es/install',
|
|
495
|
+
]) {
|
|
496
|
+
expect(serialized, href).toContain(href);
|
|
497
|
+
}
|
|
498
|
+
expect(serialized).toContain('"product":"Cloud"');
|
|
499
|
+
expect(serialized).not.toContain('/docs/cloud/v2/en/install');
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
describe('shared-page scoping details', () => {
|
|
503
|
+
it('keeps both containing versions through the product wrapper on the shared home', () => {
|
|
504
|
+
const scoped = getScopedNavForPath({ decoratedNav: productSharedNav, currentPath: '/docs' });
|
|
505
|
+
const serialized = JSON.stringify(scoped.scopedNav);
|
|
506
|
+
expect(serialized).toContain('/docs/v26/overview');
|
|
507
|
+
expect(serialized).toContain('/docs/v25/overview');
|
|
508
|
+
expect(serialized).toContain('/docs/cloud/get-started');
|
|
509
|
+
});
|
|
510
|
+
it('stubs the non-containing product down to selector chrome', () => {
|
|
511
|
+
const scoped = getScopedNavForPath({ decoratedNav: productSharedNav, currentPath: '/docs' });
|
|
512
|
+
const serialized = JSON.stringify(scoped.scopedNav);
|
|
513
|
+
expect(serialized).toContain('"product":"Cloud"');
|
|
514
|
+
expect(serialized).toContain('/docs/cloud/get-started');
|
|
515
|
+
expect(serialized).not.toContain('/docs/cloud/clusters');
|
|
516
|
+
});
|
|
517
|
+
it('keeps every product containing an ambiguous page', () => {
|
|
518
|
+
const scoped = getScopedNavForPath({
|
|
519
|
+
decoratedNav: ambiguousProductNav,
|
|
520
|
+
currentPath: '/docs/shared-tool',
|
|
521
|
+
});
|
|
522
|
+
const serialized = JSON.stringify(scoped.scopedNav);
|
|
523
|
+
expect(serialized).toContain('/docs/server/configure');
|
|
524
|
+
expect(serialized).toContain('/docs/cloud/billing');
|
|
525
|
+
});
|
|
526
|
+
it('preserves a stubbed product menu with one resolved page per item', () => {
|
|
527
|
+
const scoped = getScopedNavForPath({
|
|
528
|
+
decoratedNav: ambiguousProductNav,
|
|
529
|
+
currentPath: '/docs/server/install',
|
|
530
|
+
});
|
|
531
|
+
const serialized = JSON.stringify(scoped.scopedNav);
|
|
532
|
+
expect(serialized).toContain('"item":"Guides"');
|
|
533
|
+
expect(serialized).toContain('/docs/cloud/sign-up');
|
|
534
|
+
expect(serialized).not.toContain('/docs/cloud/billing');
|
|
535
|
+
});
|
|
536
|
+
it('stubs no product when the path lives outside all of them', () => {
|
|
537
|
+
const scoped = getScopedNavForPath({
|
|
538
|
+
decoratedNav: outsideProductsNav,
|
|
539
|
+
currentPath: '/docs/about',
|
|
540
|
+
});
|
|
541
|
+
const serialized = JSON.stringify(scoped.scopedNav);
|
|
542
|
+
expect(serialized).toContain('/docs/server/configure');
|
|
543
|
+
expect(serialized).toContain('/docs/cloud/billing');
|
|
544
|
+
});
|
|
545
|
+
it('sends the switch into the target version when the shared page is absent there', () => {
|
|
546
|
+
const scoped = getScopedNavForPath({ decoratedNav: partialSharedNav, currentPath: '/docs' });
|
|
547
|
+
const target = scoped.firstHrefInVersion.get('v24');
|
|
548
|
+
expect(target).toBeDefined();
|
|
549
|
+
expect(target === null || target === void 0 ? void 0 : target.href).not.toBe('/docs');
|
|
550
|
+
expect(target === null || target === void 0 ? void 0 : target.href.startsWith('/docs/v24/')).toBe(true);
|
|
551
|
+
});
|
|
552
|
+
it('stubs the absent version division on the partially shared page', () => {
|
|
553
|
+
const scoped = getScopedNavForPath({ decoratedNav: partialSharedNav, currentPath: '/docs' });
|
|
554
|
+
const serialized = JSON.stringify(scoped.scopedNav);
|
|
555
|
+
expect(serialized).toContain('/docs/v25/overview');
|
|
556
|
+
expect(serialized).not.toContain('/docs/v24/reference/sql');
|
|
557
|
+
});
|
|
558
|
+
it('still stubs everything for a page owned by a single version', () => {
|
|
559
|
+
const scoped = getScopedNavForPath({
|
|
560
|
+
decoratedNav: partialSharedNav,
|
|
561
|
+
currentPath: '/docs/v26/reference/api',
|
|
562
|
+
});
|
|
563
|
+
const serialized = JSON.stringify(scoped.scopedNav);
|
|
564
|
+
expect(serialized).not.toContain('/docs/v25/reference/sql');
|
|
565
|
+
expect(serialized).not.toContain('/docs/v24/reference/sql');
|
|
566
|
+
});
|
|
567
|
+
});
|
|
@@ -327,17 +327,16 @@ function scopeDivisionArray(entries, key, ctx) {
|
|
|
327
327
|
return entries.map((entry) => (isNavNode(entry) ? scopeNode(entry, ctx) : entry));
|
|
328
328
|
}
|
|
329
329
|
const currentValue = key === 'versions' ? ctx.currentVersion : ctx.currentLanguage;
|
|
330
|
+
const containsPath = entries.map((entry) => isNavNode(entry) && divisionContainsPath(entry, ctx.currentPath));
|
|
330
331
|
let activeIndex = -1;
|
|
331
332
|
if (currentValue) {
|
|
332
|
-
activeIndex = entries.findIndex((entry) => isNavNode(entry) &&
|
|
333
|
-
divisionValue(entry, key) === currentValue &&
|
|
334
|
-
divisionContainsPath(entry, ctx.currentPath));
|
|
333
|
+
activeIndex = entries.findIndex((entry, index) => isNavNode(entry) && divisionValue(entry, key) === currentValue && containsPath[index]);
|
|
335
334
|
if (activeIndex === -1) {
|
|
336
335
|
activeIndex = entries.findIndex((entry) => isNavNode(entry) && divisionValue(entry, key) === currentValue);
|
|
337
336
|
}
|
|
338
337
|
}
|
|
339
338
|
if (activeIndex === -1) {
|
|
340
|
-
activeIndex =
|
|
339
|
+
activeIndex = containsPath.findIndex(Boolean);
|
|
341
340
|
}
|
|
342
341
|
if (activeIndex === -1) {
|
|
343
342
|
activeIndex = entries.findIndex((entry) => isNavNode(entry) && entry.default === true);
|
|
@@ -349,9 +348,54 @@ function scopeDivisionArray(entries, key, ctx) {
|
|
|
349
348
|
return entry;
|
|
350
349
|
if (index === activeIndex)
|
|
351
350
|
return scopeNode(entry, ctx);
|
|
351
|
+
// a shared page lives in several divisions at the same href; switching
|
|
352
|
+
// divisions there keeps the url, so the client re-renders from state and
|
|
353
|
+
// every containing division must stay a real (recursively scoped) tree —
|
|
354
|
+
// a stub would collapse the sidebar to its single switch-target page
|
|
355
|
+
if (containsPath[index])
|
|
356
|
+
return scopeNode(entry, ctx);
|
|
352
357
|
return stubDivision(entry, key, ctx);
|
|
353
358
|
});
|
|
354
359
|
}
|
|
360
|
+
function stubProduct(product, ctx) {
|
|
361
|
+
if (typeof product.href === 'string')
|
|
362
|
+
return product;
|
|
363
|
+
const target = resolveVisibleTarget(product, ctx);
|
|
364
|
+
if (!target)
|
|
365
|
+
return product;
|
|
366
|
+
const stub = {};
|
|
367
|
+
for (const [entryKey, value] of Object.entries(product)) {
|
|
368
|
+
if (entryKey === 'menu' && Array.isArray(value)) {
|
|
369
|
+
// the product selector renders every product's hover menu, active or not
|
|
370
|
+
stub.menu = value.map((item) => stubMenuItem(item, ctx));
|
|
371
|
+
}
|
|
372
|
+
else if (!STUB_STRIPPED_KEYS.has(entryKey)) {
|
|
373
|
+
stub[entryKey] = value;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
stub.pages = target.isRaw
|
|
377
|
+
? [target.href]
|
|
378
|
+
: [makeStubPage(typeof product.product === 'string' ? product.product : '', target)];
|
|
379
|
+
return stub;
|
|
380
|
+
}
|
|
381
|
+
// the active product is client session state, but a session can only select a
|
|
382
|
+
// product without navigating when the current path lives inside it — so every
|
|
383
|
+
// product containing the path keeps its real (recursively scoped) tree and the
|
|
384
|
+
// rest reduce to selector chrome: metadata, menu items, and one click target
|
|
385
|
+
function scopeProductArray(entries, ctx) {
|
|
386
|
+
const containsPath = entries.map((entry) => isNavNode(entry) && divisionContainsPath(entry, ctx.currentPath));
|
|
387
|
+
// a path outside every product gives no signal to stub by; keep them whole
|
|
388
|
+
if (!containsPath.some(Boolean)) {
|
|
389
|
+
return entries.map((entry) => (isNavNode(entry) ? scopeNode(entry, ctx) : entry));
|
|
390
|
+
}
|
|
391
|
+
return entries.map((entry, index) => {
|
|
392
|
+
if (!isNavNode(entry))
|
|
393
|
+
return entry;
|
|
394
|
+
if (containsPath[index])
|
|
395
|
+
return scopeNode(entry, ctx);
|
|
396
|
+
return stubProduct(entry, ctx);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
355
399
|
function scopeNode(node, ctx) {
|
|
356
400
|
const scoped = Object.assign({}, node);
|
|
357
401
|
for (const key of CONTAINER_KEYS) {
|
|
@@ -364,6 +408,9 @@ function scopeNode(node, ctx) {
|
|
|
364
408
|
else if (isSimpleDivisionKey(key) && value.length > 1) {
|
|
365
409
|
scoped[key] = scopeSimpleDivisionArray(value, key, ctx);
|
|
366
410
|
}
|
|
411
|
+
else if (key === 'products' && value.length > 1) {
|
|
412
|
+
scoped[key] = scopeProductArray(value, ctx);
|
|
413
|
+
}
|
|
367
414
|
else if (key === 'menu' &&
|
|
368
415
|
value.length > 1 &&
|
|
369
416
|
(typeof scoped.tab === 'string' || typeof scoped.anchor === 'string')) {
|