@mintlify/common 1.0.1267 → 1.0.1268

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.
@@ -3,7 +3,7 @@ import { divisions, } from '@mintlify/validation';
3
3
  import { readObjectArrayProperty } from '../divisions/iterateObjectArrayProperty.js';
4
4
  import { optionallyAddLeadingSlash } from '../fs/optionallySlash.js';
5
5
  import { isAbsoluteUrl } from '../isAbsoluteUrl.js';
6
- import { isEqualIgnoringLeadingSlash } from '../paths/isEqualIgnoringLeadingSlash.js';
6
+ import { isEqualIgnoringLeadingSlash, normalizeComparablePath, } from '../paths/isEqualIgnoringLeadingSlash.js';
7
7
  import { checkNavAccess } from './checkNavAccess.js';
8
8
  import { getFirstPageFromNavigation } from './getFirstPageFromNavigation.js';
9
9
  import { isPage } from './isPage.js';
@@ -169,6 +169,7 @@ export const filterDivisions = ({ currentPath, currentVersion, currentLanguage,
169
169
  menuDivision,
170
170
  isPreview,
171
171
  parentDivisionNames: ancestorDivisionNames,
172
+ rootNav: decoratedNav,
172
173
  });
173
174
  })
174
175
  .filter(Boolean);
@@ -269,8 +270,8 @@ function upsertGlobalDivisions(navigationDivisions, globalConfig) {
269
270
  });
270
271
  }
271
272
  function buildDivisionItem(param) {
272
- var _a, _b;
273
- const { item, isActive, cache, currentPath, currentVersion, currentLanguage, userGroups, divisionKey, menuDivision, isPreview = false, parentDivisionNames = [], } = param;
273
+ var _a, _b, _c;
274
+ const { item, isActive, cache, currentPath, currentVersion, currentLanguage, userGroups, divisionKey, menuDivision, isPreview = false, parentDivisionNames = [], rootNav, } = param;
274
275
  const { dropdownCache } = cache;
275
276
  let href = '';
276
277
  // TODO: support this for other divisions (versions, languages, etc.)
@@ -293,7 +294,7 @@ function buildDivisionItem(param) {
293
294
  const isUserDefinedHref = 'href' in item && isAbsoluteUrl(item.href);
294
295
  if (!isUserDefinedHref && divisionKey === 'dropdowns' && !isActive && currentPath) {
295
296
  href =
296
- (_b = findCorrespondingDropdownHref(item, currentPath, userGroups !== null && userGroups !== void 0 ? userGroups : new Set(), isPreview)) !== null && _b !== void 0 ? _b : '';
297
+ (_c = (_b = findCorrespondingDropdownHref(item, currentPath, userGroups !== null && userGroups !== void 0 ? userGroups : new Set(), isPreview)) !== null && _b !== void 0 ? _b : findSamePageInDropdownVersion(page === null || page === void 0 ? void 0 : page.href, findDivisionName(item), currentPath, rootNav, userGroups !== null && userGroups !== void 0 ? userGroups : new Set(), isPreview)) !== null && _c !== void 0 ? _c : '';
297
298
  }
298
299
  if (!href) {
299
300
  href = isUserDefinedHref ? item.href : (page === null || page === void 0 ? void 0 : page.href) || '/';
@@ -327,6 +328,7 @@ function buildDivisionItem(param) {
327
328
  userGroups,
328
329
  divisionKey: 'menu',
329
330
  isPreview,
331
+ rootNav,
330
332
  parentDivisionNames: [
331
333
  ...parentDivisionNames,
332
334
  ...(parentDivisionName ? [parentDivisionName] : []),
@@ -384,6 +386,102 @@ function findCorrespondingDropdownHref(item, currentPath, userGroups, isPreview)
384
386
  walk(item);
385
387
  return exactMatch !== null && exactMatch !== void 0 ? exactMatch : segmentMatch;
386
388
  }
389
+ /**
390
+ * The same page in the dropdown's version when that page lives in a different part of the
391
+ * navigation, e.g. `docs/v3/webhooks` under one tab but `docs/v2/webhooks` under another.
392
+ * The version segment is read off the dropdown's first page at the first segment where it differs
393
+ * from the current path. That segment is swapped in, added (`/webhooks` -> `/v1/webhooks`) or
394
+ * removed (`/v1/webhooks` -> `/webhooks`), and a candidate is used only if that exact page is in a
395
+ * dropdown of the same name. Without this the switcher lands on the dropdown's first page.
396
+ */
397
+ function findSamePageInDropdownVersion(dropdownFirstHref, dropdownName, currentPath, rootNav, userGroups, isPreview) {
398
+ if (!dropdownFirstHref || !dropdownName || !rootNav)
399
+ return undefined;
400
+ const target = dropdownFirstHref.split('/').filter(Boolean);
401
+ const current = currentPath.split('/').filter(Boolean);
402
+ const index = current.findIndex((segment, i) => segment !== target[i]);
403
+ if (index === -1)
404
+ return undefined;
405
+ const versionSegment = target[index];
406
+ const candidates = [];
407
+ if (versionSegment !== undefined && index < current.length - 1) {
408
+ candidates.push(current.map((segment, i) => (i === index ? versionSegment : segment)));
409
+ }
410
+ if (versionSegment !== undefined) {
411
+ candidates.push([...current.slice(0, index), versionSegment, ...current.slice(index)]);
412
+ }
413
+ if (index < current.length - 1) {
414
+ candidates.push([...current.slice(0, index), ...current.slice(index + 1)]);
415
+ }
416
+ const pages = getDropdownPages(rootNav, userGroups, isPreview).get(dropdownName);
417
+ for (const segments of candidates) {
418
+ const found = pages === null || pages === void 0 ? void 0 : pages.get(normalizeComparablePath(segments.join('/')));
419
+ if (found)
420
+ return found;
421
+ }
422
+ return undefined;
423
+ }
424
+ const dropdownPagesCache = new WeakMap();
425
+ function getDropdownPages(rootNav, userGroups, isPreview) {
426
+ let byGroups = dropdownPagesCache.get(rootNav);
427
+ if (!byGroups)
428
+ dropdownPagesCache.set(rootNav, (byGroups = new WeakMap()));
429
+ let byPreview = byGroups.get(userGroups);
430
+ if (!byPreview)
431
+ byGroups.set(userGroups, (byPreview = new Map()));
432
+ let pages = byPreview.get(isPreview);
433
+ if (!pages) {
434
+ pages = collectDropdownPages(rootNav, userGroups, isPreview);
435
+ byPreview.set(isPreview, pages);
436
+ }
437
+ return pages;
438
+ }
439
+ function collectDropdownPages(rootNav, userGroups, isPreview) {
440
+ const pagesByDropdown = new Map();
441
+ function walk(entry, dropdownNames) {
442
+ if (typeof entry !== 'object')
443
+ return;
444
+ if (isPage(entry)) {
445
+ if (!dropdownNames.length || !checkNavAccess(entry, userGroups, isPreview))
446
+ return;
447
+ const key = normalizeComparablePath(entry.href);
448
+ for (const name of dropdownNames) {
449
+ let pages = pagesByDropdown.get(name);
450
+ if (!pages)
451
+ pagesByDropdown.set(name, (pages = new Map()));
452
+ if (!pages.has(key))
453
+ pages.set(key, entry.href);
454
+ }
455
+ return;
456
+ }
457
+ if ('hidden' in entry && entry.hidden)
458
+ return;
459
+ if ('root' in entry && typeof entry.root === 'object') {
460
+ walk(entry.root, dropdownNames);
461
+ }
462
+ if ('pages' in entry && Array.isArray(entry.pages)) {
463
+ for (const page of entry.pages) {
464
+ if (typeof page === 'object')
465
+ walk(page, dropdownNames);
466
+ }
467
+ }
468
+ if ('groups' in entry && Array.isArray(entry.groups)) {
469
+ for (const group of entry.groups) {
470
+ walk(group, dropdownNames);
471
+ }
472
+ }
473
+ for (const key of divisions) {
474
+ const subDivisions = readObjectArrayProperty(entry, key);
475
+ if (!subDivisions)
476
+ continue;
477
+ for (const subDivision of subDivisions) {
478
+ walk(subDivision, key === 'dropdowns' ? [...dropdownNames, findDivisionName(subDivision)] : dropdownNames);
479
+ }
480
+ }
481
+ }
482
+ walk(rootNav, []);
483
+ return pagesByDropdown;
484
+ }
387
485
  function getFirstPageFromCurrentDivisions(item, currentVersion, currentLanguage, userGroups, isPreview = false) {
388
486
  var _a, _b;
389
487
  if ('versions' in item && currentVersion) {
@@ -698,6 +796,7 @@ function findMostRelevantDivisions(currentPath, decoratedNav, currentVersion, cu
698
796
  divisionKey: key,
699
797
  isPreview,
700
798
  parentDivisionNames: ancestorDivisionNames,
799
+ rootNav: decoratedNav,
701
800
  });
702
801
  })
703
802
  .filter(Boolean);
@@ -858,4 +858,115 @@ describe('filterDivisions dropdown corresponding pages', () => {
858
858
  }, currentPath: '/vega/0.21/install' }));
859
859
  expect((_a = result.dropdowns.find((d) => d.name === 'Version 0.22')) === null || _a === void 0 ? void 0 : _a.href).toBe('/vega/0.22/install');
860
860
  });
861
+ describe('when the page sits in a different tab in the target version', () => {
862
+ const movedPageNav = {
863
+ tabs: [
864
+ {
865
+ tab: 'Guides',
866
+ dropdowns: [
867
+ {
868
+ dropdown: 'v3',
869
+ pages: [
870
+ { title: 'Quickstart', href: '/docs/v3/quickstart' },
871
+ { title: 'Webhooks', href: '/docs/v3/webhooks' },
872
+ ],
873
+ },
874
+ { dropdown: 'v2', pages: [{ title: 'Quickstart', href: '/docs/v2/quickstart' }] },
875
+ { dropdown: 'v1', pages: [{ title: 'Quickstart', href: '/docs/v1/quickstart' }] },
876
+ ],
877
+ },
878
+ {
879
+ tab: 'Reference',
880
+ dropdowns: [
881
+ {
882
+ dropdown: 'v2',
883
+ pages: [
884
+ { title: 'Errors', href: '/docs/v2/errors' },
885
+ { title: 'Webhooks', href: '/docs/v2/webhooks' },
886
+ ],
887
+ },
888
+ {
889
+ dropdown: 'v1',
890
+ pages: [
891
+ { title: 'Errors', href: '/docs/v1/errors' },
892
+ { title: 'Webhooks', href: '/docs/v1/webhooks', groups: ['admin'] },
893
+ ],
894
+ },
895
+ ],
896
+ },
897
+ ],
898
+ };
899
+ it('links to the same page in that version wherever it is in the navigation', () => {
900
+ var _a;
901
+ const result = filterDivisions(Object.assign(Object.assign({}, defaultArgs), { decoratedNav: movedPageNav, currentPath: '/docs/v3/webhooks' }));
902
+ expect((_a = result.dropdowns.find((d) => d.name === 'v2')) === null || _a === void 0 ? void 0 : _a.href).toBe('/docs/v2/webhooks');
903
+ });
904
+ it('falls back to the first page when that version has no page the viewer can access', () => {
905
+ var _a;
906
+ const result = filterDivisions(Object.assign(Object.assign({}, defaultArgs), { decoratedNav: movedPageNav, currentPath: '/docs/v3/webhooks' }));
907
+ expect((_a = result.dropdowns.find((d) => d.name === 'v1')) === null || _a === void 0 ? void 0 : _a.href).toBe('/docs/v1/quickstart');
908
+ });
909
+ });
910
+ describe('when only one version has a path prefix', () => {
911
+ const prefixNav = {
912
+ tabs: [
913
+ {
914
+ tab: 'Guides',
915
+ dropdowns: [
916
+ {
917
+ dropdown: 'Latest',
918
+ pages: [
919
+ { title: 'Quickstart', href: '/quickstart' },
920
+ { title: 'Webhooks', href: '/webhooks' },
921
+ ],
922
+ },
923
+ { dropdown: 'v1', pages: [{ title: 'Quickstart', href: '/v1/quickstart' }] },
924
+ ],
925
+ },
926
+ {
927
+ tab: 'Reference',
928
+ dropdowns: [
929
+ {
930
+ dropdown: 'v1',
931
+ pages: [
932
+ { title: 'Errors', href: '/v1/errors' },
933
+ { title: 'Webhooks', href: '/v1/webhooks' },
934
+ ],
935
+ },
936
+ { dropdown: 'Latest', pages: [{ title: 'Errors', href: '/errors' }] },
937
+ { dropdown: 'v2', pages: [{ title: 'Errors', href: '/v2/errors' }] },
938
+ ],
939
+ },
940
+ ],
941
+ };
942
+ it('adds the version prefix when switching from an unprefixed page', () => {
943
+ var _a;
944
+ const result = filterDivisions(Object.assign(Object.assign({}, defaultArgs), { decoratedNav: prefixNav, currentPath: '/webhooks' }));
945
+ expect((_a = result.dropdowns.find((d) => d.name === 'v1')) === null || _a === void 0 ? void 0 : _a.href).toBe('/v1/webhooks');
946
+ });
947
+ it('drops the version prefix when switching to an unprefixed version', () => {
948
+ var _a;
949
+ const result = filterDivisions(Object.assign(Object.assign({}, defaultArgs), { decoratedNav: prefixNav, currentPath: '/v1/errors' }));
950
+ expect((_a = result.dropdowns.find((d) => d.name === 'Latest')) === null || _a === void 0 ? void 0 : _a.href).toBe('/errors');
951
+ });
952
+ it('never links to a page from another version', () => {
953
+ var _a;
954
+ const result = filterDivisions(Object.assign(Object.assign({}, defaultArgs), { decoratedNav: prefixNav, currentPath: '/v1/webhooks' }));
955
+ expect((_a = result.dropdowns.find((d) => d.name === 'v2')) === null || _a === void 0 ? void 0 : _a.href).toBe('/v2/errors');
956
+ });
957
+ it('never links to a page inside a hidden tab', () => {
958
+ var _a;
959
+ const result = filterDivisions(Object.assign(Object.assign({}, defaultArgs), { decoratedNav: {
960
+ tabs: [
961
+ ...prefixNav.tabs,
962
+ {
963
+ tab: 'Internal',
964
+ hidden: true,
965
+ dropdowns: [{ dropdown: 'v2', pages: [{ title: 'Webhooks', href: '/v2/webhooks' }] }],
966
+ },
967
+ ],
968
+ }, currentPath: '/v1/webhooks' }));
969
+ expect((_a = result.dropdowns.find((d) => d.name === 'v2')) === null || _a === void 0 ? void 0 : _a.href).toBe('/v2/errors');
970
+ });
971
+ });
861
972
  });