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:
Joshua Chen 2022-05-10 14:50:43 +08:00 committed by GitHub
parent c3880cc342
commit 6e10a48059
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 258 additions and 46 deletions

View file

@ -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`. 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. 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`. 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`. 5. **Post-processing**. Defaults are applied (collapsed states), category links are resolved, empty categories are flattened. Returns `Sidebars`.

View file

@ -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": []
}
]
}

View file

@ -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"
]
}
]
}

View file

@ -1,5 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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`] = ` exports[`loadSidebars sidebars link 1`] = `
{ {
"docs": [ "docs": [

View file

@ -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": [ "sidebar": [
{ {
"href": "version/generated/permalink", "id": "another",
"label": "Category", "label": "Category",
"type": "link", "type": "doc",
}, },
],
}
`;
exports[`postProcess transforms category without subitems 1`] = `
{
"sidebar": [
{ {
"id": "doc ID", "id": "doc ID",
"label": "Category 2", "label": "Category 2",

View file

@ -7,6 +7,7 @@
import {jest} from '@jest/globals'; import {jest} from '@jest/globals';
import path from 'path'; import path from 'path';
import {createSlugger} from '@docusaurus/utils';
import {loadSidebars, DisabledSidebars} from '../index'; import {loadSidebars, DisabledSidebars} from '../index';
import type {SidebarProcessorParams} from '../types'; import type {SidebarProcessorParams} from '../types';
import {DefaultSidebarItemsGenerator} from '../generator'; import {DefaultSidebarItemsGenerator} from '../generator';
@ -27,6 +28,7 @@ describe('loadSidebars', () => {
], ],
drafts: [], drafts: [],
version: { version: {
path: 'version',
contentPath: path.join(fixtureDir, 'docs'), contentPath: path.join(fixtureDir, 'docs'),
contentPathLocalized: path.join(fixtureDir, 'docs'), contentPathLocalized: path.join(fixtureDir, 'docs'),
}, },
@ -124,6 +126,32 @@ describe('loadSidebars', () => {
expect(result).toMatchSnapshot(); 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 () => { it('duplicate category metadata files', async () => {
const sidebarPath = path.join( const sidebarPath = path.join(
fixtureDir, fixtureDir,

View file

@ -35,6 +35,7 @@ describe('postProcess', () => {
{ {
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
version: {path: 'version'}, version: {path: 'version'},
drafts: [],
}, },
); );
@ -54,6 +55,7 @@ describe('postProcess', () => {
{ {
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
version: {path: 'version'}, version: {path: 'version'},
drafts: [],
}, },
); );
}).toThrowErrorMatchingInlineSnapshot( }).toThrowErrorMatchingInlineSnapshot(
@ -79,6 +81,7 @@ describe('postProcess', () => {
{ {
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true}, sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
version: {path: 'version'}, version: {path: 'version'},
drafts: [],
}, },
), ),
).toMatchSnapshot(); ).toMatchSnapshot();
@ -99,6 +102,7 @@ describe('postProcess', () => {
{ {
sidebarOptions: {sidebarCollapsed: false, sidebarCollapsible: false}, sidebarOptions: {sidebarCollapsed: false, sidebarCollapsible: false},
version: {path: 'version'}, version: {path: 'version'},
drafts: [],
}, },
), ),
).toMatchSnapshot(); ).toMatchSnapshot();
@ -118,6 +122,37 @@ describe('postProcess', () => {
{ {
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: false}, sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: false},
version: {path: 'version'}, 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(); ).toMatchSnapshot();

View file

@ -15,12 +15,20 @@ import type {
ProcessedSidebars, ProcessedSidebars,
SidebarItemCategoryLink, SidebarItemCategoryLink,
} from './types'; } from './types';
import {getDocIds} from '../docs';
import _ from 'lodash'; import _ from 'lodash';
type SidebarPostProcessorParams = SidebarProcessorParams & {
draftIds: Set<string>;
};
function normalizeCategoryLink( function normalizeCategoryLink(
category: ProcessedSidebarItemCategory, category: ProcessedSidebarItemCategory,
params: SidebarProcessorParams, params: SidebarPostProcessorParams,
): SidebarItemCategoryLink | undefined { ): SidebarItemCategoryLink | undefined {
if (category.link?.type === 'doc' && params.draftIds.has(category.link.id)) {
return undefined;
}
if (category.link?.type === 'generated-index') { if (category.link?.type === 'generated-index') {
// Default slug logic can be improved // Default slug logic can be improved
const getDefaultSlug = () => const getDefaultSlug = () =>
@ -38,36 +46,41 @@ function normalizeCategoryLink(
function postProcessSidebarItem( function postProcessSidebarItem(
item: ProcessedSidebarItem, item: ProcessedSidebarItem,
params: SidebarProcessorParams, params: SidebarPostProcessorParams,
): SidebarItem { ): SidebarItem | null {
if (item.type === 'category') { 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 = { const category = {
...item, ...item,
collapsed: item.collapsed ?? params.sidebarOptions.sidebarCollapsed, collapsed: item.collapsed ?? params.sidebarOptions.sidebarCollapsed,
collapsible: item.collapsible ?? params.sidebarOptions.sidebarCollapsible, collapsible: item.collapsible ?? params.sidebarOptions.sidebarCollapsible,
link: normalizeCategoryLink(item, params), link: normalizeCategoryLink(item, params),
items: item.items.map((subItem) => items: item.items
postProcessSidebarItem(subItem, params), .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 // If the current category doesn't have subitems, we render a normal link
// instead. // instead.
if (category.items.length === 0) { if (category.items.length === 0) {
if (!category.link) { // Doesn't make sense to render an empty generated index page, so we
throw new Error( // filter the entire category out as well.
`Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`, if (
); !category.link ||
category.link.type === 'generated-index' ||
params.draftIds.has(category.link.id)
) {
return null;
} }
return category.link.type === 'doc' return {
? {
type: 'doc', type: 'doc',
label: category.label, label: category.label,
id: category.link.id, id: category.link.id,
}
: {
type: 'link',
label: category.label,
href: category.link.permalink,
}; };
} }
// A non-collapsible category can't be collapsed! // A non-collapsible category can't be collapsed!
@ -76,6 +89,12 @@ function postProcessSidebarItem(
} }
return category; return category;
} }
if (
(item.type === 'doc' || item.type === 'ref') &&
params.draftIds.has(item.id)
) {
return null;
}
return item; return item;
} }
@ -83,7 +102,11 @@ export function postProcessSidebars(
sidebars: ProcessedSidebars, sidebars: ProcessedSidebars,
params: SidebarProcessorParams, params: SidebarProcessorParams,
): Sidebars { ): Sidebars {
const draftIds = new Set(params.drafts.flatMap(getDocIds));
return _.mapValues(sidebars, (sidebar) => return _.mapValues(sidebars, (sidebar) =>
sidebar.map((item) => postProcessSidebarItem(item, params)), sidebar
.map((item) => postProcessSidebarItem(item, {...params, draftIds}))
.filter((v): v is SidebarItem => Boolean(v)),
); );
} }

View file

@ -26,7 +26,7 @@ import {DefaultSidebarItemsGenerator} from './generator';
import {validateSidebars} from './validation'; import {validateSidebars} from './validation';
import _ from 'lodash'; import _ from 'lodash';
import combinePromises from 'combine-promises'; import combinePromises from 'combine-promises';
import {getDocIds, isCategoryIndex} from '../docs'; import {isCategoryIndex} from '../docs';
function toSidebarItemsGeneratorDoc( function toSidebarItemsGeneratorDoc(
doc: DocMetadataBase, doc: DocMetadataBase,
@ -55,8 +55,7 @@ async function processSidebar(
categoriesMetadata: {[filePath: string]: CategoryMetadataFile}, categoriesMetadata: {[filePath: string]: CategoryMetadataFile},
params: SidebarProcessorParams, params: SidebarProcessorParams,
): Promise<ProcessedSidebar> { ): Promise<ProcessedSidebar> {
const {sidebarItemsGenerator, numberPrefixParser, docs, drafts, version} = const {sidebarItemsGenerator, numberPrefixParser, docs, version} = params;
params;
// Just a minor lazy transformation optimization // Just a minor lazy transformation optimization
const getSidebarItemsGeneratorDocsAndVersion = _.memoize(() => ({ const getSidebarItemsGeneratorDocsAndVersion = _.memoize(() => ({
@ -82,19 +81,6 @@ async function processSidebar(
return processItems(generatedItems); 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( async function processItem(
item: NormalizedSidebarItem, item: NormalizedSidebarItem,
): Promise<ProcessedSidebarItem[]> { ): Promise<ProcessedSidebarItem[]> {
@ -102,7 +88,7 @@ async function processSidebar(
return [ return [
{ {
...item, ...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( async function processItems(
items: NormalizedSidebarItem[], items: NormalizedSidebarItem[],
): Promise<ProcessedSidebarItem[]> { ): Promise<ProcessedSidebarItem[]> {
return ( return (await Promise.all(items.map(processItem))).flat();
await Promise.all(items.filter((i) => !isDraftItem(i)).map(processItem))
).flat();
} }
const processedSidebar = await processItems(unprocessedSidebar); const processedSidebar = await processItems(unprocessedSidebar);

View file

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Before After
Before After

View file

@ -64,4 +64,4 @@ An embedded expression is optionally preceded by a flag in the form `[a-z]+=` (a
If the expression is an array, it's formatted by `` `\n- ${array.join('\n- ')}\n` `` (note it automatically gets a leading line end). Each member is formatted by itself and the bullet is not formatted. So you would see the above message printed as: If the expression is an array, it's formatted by `` `\n- ${array.join('\n- ')}\n` `` (note it automatically gets a leading line end). Each member is formatted by itself and the bullet is not formatted. So you would see the above message printed as:
![demo](./img/logger-demo.png) ![demo](./demo.png)