Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(v2): persist docs preferred version #3543

Merged
merged 13 commits into from
Oct 12, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ import {
GetActivePluginOptions,
} from '../../client/docsClientUtils';

const useAllDocsData = (): Record<string, GlobalPluginData> =>
export const useAllDocsData = (): Record<string, GlobalPluginData> =>
useAllPluginInstancesData('docusaurus-plugin-content-docs');

const useDocsData = (pluginId: string | undefined) =>
export const useDocsData = (pluginId: string | undefined) =>
usePluginData('docusaurus-plugin-content-docs', pluginId) as GlobalPluginData;

export const useActivePlugin = (options: GetActivePluginOptions = {}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useActiveVersion,
useDocVersionSuggestions,
} from '@theme/hooks/useDocs';
import useDocsPreferredVersion from '../../utils/docsPreferredVersion/useDocsPreferredVersion';

const getVersionMainDoc = (version) =>
version.docs.find((doc) => doc.id === version.mainDocId);
Expand All @@ -22,6 +23,9 @@ function DocVersionSuggestions(): JSX.Element {
siteConfig: {title: siteTitle},
} = useDocusaurusContext();
const {pluginId} = useActivePlugin({failfast: true});

const {savePreferredVersionName} = useDocsPreferredVersion(pluginId);

const activeVersion = useActiveVersion(pluginId);
const {
latestDocSuggestion,
Expand All @@ -35,7 +39,7 @@ function DocVersionSuggestions(): JSX.Element {

// try to link to same doc in latest version (not always possible)
// fallback to main doc of latest version
const suggestedDoc =
const latestVersionSuggestedDoc =
latestDocSuggestion ?? getVersionMainDoc(latestVersionSuggestion);

return (
Expand All @@ -58,7 +62,13 @@ function DocVersionSuggestions(): JSX.Element {
<div className="margin-top--md">
For up-to-date documentation, see the{' '}
<strong>
<Link to={suggestedDoc.path}>latest version</Link>
<Link
to={latestVersionSuggestedDoc.path}
onClick={() =>
savePreferredVersionName(latestVersionSuggestion.name)
}>
latest version
</Link>
</strong>{' '}
({latestVersionSuggestion.label}).
</div>
Expand Down
7 changes: 6 additions & 1 deletion packages/docusaurus-theme-classic/src/theme/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ import Footer from '@theme/Footer';
import type {Props} from '@theme/Layout';

import './styles.css';
import DocsPreferredVersionContextProvider from '../../utils/docsPreferredVersion/DocsPreferredVersionProvider';

function Providers({children}) {
return (
<ThemeProvider>
<UserPreferencesProvider>{children}</UserPreferencesProvider>
<UserPreferencesProvider>
<DocsPreferredVersionContextProvider>
{children}
</DocsPreferredVersionContextProvider>
</UserPreferencesProvider>
</ThemeProvider>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useActiveDocContext,
} from '@theme/hooks/useDocs';
import type {Props} from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
import useDocsPreferredVersion from '../../utils/docsPreferredVersion/useDocsPreferredVersion';

const getVersionMainDoc = (version) =>
version.docs.find((doc) => doc.id === version.mainDocId);
Expand All @@ -26,6 +27,10 @@ export default function DocsVersionDropdownNavbarItem({
const versions = useVersions(docsPluginId);
const latestVersion = useLatestVersion(docsPluginId);

const {preferredVersion, savePreferredVersionName} = useDocsPreferredVersion(
docsPluginId,
);

function getItems() {
// We don't want to render a version dropdown with 0 or 1 item
// If we build the site with a single docs version (onlyIncludeVersions: ['1.0.0'])
Expand All @@ -45,11 +50,15 @@ export default function DocsVersionDropdownNavbarItem({
label: version.label,
to: versionDoc.path,
isActive: () => version === activeDocContext?.activeVersion,
onClick: () => {
savePreferredVersionName(version.name);
},
};
});
}

const dropdownVersion = activeDocContext.activeVersion ?? latestVersion;
const dropdownVersion =
activeDocContext.activeVersion ?? preferredVersion ?? latestVersion;

// Mobile is handled a bit differently
const dropdownLabel = mobile ? 'Versions' : dropdownVersion.label;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* 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 React, {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import useThemeConfig, {DocsVersionPersistence} from '../useThemeConfig';

import {useAllDocsData} from '@theme/hooks/useDocs';
import DocsPreferredVersionStorage from './DocsPreferredVersionStorage';

type DocsPreferredVersionName = string | null;

// State for a single docs plugin instance
type DocsPreferredVersionPluginState = {
preferredVersionName: DocsPreferredVersionName;
};

// We need to store in state/storage globally
// one preferred version per docs plugin instance
// pluginId => pluginState
type DocsPreferredVersionState = Record<
string,
DocsPreferredVersionPluginState
>;

// Initial state is always null as we can't read localstorage from node SSR
function getInitialState(pluginIds: string[]): DocsPreferredVersionState {
const initialState: DocsPreferredVersionState = {};
pluginIds.forEach((pluginId) => {
initialState[pluginId] = {
preferredVersionName: null,
};
});
return initialState;
}

// Read storage for all docs plugins
// Assign to each doc plugin a preferred version (if found)
function readStorageState({
pluginIds,
versionPersistence,
allDocsData,
}: {
pluginIds: string[];
versionPersistence: DocsVersionPersistence;
allDocsData: any; // TODO find a way to type it :(
}): DocsPreferredVersionState {
// The storage value we read might be stale,
// and belong to a version that does not exist in the site anymore
// In such case, we remove the storage value to avoid downstream errors
function restorePluginState(
pluginId: string,
): DocsPreferredVersionPluginState {
const preferredVersionNameUnsafe = DocsPreferredVersionStorage.read(
pluginId,
versionPersistence,
);
const pluginData = allDocsData[pluginId];
const versionExists = pluginData.versions.some(
(version) => version.name === preferredVersionNameUnsafe,
);
if (versionExists) {
return {preferredVersionName: preferredVersionNameUnsafe};
} else {
DocsPreferredVersionStorage.clear(pluginId, versionPersistence);
return {preferredVersionName: null};
}
}

const initialState: DocsPreferredVersionState = {};
pluginIds.forEach((pluginId) => {
initialState[pluginId] = restorePluginState(pluginId);
});
return initialState;
}

function useVersionPersistence(): DocsVersionPersistence {
return useThemeConfig().docs.versionPersistence;
}

// Value that will be accessible through context: [state,api]
function useContextValue() {
const allDocsData = useAllDocsData();
const versionPersistence = useVersionPersistence();
const pluginIds = useMemo(() => Object.keys(allDocsData), [allDocsData]);

// Initial state is empty, as we can't read browser storage in node/SSR
const [state, setState] = useState(() => getInitialState(pluginIds));

// On mount, we set the state read from browser storage
useEffect(() => {
setState(readStorageState({allDocsData, versionPersistence, pluginIds}));
}, [allDocsData, versionPersistence, pluginIds]);

// The API that we expose to consumer hooks (memo for constant object)
const api = useMemo(() => {
function savePreferredVersion(pluginId: string, versionName: string) {
DocsPreferredVersionStorage.save(
pluginId,
versionPersistence,
versionName,
);
setState((s) => ({
...s,
[pluginId]: {preferredVersionName: versionName},
}));
}

return {
savePreferredVersion,
};
}, [setState]);

return [state, api] as const;
}

type DocsPreferredVersionContextValue = ReturnType<typeof useContextValue>;

const Context = createContext<DocsPreferredVersionContextValue | null>(null);

export default function DocsPreferredVersionContextProvider({
children,
}: {
children: ReactNode;
}) {
const contextValue = useContextValue();
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}

export function useDocsPreferredVersionContext(): DocsPreferredVersionContextValue {
const value = useContext(Context);
if (!value) {
throw new Error(
"Can't find docs preferred context, maybe you forgot to use the DocsPreferredVersionContextProvider ?",
);
}
return value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* 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 {DocsVersionPersistence} from '../useThemeConfig';

const storageKey = (pluginId: string) => `docs-preferred-version-${pluginId}`;

const DocsPreferredVersionStorage = {
save: (
pluginId: string,
persistence: DocsVersionPersistence,
versionName: string,
): void => {
if (persistence === 'none') {
// noop
} else {
window.localStorage.setItem(storageKey(pluginId), versionName);
}
},

read: (
pluginId: string,
persistence: DocsVersionPersistence,
): string | null => {
if (persistence === 'none') {
return null;
} else {
return window.localStorage.getItem(storageKey(pluginId));
}
},

clear: (pluginId: string, persistence: DocsVersionPersistence): void => {
if (persistence === 'none') {
// noop
} else {
window.localStorage.removeItem(storageKey(pluginId));
}
},
};

export default DocsPreferredVersionStorage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* 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 {useCallback} from 'react';
import {useDocsPreferredVersionContext} from './DocsPreferredVersionProvider';
import {useDocsData} from '@theme/hooks/useDocs';

// TODO why this import does not work!
// import {DEFAULT_PLUGIN_ID} from '@docusaurus/constants';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no idea why, but unable to make this work

const DEFAULT_PLUGIN_ID = 'default';

// Note, the preferredVersion attribute will always be null before mount
export default function useDocsPreferredVersion(
pluginId: string | undefined = DEFAULT_PLUGIN_ID,
) {
const docsData = useDocsData(pluginId);
const [state, api] = useDocsPreferredVersionContext();

const {preferredVersionName} = state[pluginId];

const preferredVersion = preferredVersionName
? docsData.versions.find((version) => version.name === preferredVersionName)
: null;

const savePreferredVersionName = useCallback(
(versionName: string) => {
api.savePreferredVersion(pluginId, versionName);
},
[api],
);

return {preferredVersion, savePreferredVersionName} as const;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
*/
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

type ThemeConfig = {
export type DocsVersionPersistence = 'localStorage' | 'none';

export type ThemeConfig = {
docs: {
versionPersistence: DocsVersionPersistence;
};

// TODO we should complete this theme config type over time
// and share it across all themes
// and use it in the Joi validation schema?
Expand Down
11 changes: 11 additions & 0 deletions packages/docusaurus-theme-classic/src/validateThemeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
const Joi = require('@hapi/joi');
const {URISchema} = require('@docusaurus/utils-validation');

const DEFAULT_DOCS_CONFIG = {
versionPersistence: 'localStorage',
};
const DocsSchema = Joi.object({
versionPersistence: Joi.string()
.equal('localStorage', 'none')
.default(DEFAULT_DOCS_CONFIG.versionPersistence),
}).default(DEFAULT_DOCS_CONFIG);

const DEFAULT_COLOR_MODE_CONFIG = {
defaultMode: 'light',
disableSwitch: false,
Expand All @@ -22,6 +31,7 @@ const DEFAULT_COLOR_MODE_CONFIG = {

const DEFAULT_CONFIG = {
colorMode: DEFAULT_COLOR_MODE_CONFIG,
docs: DEFAULT_DOCS_CONFIG,
metadatas: [],
prism: {
additionalLanguages: [],
Expand Down Expand Up @@ -190,6 +200,7 @@ const ThemeConfigSchema = Joi.object({
customCss: CustomCssSchema,
colorMode: ColorModeSchema,
image: Joi.string(),
docs: DocsSchema,
metadatas: Joi.array()
.items(HtmlMetadataSchema)
.default(DEFAULT_CONFIG.metadatas),
Expand Down
Loading