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`] = ` exports[`site with custom sidebar items generator sidebarItemsGenerator is called with appropriate data 1`] = `
Object { Object {
"defaultSidebarItemsGenerator": [Function],
"docs": Array [ "docs": Array [
Object { Object {
"frontMatter": Object {}, "frontMatter": Object {},

View file

@ -24,11 +24,18 @@ import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
import * as cliDocs from '../cli'; import * as cliDocs from '../cli';
import {OptionsSchema} from '../options'; import {OptionsSchema} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation'; import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {DocMetadata, LoadedVersion, SidebarItemsGenerator} from '../types'; import {
DocMetadata,
LoadedVersion,
SidebarItem,
SidebarItemsGeneratorOption,
SidebarItemsGeneratorOptionArgs,
} from '../types';
import {toSidebarsProp} from '../props'; import {toSidebarsProp} from '../props';
// @ts-expect-error: TODO typedefs missing? // @ts-expect-error: TODO typedefs missing?
import {validate} from 'webpack'; import {validate} from 'webpack';
import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator';
function findDocById(version: LoadedVersion, unversionedId: string) { function findDocById(version: LoadedVersion, unversionedId: string) {
return version.docs.find((item) => item.unversionedId === unversionedId); 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', () => { describe('site with custom sidebar items generator', () => {
async function loadSite(sidebarItemsGenerator: SidebarItemsGenerator) { async function loadSite(sidebarItemsGenerator: SidebarItemsGeneratorOption) {
const siteDir = path.join( const siteDir = path.join(
__dirname, __dirname,
'__fixtures__', '__fixtures__',
@ -1594,42 +1601,19 @@ describe('site with custom sidebar items generator', () => {
return {content, siteDir}; 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 () => { test('sidebarItemsGenerator is called with appropriate data', async () => {
type GeneratorArg = Parameters<SidebarItemsGenerator>[0];
const customSidebarItemsGeneratorMock = jest.fn( const customSidebarItemsGeneratorMock = jest.fn(
async (_arg: GeneratorArg) => [], async (_arg: SidebarItemsGeneratorOptionArgs) => [],
); );
const {siteDir} = await loadSite(customSidebarItemsGeneratorMock); const {siteDir} = await loadSite(customSidebarItemsGeneratorMock);
const generatorArg: GeneratorArg = const generatorArg: SidebarItemsGeneratorOptionArgs =
customSidebarItemsGeneratorMock.mock.calls[0][0]; customSidebarItemsGeneratorMock.mock.calls[0][0];
// Make test pass even if docs are in different order and paths are absolutes // 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 { return {
...arg, ...arg,
docs: orderBy(arg.docs, 'id'), docs: orderBy(arg.docs, 'id'),
@ -1641,5 +1625,140 @@ describe('site with custom sidebar items generator', () => {
} }
expect(makeDeterministic(generatorArg)).toMatchSnapshot(); 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, Sidebars,
UnprocessedSidebars, UnprocessedSidebars,
} from '../types'; } from '../types';
import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator';
/* eslint-disable global-require, import/no-dynamic-require */ /* eslint-disable global-require, import/no-dynamic-require */
@ -534,16 +535,19 @@ describe('processSidebars', () => {
expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3); expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3);
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
item: {type: 'autogenerated', dirName: 'dir1'}, item: {type: 'autogenerated', dirName: 'dir1'},
docs: [], docs: [],
version: {}, version: {},
}); });
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
item: {type: 'autogenerated', dirName: 'dir2'}, item: {type: 'autogenerated', dirName: 'dir2'},
docs: [], docs: [],
version: {}, version: {},
}); });
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
item: {type: 'autogenerated', dirName: 'dir3'}, item: {type: 'autogenerated', dirName: 'dir3'},
docs: [], docs: [],
version: {}, version: {},

View file

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

View file

@ -84,7 +84,7 @@ export type PluginOptions = MetadataOptions &
disableVersioning: boolean; disableVersioning: boolean;
excludeNextVersionDocs?: boolean; excludeNextVersionDocs?: boolean;
includeCurrentVersion: boolean; includeCurrentVersion: boolean;
sidebarItemsGenerator: SidebarItemsGenerator; sidebarItemsGenerator: SidebarItemsGeneratorOption;
}; };
export type SidebarItemBase = { export type SidebarItemBase = {
@ -151,12 +151,25 @@ export type SidebarItemsGeneratorVersion = Pick<
VersionMetadata, VersionMetadata,
'versionName' | 'contentPath' 'versionName' | 'contentPath'
>; >;
export type SidebarItemsGenerator = (generatorArgs: {
export type SidebarItemsGeneratorArgs = {
item: UnprocessedSidebarItemAutogenerated; item: UnprocessedSidebarItemAutogenerated;
version: SidebarItemsGeneratorVersion; version: SidebarItemsGeneratorVersion;
docs: SidebarItemsGeneratorDoc[]; docs: SidebarItemsGeneratorDoc[];
numberPrefixParser: NumberPrefixParser; 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 = { export type OrderMetadata = {
previous?: string; previous?: string;

View file

@ -76,14 +76,25 @@ module.exports = {
* Function used to replace the sidebar items of type "autogenerated" * Function used to replace the sidebar items of type "autogenerated"
* by real sidebar items (docs, categories, links...) * by real sidebar items (docs, categories, links...)
*/ */
sidebarItemsGenerator: function ({ sidebarItemsGenerator: async function ({
item, defaultSidebarItemsGenerator, // useful to re-use/enhance default sidebar generation logic from Docusaurus
version, numberPrefixParser, // numberPrefixParser configured for this plugin
docs, item, // the sidebar item with type "autogenerated"
numberPrefixParser, version, // the current version
docs, // all the docs of that version (unfiltered)
}) { }) {
// Use the provided data to create a custom "sidebar slice" // Use the provided data to generate a custom sidebar slice
return [{type: 'doc', id: 'doc1'}]; 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". * 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', '@docusaurus/plugin-content-docs',
{ {
/**
* Function used to replace the sidebar items of type "autogenerated"
* by real sidebar items (docs, categories, links...)
*/
// highlight-start // highlight-start
sidebarItemsGenerator: function ({item, version, docs}) { sidebarItemsGenerator: async function ({
// Use the provided data to create a custom "sidebar slice" defaultSidebarItemsGenerator,
return [{type: 'doc', id: 'doc1'}]; numberPrefixParser,
item,
version,
docs,
}) {
// Example: return an hardcoded list of static sidebar items
return [
{type: 'doc', id: 'doc1'},
{type: 'doc', id: 'doc2'},
];
}, },
// highlight-end // 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} ## 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). 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).