feat(v2): allow user to customize/enhance the default sidebar items generator (#4658)

* allow and document how to wrap/enhance the default docusaurus sidebar items generator

* improve doc

* doc

* doc
This commit is contained in:
Sébastien Lorber 2021-04-21 18:35:03 +02:00 committed by GitHub
parent e11597aba9
commit 792f4ac6fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 250 additions and 50 deletions

View file

@ -662,6 +662,7 @@ Array [
exports[`site with custom sidebar items generator sidebarItemsGenerator is called with appropriate data 1`] = `
Object {
"defaultSidebarItemsGenerator": [Function],
"docs": Array [
Object {
"frontMatter": Object {},

View file

@ -24,11 +24,18 @@ import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
import * as cliDocs from '../cli';
import {OptionsSchema} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {DocMetadata, LoadedVersion, SidebarItemsGenerator} from '../types';
import {
DocMetadata,
LoadedVersion,
SidebarItem,
SidebarItemsGeneratorOption,
SidebarItemsGeneratorOptionArgs,
} from '../types';
import {toSidebarsProp} from '../props';
// @ts-expect-error: TODO typedefs missing?
import {validate} from 'webpack';
import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator';
function findDocById(version: LoadedVersion, unversionedId: string) {
return version.docs.find((item) => item.unversionedId === unversionedId);
@ -1576,7 +1583,7 @@ describe('site with partial autogenerated sidebars 2 (fix #4638)', () => {
});
describe('site with custom sidebar items generator', () => {
async function loadSite(sidebarItemsGenerator: SidebarItemsGenerator) {
async function loadSite(sidebarItemsGenerator: SidebarItemsGeneratorOption) {
const siteDir = path.join(
__dirname,
'__fixtures__',
@ -1594,42 +1601,19 @@ describe('site with custom sidebar items generator', () => {
return {content, siteDir};
}
test('sidebar is autogenerated according to custom sidebarItemsGenerator', async () => {
const customSidebarItemsGenerator: SidebarItemsGenerator = async () => {
return [
{type: 'doc', id: 'API/api-overview'},
{type: 'doc', id: 'API/api-end'},
];
};
const customSidebarItemsGeneratorMock: SidebarItemsGenerator = jest.fn(
customSidebarItemsGenerator,
);
const {content} = await loadSite(customSidebarItemsGeneratorMock);
const version = content.loadedVersions[0];
expect(version.sidebars).toEqual({
defaultSidebar: [
{type: 'doc', id: 'API/api-overview'},
{type: 'doc', id: 'API/api-end'},
],
});
});
test('sidebarItemsGenerator is called with appropriate data', async () => {
type GeneratorArg = Parameters<SidebarItemsGenerator>[0];
const customSidebarItemsGeneratorMock = jest.fn(
async (_arg: GeneratorArg) => [],
async (_arg: SidebarItemsGeneratorOptionArgs) => [],
);
const {siteDir} = await loadSite(customSidebarItemsGeneratorMock);
const generatorArg: GeneratorArg =
const generatorArg: SidebarItemsGeneratorOptionArgs =
customSidebarItemsGeneratorMock.mock.calls[0][0];
// Make test pass even if docs are in different order and paths are absolutes
function makeDeterministic(arg: GeneratorArg): GeneratorArg {
function makeDeterministic(
arg: SidebarItemsGeneratorOptionArgs,
): SidebarItemsGeneratorOptionArgs {
return {
...arg,
docs: orderBy(arg.docs, 'id'),
@ -1641,5 +1625,140 @@ describe('site with custom sidebar items generator', () => {
}
expect(makeDeterministic(generatorArg)).toMatchSnapshot();
expect(generatorArg.defaultSidebarItemsGenerator).toEqual(
DefaultSidebarItemsGenerator,
);
});
test('sidebar is autogenerated according to a custom sidebarItemsGenerator', async () => {
const customSidebarItemsGenerator: SidebarItemsGeneratorOption = async () => {
return [
{type: 'doc', id: 'API/api-overview'},
{type: 'doc', id: 'API/api-end'},
];
};
const {content} = await loadSite(customSidebarItemsGenerator);
const version = content.loadedVersions[0];
expect(version.sidebars).toEqual({
defaultSidebar: [
{type: 'doc', id: 'API/api-overview'},
{type: 'doc', id: 'API/api-end'},
],
});
});
test('sidebarItemsGenerator can wrap/enhance/sort/reverse the default sidebar generator', async () => {
function reverseSidebarItems(items: SidebarItem[]): SidebarItem[] {
const result: SidebarItem[] = items.map((item) => {
if (item.type === 'category') {
return {...item, items: reverseSidebarItems(item.items)};
}
return item;
});
result.reverse();
return result;
}
const reversedSidebarItemsGenerator: SidebarItemsGeneratorOption = async ({
defaultSidebarItemsGenerator,
...args
}) => {
const sidebarItems = await defaultSidebarItemsGenerator(args);
return reverseSidebarItems(sidebarItems);
};
const {content} = await loadSite(reversedSidebarItemsGenerator);
const version = content.loadedVersions[0];
expect(version.sidebars).toEqual({
defaultSidebar: [
{
type: 'category',
label: 'API (label from _category_.json)',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/api-end',
},
{
type: 'category',
label: 'Extension APIs (label from _category_.yml)',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/Extension APIs/Theme API',
},
{
type: 'doc',
id: 'API/Extension APIs/Plugin API',
},
],
},
{
type: 'category',
label: 'Core APIs',
collapsed: true,
items: [
{
type: 'doc',
id: 'API/Core APIs/Server API',
},
{
type: 'doc',
id: 'API/Core APIs/Client API',
},
],
},
{
type: 'doc',
id: 'API/api-overview',
},
],
},
{
type: 'category',
label: 'Guides',
collapsed: true,
items: [
{
type: 'doc',
id: 'Guides/guide5',
},
{
type: 'doc',
id: 'Guides/guide4',
},
{
type: 'doc',
id: 'Guides/guide3',
},
{
type: 'doc',
id: 'Guides/guide2.5',
},
{
type: 'doc',
id: 'Guides/guide2',
},
{
type: 'doc',
id: 'Guides/guide1',
},
],
},
{
type: 'doc',
id: 'installation',
},
{
type: 'doc',
id: 'getting-started',
},
],
});
});
});

View file

@ -24,6 +24,7 @@ import {
Sidebars,
UnprocessedSidebars,
} from '../types';
import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator';
/* eslint-disable global-require, import/no-dynamic-require */
@ -534,16 +535,19 @@ describe('processSidebars', () => {
expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3);
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
item: {type: 'autogenerated', dirName: 'dir1'},
docs: [],
version: {},
});
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
item: {type: 'autogenerated', dirName: 'dir2'},
docs: [],
version: {},
});
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
item: {type: 'autogenerated', dirName: 'dir3'},
docs: [],
version: {},

View file

@ -21,14 +21,15 @@ import {
UnprocessedSidebar,
DocMetadataBase,
VersionMetadata,
SidebarItemsGenerator,
SidebarItemsGeneratorDoc,
SidebarItemsGeneratorVersion,
NumberPrefixParser,
SidebarItemsGeneratorOption,
} from './types';
import {mapValues, flatten, flatMap, difference, pick, memoize} from 'lodash';
import {getElementsAround} from '@docusaurus/utils';
import combinePromises from 'combine-promises';
import {DefaultSidebarItemsGenerator} from './sidebarItemsGenerator';
type SidebarItemCategoryJSON = SidebarItemBase & {
type: 'category';
@ -295,7 +296,7 @@ export async function processSidebar({
docs,
version,
}: {
sidebarItemsGenerator: SidebarItemsGenerator;
sidebarItemsGenerator: SidebarItemsGeneratorOption;
numberPrefixParser: NumberPrefixParser;
unprocessedSidebar: UnprocessedSidebar;
docs: DocMetadataBase[];
@ -322,6 +323,7 @@ export async function processSidebar({
return sidebarItemsGenerator({
item,
numberPrefixParser,
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
...getSidebarItemsGeneratorDocsAndVersion(),
});
}
@ -338,7 +340,7 @@ export async function processSidebars({
docs,
version,
}: {
sidebarItemsGenerator: SidebarItemsGenerator;
sidebarItemsGenerator: SidebarItemsGeneratorOption;
numberPrefixParser: NumberPrefixParser;
unprocessedSidebars: UnprocessedSidebars;
docs: DocMetadataBase[];

View file

@ -84,7 +84,7 @@ export type PluginOptions = MetadataOptions &
disableVersioning: boolean;
excludeNextVersionDocs?: boolean;
includeCurrentVersion: boolean;
sidebarItemsGenerator: SidebarItemsGenerator;
sidebarItemsGenerator: SidebarItemsGeneratorOption;
};
export type SidebarItemBase = {
@ -151,12 +151,25 @@ export type SidebarItemsGeneratorVersion = Pick<
VersionMetadata,
'versionName' | 'contentPath'
>;
export type SidebarItemsGenerator = (generatorArgs: {
export type SidebarItemsGeneratorArgs = {
item: UnprocessedSidebarItemAutogenerated;
version: SidebarItemsGeneratorVersion;
docs: SidebarItemsGeneratorDoc[];
numberPrefixParser: NumberPrefixParser;
}) => Promise<SidebarItem[]>;
};
export type SidebarItemsGenerator = (
generatorArgs: SidebarItemsGeneratorArgs,
) => Promise<SidebarItem[]>;
// Also inject the default generator to conveniently wrap/enhance/sort the default sidebar gen logic
// see https://github.com/facebook/docusaurus/issues/4640#issuecomment-822292320
export type SidebarItemsGeneratorOptionArgs = {
defaultSidebarItemsGenerator: SidebarItemsGenerator;
} & SidebarItemsGeneratorArgs;
export type SidebarItemsGeneratorOption = (
generatorArgs: SidebarItemsGeneratorOptionArgs,
) => Promise<SidebarItem[]>;
export type OrderMetadata = {
previous?: string;

View file

@ -76,14 +76,25 @@ module.exports = {
* Function used to replace the sidebar items of type "autogenerated"
* by real sidebar items (docs, categories, links...)
*/
sidebarItemsGenerator: function ({
item,
version,
docs,
numberPrefixParser,
sidebarItemsGenerator: async function ({
defaultSidebarItemsGenerator, // useful to re-use/enhance default sidebar generation logic from Docusaurus
numberPrefixParser, // numberPrefixParser configured for this plugin
item, // the sidebar item with type "autogenerated"
version, // the current version
docs, // all the docs of that version (unfiltered)
}) {
// Use the provided data to create a custom "sidebar slice"
return [{type: 'doc', id: 'doc1'}];
// Use the provided data to generate a custom sidebar slice
return [
{type: 'doc', id: 'intro'},
{
type: 'category',
label: 'Tutorials',
items: [
{type: 'doc', id: 'tutorial1'},
{type: 'doc', id: 'tutorial2'},
],
},
];
},
/**
* The Docs plugin supports number prefixes like "01-My Folder/02.My Doc.md".

View file

@ -521,14 +521,19 @@ module.exports = {
[
'@docusaurus/plugin-content-docs',
{
/**
* Function used to replace the sidebar items of type "autogenerated"
* by real sidebar items (docs, categories, links...)
*/
// highlight-start
sidebarItemsGenerator: function ({item, version, docs}) {
// Use the provided data to create a custom "sidebar slice"
return [{type: 'doc', id: 'doc1'}];
sidebarItemsGenerator: async function ({
defaultSidebarItemsGenerator,
numberPrefixParser,
item,
version,
docs,
}) {
// Example: return an hardcoded list of static sidebar items
return [
{type: 'doc', id: 'doc1'},
{type: 'doc', id: 'doc2'},
];
},
// highlight-end
},
@ -537,6 +542,51 @@ module.exports = {
};
```
:::tip
**Re-use and enhance the default generator** instead of writing a generator from scratch.
**Add, update, filter, re-order** the sidebar items according to your use-case:
```js title="docusaurus.config.js"
// highlight-start
// Reverse the sidebar items ordering (including nested category items)
function reverseSidebarItems(items) {
// Reverse items in categories
const result = items.map((item) => {
if (item.type === 'category') {
return {...item, items: reverseSidebarItems(item.items)};
}
return item;
});
// Reverse items at current level
result.reverse();
return result;
}
// highlight-end
module.exports = {
plugins: [
[
'@docusaurus/plugin-content-docs',
{
// highlight-start
sidebarItemsGenerator: async function ({
defaultSidebarItemsGenerator,
...args
}) {
const sidebarItems = await defaultSidebarItemsGenerator(args);
return reverseSidebarItems(sidebarItems);
},
// highlight-end
},
],
],
};
```
:::
## Hideable sidebar {#hideable-sidebar}
Using the enabled `themeConfig.hideableSidebar` option, you can make the entire sidebar hidden, allowing you to better focus your users on the content. This is especially useful when content consumption on medium screens (e.g. on tablets).