feat(v2): global data + useGlobalData + docs versions dropdown (#2971)

* doc components initial simplification

* doc components initial simplification

* add docContext test

* Add poc of global data system + use it in the theme

* Revert "doc components initial simplification"

This reverts commit f657b4c4

* revert useless changes

* avoid loosing context on docs switch

* fix docs tests

* fix @generated/globalData ts declaration / es import

* typo

* revert bad commit

* refactor navbar in multiple parts + add navbar item types validation + try to fix remaining merge bugs

* add missing watch mode for plugin debug

* fix docs global data integration, move related hooks to docs plugin + convert to TS

* change versions link label

* fix activeClassName react warning

* improve docs global data system + contextual navbar dropdown

* fix bug preventing the deployment

* refactor the global data system to namespace automatically by plugin name + plugin id

* proper NavbarItem comp

* fix tests

* fix snapshot

* extract theme config schema in separate file + rename navbar links to navbar items

* minor typos

* polish docs components/api

* polish useDocs api surface

* fix the docs version suggestions comp + data

* refactors + add docsClientUtils unit tests

* Add documentation

* typo

* Add check for duplicate plugin ids detection

* multi-instance: createData plugin data should be namespaced by plugin instance id

* remove attempt for multi-instance support
This commit is contained in:
Sébastien Lorber 2020-07-21 11:16:08 +02:00 committed by GitHub
parent a51a56ec42
commit 15e73daae7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 1954 additions and 531 deletions

View file

@ -109,6 +109,46 @@ Array [
]
`;
exports[`simple website content 3`] = `
Object {
"pluginName": Object {
"pluginId": Object {
"latestVersionName": null,
"path": "docs",
"versions": Array [
Object {
"docs": Array [
Object {
"id": "foo/bar",
"path": "/docs/foo/bar",
},
Object {
"id": "foo/baz",
"path": "/docs/foo/bazSlug.html",
},
Object {
"id": "hello",
"path": "/docs/",
},
Object {
"id": "ipsum",
"path": "/docs/ipsum",
},
Object {
"id": "lorem",
"path": "/docs/lorem",
},
],
"mainDocId": "hello",
"name": null,
"path": "/docs",
},
],
},
},
}
`;
exports[`site with wrong sidebar file 1`] = `
"Bad sidebars file. The document id 'goku' was used in the sidebar, but no document with this id could be found.
Available document ids=
@ -213,6 +253,68 @@ Array [
]
`;
exports[`versioned website content 2`] = `
Object {
"pluginName": Object {
"pluginId": Object {
"latestVersionName": "1.0.1",
"path": "docs",
"versions": Array [
Object {
"docs": Array [
Object {
"id": "foo/bar",
"path": "/docs/next/foo/barSlug",
},
Object {
"id": "hello",
"path": "/docs/next/",
},
],
"mainDocId": "hello",
"name": "next",
"path": "/docs/next",
},
Object {
"docs": Array [
Object {
"id": "foo/bar",
"path": "/docs/foo/bar",
},
Object {
"id": "hello",
"path": "/docs/",
},
],
"mainDocId": "hello",
"name": "1.0.1",
"path": "/docs",
},
Object {
"docs": Array [
Object {
"id": "foo/bar",
"path": "/docs/1.0.0/foo/barSlug",
},
Object {
"id": "foo/baz",
"path": "/docs/1.0.0/foo/baz",
},
Object {
"id": "hello",
"path": "/docs/1.0.0/",
},
],
"mainDocId": "hello",
"name": "1.0.0",
"path": "/docs/1.0.0",
},
],
},
},
}
`;
exports[`versioned website content: all sidebars 1`] = `
Object {
"docs": Array [

View file

@ -0,0 +1,361 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
ActivePlugin,
getActivePlugin,
getLatestVersion,
getActiveDocContext,
getActiveVersion,
getDocVersionSuggestions,
} from '../../client/docsClientUtils';
import {GlobalPluginData, GlobalVersion} from '../../types';
import {shuffle} from 'lodash';
describe('docsClientUtils', () => {
test('getActivePlugin', () => {
const data: Record<string, GlobalPluginData> = {
pluginIosId: {
path: 'ios',
latestVersionName: 'xyz',
versions: [],
},
pluginAndroidId: {
path: 'android',
latestVersionName: 'xyz',
versions: [],
},
};
expect(getActivePlugin(data, '/')).toEqual(undefined);
expect(getActivePlugin(data, '/xyz')).toEqual(undefined);
const activePluginIos: ActivePlugin = {
pluginId: 'pluginIosId',
pluginData: data.pluginIosId,
};
expect(getActivePlugin(data, '/ios')).toEqual(activePluginIos);
expect(getActivePlugin(data, '/ios/')).toEqual(activePluginIos);
expect(getActivePlugin(data, '/ios/abc/def')).toEqual(activePluginIos);
const activePluginAndroid: ActivePlugin = {
pluginId: 'pluginAndroidId',
pluginData: data.pluginAndroidId,
};
expect(getActivePlugin(data, '/android')).toEqual(activePluginAndroid);
expect(getActivePlugin(data, '/android/')).toEqual(activePluginAndroid);
expect(getActivePlugin(data, '/android/ijk')).toEqual(activePluginAndroid);
});
test('getLatestVersion', () => {
const versions: GlobalVersion[] = [
{
name: 'version1',
path: '/???',
docs: [],
mainDocId: '???',
},
{
name: 'version2',
path: '/???',
docs: [],
mainDocId: '???',
},
{
name: 'version3',
path: '/???',
docs: [],
mainDocId: '???',
},
];
expect(
getLatestVersion({
path: '???',
latestVersionName: 'does not exist',
versions,
}),
).toEqual(undefined);
expect(
getLatestVersion({
path: '???',
latestVersionName: 'version1',
versions,
})?.name,
).toEqual('version1');
expect(
getLatestVersion({
path: '???',
latestVersionName: 'version2',
versions,
})?.name,
).toEqual('version2');
expect(
getLatestVersion({
path: '???',
latestVersionName: 'version3',
versions,
})?.name,
).toEqual('version3');
});
test('getActiveVersion', () => {
const data: GlobalPluginData = {
path: 'docs',
latestVersionName: 'version2',
versions: [
{
name: 'next',
path: '/docs/next',
docs: [],
mainDocId: '???',
},
{
name: 'version2',
path: '/docs',
docs: [],
mainDocId: '???',
},
{
name: 'version1',
path: '/docs/version1',
docs: [],
mainDocId: '???',
},
],
};
expect(getActiveVersion(data, '/docs/next')?.name).toEqual('next');
expect(getActiveVersion(data, '/docs/next/')?.name).toEqual('next');
expect(getActiveVersion(data, '/docs/next/someDoc')?.name).toEqual('next');
expect(getActiveVersion(data, '/docs')?.name).toEqual('version2');
expect(getActiveVersion(data, '/docs/')?.name).toEqual('version2');
expect(getActiveVersion(data, '/docs/someDoc')?.name).toEqual('version2');
expect(getActiveVersion(data, '/docs/version1')?.name).toEqual('version1');
expect(getActiveVersion(data, '/docs/version1')?.name).toEqual('version1');
expect(getActiveVersion(data, '/docs/version1/someDoc')?.name).toEqual(
'version1',
);
});
test('getActiveDocContext', () => {
const versionNext: GlobalVersion = {
name: 'next',
path: '/docs/next',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/next/',
},
{
id: 'doc2',
path: '/docs/next/doc2',
},
],
};
const version2: GlobalVersion = {
name: 'version2',
path: '/docs',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/',
},
{
id: 'doc2',
path: '/docs/doc2',
},
],
};
const version1: GlobalVersion = {
name: 'version1',
path: '/docs/version1',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/version1/',
},
],
};
// shuffle, because order shouldn't matter
const versions: GlobalVersion[] = shuffle([
versionNext,
version2,
version1,
]);
const data: GlobalPluginData = {
path: 'docs',
latestVersionName: 'version2',
versions,
};
expect(getActiveDocContext(data, '/doesNotExist')).toEqual({
activeVersion: undefined,
activeDoc: undefined,
alternateDocVersions: {},
});
expect(getActiveDocContext(data, '/docs/next/doesNotExist')).toEqual({
activeVersion: versionNext,
activeDoc: undefined,
alternateDocVersions: {},
});
expect(getActiveDocContext(data, '/docs/next')).toEqual({
activeVersion: versionNext,
activeDoc: versionNext.docs[0],
alternateDocVersions: {
next: versionNext.docs[0],
version2: version2.docs[0],
version1: version1.docs[0],
},
});
expect(getActiveDocContext(data, '/docs/next/doc2')).toEqual({
activeVersion: versionNext,
activeDoc: versionNext.docs[1],
alternateDocVersions: {
next: versionNext.docs[1],
version2: version2.docs[1],
version1: undefined,
},
});
expect(getActiveDocContext(data, '/docs/')).toEqual({
activeVersion: version2,
activeDoc: version2.docs[0],
alternateDocVersions: {
next: versionNext.docs[0],
version2: version2.docs[0],
version1: version1.docs[0],
},
});
expect(getActiveDocContext(data, '/docs/doc2')).toEqual({
activeVersion: version2,
activeDoc: version2.docs[1],
alternateDocVersions: {
next: versionNext.docs[1],
version2: version2.docs[1],
version1: undefined,
},
});
expect(getActiveDocContext(data, '/docs/version1')).toEqual({
activeVersion: version1,
activeDoc: version1.docs[0],
alternateDocVersions: {
next: versionNext.docs[0],
version2: version2.docs[0],
version1: version1.docs[0],
},
});
expect(getActiveDocContext(data, '/docs/version1/doc2')).toEqual({
activeVersion: version1,
activeDoc: undefined,
alternateDocVersions: {},
});
});
test('getDocVersionSuggestions', () => {
const versionNext: GlobalVersion = {
name: 'next',
path: '/docs/next',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/next/',
},
{
id: 'doc2',
path: '/docs/next/doc2',
},
],
};
const version2: GlobalVersion = {
name: 'version2',
path: '/docs',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/',
},
{
id: 'doc2',
path: '/docs/doc2',
},
],
};
const version1: GlobalVersion = {
name: 'version1',
path: '/docs/version1',
mainDocId: 'doc1',
docs: [
{
id: 'doc1',
path: '/docs/version1/',
},
],
};
// shuffle, because order shouldn't matter
const versions: GlobalVersion[] = shuffle([
versionNext,
version2,
version1,
]);
const data: GlobalPluginData = {
path: 'docs',
latestVersionName: 'version2',
versions,
};
expect(getDocVersionSuggestions(data, '/doesNotExist')).toEqual({
latestDocSuggestion: undefined,
latestVersionSuggestion: version2,
});
expect(getDocVersionSuggestions(data, '/docs/next')).toEqual({
latestDocSuggestion: version2.docs[0],
latestVersionSuggestion: version2,
});
expect(getDocVersionSuggestions(data, '/docs/next/doc2')).toEqual({
latestDocSuggestion: version2.docs[1],
latestVersionSuggestion: version2,
});
// nothing to suggest, we are already on latest version
expect(getDocVersionSuggestions(data, '/docs/')).toEqual({
latestDocSuggestion: undefined,
latestVersionSuggestion: undefined,
});
expect(getDocVersionSuggestions(data, '/docs/doc2')).toEqual({
latestDocSuggestion: undefined,
latestVersionSuggestion: undefined,
});
expect(getDocVersionSuggestions(data, '/docs/version1/')).toEqual({
latestDocSuggestion: version2.docs[0],
latestVersionSuggestion: version2,
});
expect(getDocVersionSuggestions(data, '/docs/version1/doc2')).toEqual({
latestDocSuggestion: undefined, // because /docs/version1/doc2 does not exist
latestVersionSuggestion: version2,
});
});
});

View file

@ -25,6 +25,7 @@ const createFakeActions = (
routeConfigs: RouteConfig[],
contentDir,
dataContainer?,
globalDataContainer?,
) => {
return {
addRoute: (config: RouteConfig) => {
@ -36,6 +37,9 @@ const createFakeActions = (
}
return path.join(contentDir, name);
},
setGlobalData: (data) => {
globalDataContainer.pluginName = {pluginId: data};
},
};
};
@ -166,6 +170,7 @@ describe('simple website', () => {
expect(versionToSidebars).toEqual({});
expect(docsMetadata.hello).toEqual({
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: true,
permalink: '/docs/',
previous: {
@ -176,11 +181,11 @@ describe('simple website', () => {
source: path.join('@site', pluginPath, 'hello.md'),
title: 'Hello, World !',
description: 'Hi, Endilie here :)',
latestVersionMainDocPermalink: undefined,
});
expect(docsMetadata['foo/bar']).toEqual({
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
next: {
title: 'baz',
@ -191,17 +196,18 @@ describe('simple website', () => {
source: path.join('@site', pluginPath, 'foo', 'bar.md'),
title: 'Bar',
description: 'This is custom description',
latestVersionMainDocPermalink: undefined,
});
expect(docsSidebars).toMatchSnapshot();
const routeConfigs = [];
const dataContainer = {};
const globalDataContainer = {};
const actions = createFakeActions(
routeConfigs,
pluginContentDir,
dataContainer,
globalDataContainer,
);
await plugin.contentLoaded({
@ -219,6 +225,7 @@ describe('simple website', () => {
expect(routeConfigs).not.toEqual([]);
expect(routeConfigs).toMatchSnapshot();
expect(globalDataContainer).toMatchSnapshot();
});
});
@ -313,6 +320,7 @@ describe('versioned website', () => {
expect(docsMetadata['version-1.0.1/foo/baz']).toBeUndefined();
expect(docsMetadata['foo/bar']).toEqual({
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug',
source: path.join('@site', routeBasePath, 'foo', 'bar.md'),
@ -327,6 +335,7 @@ describe('versioned website', () => {
});
expect(docsMetadata.hello).toEqual({
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: true,
permalink: '/docs/next/',
source: path.join('@site', routeBasePath, 'hello.md'),
@ -341,6 +350,7 @@ describe('versioned website', () => {
});
expect(docsMetadata['version-1.0.1/hello']).toEqual({
id: 'version-1.0.1/hello',
unversionedId: 'hello',
isDocsHomePage: true,
permalink: '/docs/',
source: path.join(
@ -357,10 +367,10 @@ describe('versioned website', () => {
title: 'bar',
permalink: '/docs/foo/bar',
},
latestVersionMainDocPermalink: undefined,
});
expect(docsMetadata['version-1.0.0/foo/baz']).toEqual({
id: 'version-1.0.0/foo/baz',
unversionedId: 'foo/baz',
isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/baz',
source: path.join(
@ -391,10 +401,12 @@ describe('versioned website', () => {
);
const routeConfigs = [];
const dataContainer = {};
const globalDataContainer = {};
const actions = createFakeActions(
routeConfigs,
pluginContentDir,
dataContainer,
globalDataContainer,
);
await plugin.contentLoaded({
content,
@ -438,5 +450,6 @@ describe('versioned website', () => {
expect(routeConfigs).not.toEqual([]);
expect(routeConfigs).toMatchSnapshot();
expect(globalDataContainer).toMatchSnapshot();
});
});

View file

@ -46,21 +46,21 @@ describe('simple site', () => {
expect(dataA).toEqual({
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/foo/bar',
source: path.join('@site', routeBasePath, sourceA),
title: 'Bar',
description: 'This is custom description',
latestVersionMainDocPermalink: undefined,
});
expect(dataB).toEqual({
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/hello',
source: path.join('@site', routeBasePath, sourceB),
title: 'Hello, World !',
description: `Hi, Endilie here :)`,
latestVersionMainDocPermalink: undefined,
});
});
@ -81,6 +81,7 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: true,
permalink: '/docs/',
source: path.join('@site', routeBasePath, source),
@ -106,6 +107,7 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: true,
permalink: '/docs/',
source: path.join('@site', routeBasePath, source),
@ -133,6 +135,7 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'foo/baz',
unversionedId: 'foo/baz',
isDocsHomePage: false,
permalink: '/docs/foo/bazSlug.html',
source: path.join('@site', routeBasePath, source),
@ -140,7 +143,6 @@ describe('simple site', () => {
editUrl:
'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md',
description: 'Images',
latestVersionMainDocPermalink: undefined,
});
});
@ -160,13 +162,13 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'lorem',
unversionedId: 'lorem',
isDocsHomePage: false,
permalink: '/docs/lorem',
source: path.join('@site', routeBasePath, source),
title: 'lorem',
editUrl: 'https://github.com/customUrl/docs/lorem.md',
description: 'Lorem ipsum.',
latestVersionMainDocPermalink: undefined,
});
// unrelated frontmatter is not part of metadata
@ -192,6 +194,7 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'lorem',
unversionedId: 'lorem',
isDocsHomePage: false,
permalink: '/docs/lorem',
source: path.join('@site', routeBasePath, source),
@ -200,7 +203,6 @@ describe('simple site', () => {
description: 'Lorem ipsum.',
lastUpdatedAt: 1539502055,
lastUpdatedBy: 'Author',
latestVersionMainDocPermalink: undefined,
});
});
@ -222,6 +224,7 @@ describe('simple site', () => {
expect(data).toEqual({
id: 'ipsum',
unversionedId: 'ipsum',
isDocsHomePage: false,
permalink: '/docs/ipsum',
source: path.join('@site', routeBasePath, source),
@ -230,7 +233,6 @@ describe('simple site', () => {
description: 'Lorem ipsum.',
lastUpdatedAt: 1539502055,
lastUpdatedBy: 'Author',
latestVersionMainDocPermalink: undefined,
});
});
@ -327,6 +329,7 @@ describe('versioned site', () => {
expect(dataA).toEqual({
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug',
source: path.join('@site', routeBasePath, sourceA),
@ -336,6 +339,7 @@ describe('versioned site', () => {
});
expect(dataB).toEqual({
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/next/hello',
source: path.join('@site', routeBasePath, sourceB),
@ -387,6 +391,7 @@ describe('versioned site', () => {
expect(dataA).toEqual({
id: 'version-1.0.0/foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/barSlug',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceA),
@ -396,6 +401,7 @@ describe('versioned site', () => {
});
expect(dataB).toEqual({
id: 'version-1.0.0/hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/1.0.0/hello',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceB),
@ -405,6 +411,7 @@ describe('versioned site', () => {
});
expect(dataC).toEqual({
id: 'version-1.0.1/foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/foo/bar',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceC),
@ -414,6 +421,7 @@ describe('versioned site', () => {
});
expect(dataD).toEqual({
id: 'version-1.0.1/hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/hello',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceD),