mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
fix(content-docs): restore functionality when a category only has index page (#7385)
* fix(content-docs): restore functionality when a category only has index page * use this internally
This commit is contained in:
parent
c3880cc342
commit
6e10a48059
13 changed files with 258 additions and 46 deletions
|
@ -6,4 +6,5 @@ This part is very complicated and hard to navigate. Sidebars are loaded through
|
|||
2. **Normalization**. The shorthands are expanded. This step is very lenient about the sidebars' shapes. Returns `NormalizedSidebars`.
|
||||
3. **Validation**. The normalized sidebars are validated. This step happens after normalization, because the normalized sidebars are easier to validate, and allows us to repeatedly validate & generate in the future.
|
||||
4. **Generation**. This step is done through the "processor" (naming is hard). The `autogenerated` items are unwrapped. In the future, steps 3 and 4 may be repeatedly done until all autogenerated items are unwrapped. Returns `ProcessedSidebars`.
|
||||
- **Important**: this step should only care about unwrapping autogenerated items, not filtering them, writing additional metadata, applying defaults, etc.—everything will be handled in the post-processor. Important because the generator is exposed to the end-user and we want it to be easy to be reasoned about.
|
||||
5. **Post-processing**. Defaults are applied (collapsed states), category links are resolved, empty categories are flattened. Returns `Sidebars`.
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"docs": [
|
||||
{
|
||||
"label": "Tutorials",
|
||||
"type": "category",
|
||||
"items": [
|
||||
{
|
||||
"type": "autogenerated",
|
||||
"dirName": "tutorials"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "index-only",
|
||||
"type": "category",
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "tutorials/tutorial-basics"
|
||||
},
|
||||
"items": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"sidebar": [
|
||||
"draft1",
|
||||
{
|
||||
"type": "category",
|
||||
"label": "all drafts",
|
||||
"items": [
|
||||
"draft2",
|
||||
"draft3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "category",
|
||||
"label": "all drafts",
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
},
|
||||
"items": [
|
||||
"draft2",
|
||||
"draft3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "category",
|
||||
"label": "all drafts",
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "draft1"
|
||||
},
|
||||
"items": [
|
||||
"draft2",
|
||||
"draft3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "category",
|
||||
"label": "index not draft",
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "not-draft"
|
||||
},
|
||||
"items": [
|
||||
"draft2",
|
||||
"draft3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "category",
|
||||
"label": "subitem not draft",
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "draft1"
|
||||
},
|
||||
"items": [
|
||||
"not-draft",
|
||||
"draft3"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,5 +1,56 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`loadSidebars loads sidebars with index-only categories 1`] = `
|
||||
{
|
||||
"docs": [
|
||||
{
|
||||
"collapsed": true,
|
||||
"collapsible": true,
|
||||
"items": [
|
||||
{
|
||||
"id": "tutorials/tutorial-basics",
|
||||
"label": "tutorial-basics",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
"label": "Tutorials",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
{
|
||||
"id": "tutorials/tutorial-basics",
|
||||
"label": "index-only",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`loadSidebars loads sidebars with interspersed draft items 1`] = `
|
||||
{
|
||||
"sidebar": [
|
||||
{
|
||||
"id": "not-draft",
|
||||
"label": "index not draft",
|
||||
"type": "doc",
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"collapsible": true,
|
||||
"items": [
|
||||
{
|
||||
"id": "not-draft",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
"label": "subitem not draft",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`loadSidebars sidebars link 1`] = `
|
||||
{
|
||||
"docs": [
|
||||
|
|
|
@ -60,14 +60,21 @@ exports[`postProcess corrects collapsed state inconsistencies 3`] = `
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`postProcess transforms category without subitems 1`] = `
|
||||
exports[`postProcess filters draft items 1`] = `
|
||||
{
|
||||
"sidebar": [
|
||||
{
|
||||
"href": "version/generated/permalink",
|
||||
"id": "another",
|
||||
"label": "Category",
|
||||
"type": "link",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`postProcess transforms category without subitems 1`] = `
|
||||
{
|
||||
"sidebar": [
|
||||
{
|
||||
"id": "doc ID",
|
||||
"label": "Category 2",
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import {jest} from '@jest/globals';
|
||||
import path from 'path';
|
||||
import {createSlugger} from '@docusaurus/utils';
|
||||
import {loadSidebars, DisabledSidebars} from '../index';
|
||||
import type {SidebarProcessorParams} from '../types';
|
||||
import {DefaultSidebarItemsGenerator} from '../generator';
|
||||
|
@ -27,6 +28,7 @@ describe('loadSidebars', () => {
|
|||
],
|
||||
drafts: [],
|
||||
version: {
|
||||
path: 'version',
|
||||
contentPath: path.join(fixtureDir, 'docs'),
|
||||
contentPathLocalized: path.join(fixtureDir, 'docs'),
|
||||
},
|
||||
|
@ -124,6 +126,32 @@ describe('loadSidebars', () => {
|
|||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('loads sidebars with index-only categories', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-category-index.json');
|
||||
const result = await loadSidebars(sidebarPath, {
|
||||
...params,
|
||||
docs: [
|
||||
{
|
||||
id: 'tutorials/tutorial-basics',
|
||||
source: '@site/docs/tutorials/tutorial-basics/index.md',
|
||||
sourceDirName: 'tutorials/tutorial-basics',
|
||||
frontMatter: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('loads sidebars with interspersed draft items', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-drafts.json');
|
||||
const result = await loadSidebars(sidebarPath, {
|
||||
...params,
|
||||
drafts: [{id: 'draft1'}, {id: 'draft2'}, {id: 'draft3'}],
|
||||
categoryLabelSlugger: createSlugger(),
|
||||
});
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('duplicate category metadata files', async () => {
|
||||
const sidebarPath = path.join(
|
||||
fixtureDir,
|
||||
|
|
|
@ -35,6 +35,7 @@ describe('postProcess', () => {
|
|||
{
|
||||
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
|
||||
version: {path: 'version'},
|
||||
drafts: [],
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -54,6 +55,7 @@ describe('postProcess', () => {
|
|||
{
|
||||
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
|
||||
version: {path: 'version'},
|
||||
drafts: [],
|
||||
},
|
||||
);
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
|
@ -79,6 +81,7 @@ describe('postProcess', () => {
|
|||
{
|
||||
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
|
||||
version: {path: 'version'},
|
||||
drafts: [],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
|
@ -99,6 +102,7 @@ describe('postProcess', () => {
|
|||
{
|
||||
sidebarOptions: {sidebarCollapsed: false, sidebarCollapsible: false},
|
||||
version: {path: 'version'},
|
||||
drafts: [],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
|
@ -118,6 +122,37 @@ describe('postProcess', () => {
|
|||
{
|
||||
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: false},
|
||||
version: {path: 'version'},
|
||||
drafts: [],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('filters draft items', () => {
|
||||
expect(
|
||||
postProcessSidebars(
|
||||
{
|
||||
sidebar: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
items: [{type: 'doc', id: 'foo'}],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'another',
|
||||
},
|
||||
items: [{type: 'doc', id: 'foo'}],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
|
||||
version: {path: 'version'},
|
||||
drafts: [{id: 'foo', unversionedId: 'foo'}],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
|
|
|
@ -15,12 +15,20 @@ import type {
|
|||
ProcessedSidebars,
|
||||
SidebarItemCategoryLink,
|
||||
} from './types';
|
||||
import {getDocIds} from '../docs';
|
||||
import _ from 'lodash';
|
||||
|
||||
type SidebarPostProcessorParams = SidebarProcessorParams & {
|
||||
draftIds: Set<string>;
|
||||
};
|
||||
|
||||
function normalizeCategoryLink(
|
||||
category: ProcessedSidebarItemCategory,
|
||||
params: SidebarProcessorParams,
|
||||
params: SidebarPostProcessorParams,
|
||||
): SidebarItemCategoryLink | undefined {
|
||||
if (category.link?.type === 'doc' && params.draftIds.has(category.link.id)) {
|
||||
return undefined;
|
||||
}
|
||||
if (category.link?.type === 'generated-index') {
|
||||
// Default slug logic can be improved
|
||||
const getDefaultSlug = () =>
|
||||
|
@ -38,37 +46,42 @@ function normalizeCategoryLink(
|
|||
|
||||
function postProcessSidebarItem(
|
||||
item: ProcessedSidebarItem,
|
||||
params: SidebarProcessorParams,
|
||||
): SidebarItem {
|
||||
params: SidebarPostProcessorParams,
|
||||
): SidebarItem | null {
|
||||
if (item.type === 'category') {
|
||||
// Fail-fast if there's actually no subitems, no because all subitems are
|
||||
// drafts. This is likely a configuration mistake.
|
||||
if (item.items.length === 0 && !item.link) {
|
||||
throw new Error(
|
||||
`Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`,
|
||||
);
|
||||
}
|
||||
const category = {
|
||||
...item,
|
||||
collapsed: item.collapsed ?? params.sidebarOptions.sidebarCollapsed,
|
||||
collapsible: item.collapsible ?? params.sidebarOptions.sidebarCollapsible,
|
||||
link: normalizeCategoryLink(item, params),
|
||||
items: item.items.map((subItem) =>
|
||||
postProcessSidebarItem(subItem, params),
|
||||
),
|
||||
items: item.items
|
||||
.map((subItem) => postProcessSidebarItem(subItem, params))
|
||||
.filter((v): v is SidebarItem => Boolean(v)),
|
||||
};
|
||||
// If the current category doesn't have subitems, we render a normal link
|
||||
// instead.
|
||||
if (category.items.length === 0) {
|
||||
if (!category.link) {
|
||||
throw new Error(
|
||||
`Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`,
|
||||
);
|
||||
// Doesn't make sense to render an empty generated index page, so we
|
||||
// filter the entire category out as well.
|
||||
if (
|
||||
!category.link ||
|
||||
category.link.type === 'generated-index' ||
|
||||
params.draftIds.has(category.link.id)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return category.link.type === 'doc'
|
||||
? {
|
||||
type: 'doc',
|
||||
label: category.label,
|
||||
id: category.link.id,
|
||||
}
|
||||
: {
|
||||
type: 'link',
|
||||
label: category.label,
|
||||
href: category.link.permalink,
|
||||
};
|
||||
return {
|
||||
type: 'doc',
|
||||
label: category.label,
|
||||
id: category.link.id,
|
||||
};
|
||||
}
|
||||
// A non-collapsible category can't be collapsed!
|
||||
if (category.collapsible === false) {
|
||||
|
@ -76,6 +89,12 @@ function postProcessSidebarItem(
|
|||
}
|
||||
return category;
|
||||
}
|
||||
if (
|
||||
(item.type === 'doc' || item.type === 'ref') &&
|
||||
params.draftIds.has(item.id)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
|
@ -83,7 +102,11 @@ export function postProcessSidebars(
|
|||
sidebars: ProcessedSidebars,
|
||||
params: SidebarProcessorParams,
|
||||
): Sidebars {
|
||||
const draftIds = new Set(params.drafts.flatMap(getDocIds));
|
||||
|
||||
return _.mapValues(sidebars, (sidebar) =>
|
||||
sidebar.map((item) => postProcessSidebarItem(item, params)),
|
||||
sidebar
|
||||
.map((item) => postProcessSidebarItem(item, {...params, draftIds}))
|
||||
.filter((v): v is SidebarItem => Boolean(v)),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import {DefaultSidebarItemsGenerator} from './generator';
|
|||
import {validateSidebars} from './validation';
|
||||
import _ from 'lodash';
|
||||
import combinePromises from 'combine-promises';
|
||||
import {getDocIds, isCategoryIndex} from '../docs';
|
||||
import {isCategoryIndex} from '../docs';
|
||||
|
||||
function toSidebarItemsGeneratorDoc(
|
||||
doc: DocMetadataBase,
|
||||
|
@ -55,8 +55,7 @@ async function processSidebar(
|
|||
categoriesMetadata: {[filePath: string]: CategoryMetadataFile},
|
||||
params: SidebarProcessorParams,
|
||||
): Promise<ProcessedSidebar> {
|
||||
const {sidebarItemsGenerator, numberPrefixParser, docs, drafts, version} =
|
||||
params;
|
||||
const {sidebarItemsGenerator, numberPrefixParser, docs, version} = params;
|
||||
|
||||
// Just a minor lazy transformation optimization
|
||||
const getSidebarItemsGeneratorDocsAndVersion = _.memoize(() => ({
|
||||
|
@ -82,19 +81,6 @@ async function processSidebar(
|
|||
return processItems(generatedItems);
|
||||
}
|
||||
|
||||
const draftIds = new Set(drafts.flatMap(getDocIds));
|
||||
|
||||
const isDraftItem = (item: NormalizedSidebarItem): boolean => {
|
||||
if (item.type === 'doc' || item.type === 'ref') {
|
||||
return draftIds.has(item.id);
|
||||
}
|
||||
// If a category only contains draft items, it should be filtered entirely.
|
||||
if (item.type === 'category') {
|
||||
return item.items.every(isDraftItem);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
async function processItem(
|
||||
item: NormalizedSidebarItem,
|
||||
): Promise<ProcessedSidebarItem[]> {
|
||||
|
@ -102,7 +88,7 @@ async function processSidebar(
|
|||
return [
|
||||
{
|
||||
...item,
|
||||
items: await processItems(item.items),
|
||||
items: (await Promise.all(item.items.map(processItem))).flat(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -115,9 +101,7 @@ async function processSidebar(
|
|||
async function processItems(
|
||||
items: NormalizedSidebarItem[],
|
||||
): Promise<ProcessedSidebarItem[]> {
|
||||
return (
|
||||
await Promise.all(items.filter((i) => !isDraftItem(i)).map(processItem))
|
||||
).flat();
|
||||
return (await Promise.all(items.map(processItem))).flat();
|
||||
}
|
||||
|
||||
const processedSidebar = await processItems(unprocessedSidebar);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue