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 all 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 @@ -10,6 +10,7 @@ import DefaultNavbarItem from './DefaultNavbarItem';
import {useLatestVersion, useActiveDocContext} from '@theme/hooks/useDocs';
import clsx from 'clsx';
import type {Props} from '@theme/NavbarItem/DocNavbarItem';
import useDocsPreferredVersion from '../../utils/docsPreferredVersion/useDocsPreferredVersion';

export default function DocNavbarItem({
docId,
Expand All @@ -18,10 +19,11 @@ export default function DocNavbarItem({
docsPluginId,
...props
}: Props): JSX.Element {
const latestVersion = useLatestVersion(docsPluginId);
const {activeVersion, activeDoc} = useActiveDocContext(docsPluginId);
const {preferredVersion} = useDocsPreferredVersion(docsPluginId);
const latestVersion = useLatestVersion(docsPluginId);

const version = activeVersion ?? latestVersion;
const version = activeVersion ?? preferredVersion ?? latestVersion;

const doc = version.docs.find((versionDoc) => versionDoc.id === docId);
if (!doc) {
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 @@ -29,6 +30,10 @@ export default function DocsVersionDropdownNavbarItem({
const versions = useVersions(docsPluginId);
const latestVersion = useLatestVersion(docsPluginId);

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

function getItems() {
const versionLinks = versions.map((version) => {
// We try to link to the same doc, in another version
Expand All @@ -41,6 +46,9 @@ export default function DocsVersionDropdownNavbarItem({
label: version.label,
to: versionDoc.path,
isActive: () => version === activeDocContext?.activeVersion,
onClick: () => {
savePreferredVersionName(version.name);
},
};
});

Expand All @@ -60,7 +68,8 @@ export default function DocsVersionDropdownNavbarItem({
return items;
}

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
Expand Up @@ -9,6 +9,7 @@ import React from 'react';
import DefaultNavbarItem from './DefaultNavbarItem';
import {useActiveVersion, useLatestVersion} from '@theme/hooks/useDocs';
import type {Props} from '@theme/NavbarItem/DocsVersionNavbarItem';
import useDocsPreferredVersion from '../../utils/docsPreferredVersion/useDocsPreferredVersion';

const getVersionMainDoc = (version) =>
version.docs.find((doc) => doc.id === version.mainDocId);
Expand All @@ -20,8 +21,9 @@ export default function DocsVersionNavbarItem({
...props
}: Props): JSX.Element {
const activeVersion = useActiveVersion(docsPluginId);
const {preferredVersion} = useDocsPreferredVersion(docsPluginId);
const latestVersion = useLatestVersion(docsPluginId);
const version = activeVersion ?? latestVersion;
const version = activeVersion ?? preferredVersion ?? latestVersion;
const label = staticLabel ?? version.label;
const path = staticTo ?? getVersionMainDoc(version).path;
return <DefaultNavbarItem {...props} label={label} to={path} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* 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 {isDocsPluginEnabled} from '../docsUtils';

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;
}) {
if (isDocsPluginEnabled) {
return (
<DocsPreferredVersionContextProviderUnsafe>
{children}
</DocsPreferredVersionContextProviderUnsafe>
);
} else {
return <>{children}</>;
}
}

function DocsPreferredVersionContextProviderUnsafe({
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,34 @@
/**
* 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';

import {DEFAULT_PLUGIN_ID} from '@docusaurus/constants';

// 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;
}
Loading