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`.
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`.

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
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": [

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": [
{
"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",

View file

@ -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,

View file

@ -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();

View file

@ -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,36 +46,41 @@ 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'
? {
return {
type: 'doc',
label: category.label,
id: category.link.id,
}
: {
type: 'link',
label: category.label,
href: category.link.permalink,
};
}
// A non-collapsible category can't be collapsed!
@ -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)),
);
}

View file

@ -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);

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:
![demo](./img/logger-demo.png)
![demo](./demo.png)