diff --git a/gatsby-node.js b/gatsby-node.js index b096e9f65..81ed92f2e 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -196,6 +196,26 @@ exports.createPages = async ({ actions }) => { throw err; } + // get remote metadata for updated ToC in Atlas + let metadata = siteMetadata; + try { + const filter = { + project: manifestMetadata.project, + branch: manifestMetadata.branch, + }; + if (isAssociatedProduct || manifestMetadata?.associated_products.length) { + filter['is_merged_toc'] = true; + } + const findOptions = { + sort: { build_id: -1 }, + }; + metadata = await db.stitchInterface.getMetadata(filter, findOptions); + } catch (e) { + console.error('Error while fetching metadata from Atlas, falling back to manifest metadata'); + console.error(e); + metadata = siteMetadata; + } + return new Promise((resolve, reject) => { PAGES.forEach((page) => { const pageNodes = RESOLVED_REF_DOC_MAPPING[page]?.ast; @@ -214,6 +234,7 @@ exports.createPages = async ({ actions }) => { slug, repoBranches, associatedReposInfo, + remoteMetadata: metadata, isAssociatedProduct, template: pageNodes?.options?.template, page: pageNodes, diff --git a/src/components/RootProvider.js b/src/components/RootProvider.js index 912158f9f..8696f1962 100644 --- a/src/components/RootProvider.js +++ b/src/components/RootProvider.js @@ -16,6 +16,7 @@ const RootProvider = ({ repoBranches, associatedReposInfo, isAssociatedProduct, + remoteMetadata, }) => ( @@ -26,7 +27,7 @@ const RootProvider = ({ associatedReposInfo={associatedReposInfo} isAssociatedProduct={isAssociatedProduct} > - + {children} diff --git a/src/components/Sidenav/TOCNode.js b/src/components/Sidenav/TOCNode.js index 9c30442a2..57174c2ba 100644 --- a/src/components/Sidenav/TOCNode.js +++ b/src/components/Sidenav/TOCNode.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { css } from '@emotion/react'; import { cx, css as LeafyCSS } from '@leafygreen-ui/emotion'; @@ -8,6 +8,7 @@ import Box from '@leafygreen-ui/box'; import Icon from '@leafygreen-ui/icon'; import { theme } from '../../theme/docsTheme'; import Link from '../Link'; +import { VersionContext } from '../../context/version-context'; import { formatText } from '../../utils/format-text'; import { isActiveTocNode } from '../../utils/is-active-toc-node'; import { isSelectedTocNode } from '../../utils/is-selected-toc-node'; @@ -38,13 +39,26 @@ const wrapperStyle = LeafyCSS` flex: 1 1 auto; } `; + +const overwriteLinkStyle = LeafyCSS` + span { + display: flex; + } +`; /** * * @param {hasVersions} boolean * @returns Wrapper for Version Selector, or returns children */ -const Wrapper = ({ children, hasVersions }) => { - return hasVersions ? {children} : <>{children}; +const Wrapper = ({ children, hasVersions, project, versions }) => { + return hasVersions ? ( + + {children} + {hasVersions && } + + ) : ( + <>{children} + ); }; /** @@ -53,12 +67,13 @@ const Wrapper = ({ children, hasVersions }) => { */ const TOCNode = ({ activeSection, handleClick, level = BASE_NODE_LEVEL, node }) => { const { title, slug, url, children, options = {} } = node; - const target = slug || url; + const { activeVersions } = useContext(VersionContext); + const target = options.urls?.[activeVersions[options.project]] || slug || url; const hasChildren = !!children?.length; - const hasVersions = !!(options?.versions?.length > 1); // in the event there is only one version, do we show version selector? + const hasVersions = !!(options?.versions?.length > 1); const isActive = isActiveTocNode(activeSection, slug, children); const isSelected = isSelectedTocNode(activeSection, slug); - const isDrawer = !!(options && options.drawer); + const isDrawer = !!(options && (options.drawer || options.versions)); // TODO: convert versions option to drawer in backend const isTocIcon = !!(options.tocicon === 'sync'); const [isOpen, setIsOpen] = useState(isActive); @@ -94,28 +109,31 @@ const TOCNode = ({ activeSection, handleClick, level = BASE_NODE_LEVEL, node }) if (isDrawer && hasChildren) { return ( - { - setIsOpen(!isOpen); - }} - > - - {isTocIcon && } - {formattedTitle} - + + { + setIsOpen(!isOpen); + }} + > + + {isTocIcon && } + {formattedTitle} + + ); } return ( - + { setIsOpen(!isOpen); }} + hideExternalIcon={true} > {hasChildren && ( @@ -123,7 +141,6 @@ const TOCNode = ({ activeSection, handleClick, level = BASE_NODE_LEVEL, node }) {isTocIcon && } {formattedTitle} - {hasVersions && } ); }; diff --git a/src/components/Sidenav/Toctree.js b/src/components/Sidenav/Toctree.js index d1c89db1e..51277d406 100644 --- a/src/components/Sidenav/Toctree.js +++ b/src/components/Sidenav/Toctree.js @@ -6,7 +6,7 @@ const Toctree = ({ handleClick, slug, toctree: { children } }) => { return ( <> {children.map((c) => ( - + ))} ); diff --git a/src/components/Sidenav/VersionSelector.js b/src/components/Sidenav/VersionSelector.js index ffb37d5e7..ac1483853 100644 --- a/src/components/Sidenav/VersionSelector.js +++ b/src/components/Sidenav/VersionSelector.js @@ -11,8 +11,8 @@ const buildChoice = (branch) => { }; }; -const buildChoices = (branches) => { - return !branches ? [] : branches.map(buildChoice); +const buildChoices = (branches, tocVersionNames) => { + return !branches ? [] : branches.filter((branch) => tocVersionNames.includes(branch.gitBranchName)).map(buildChoice); }; const StyledSelect = styled(Select)` @@ -39,17 +39,13 @@ const StyledSelect = styled(Select)` } `; -const VersionSelector = ({ versionedProject = '' }) => { - // verify if this version selector is for current product - // determines if we should use reach router or not - // ie. on atlas-cli v1.3 , switch to v1.0 -> should update link (what if link is 404) +const VersionSelector = ({ versionedProject = '', tocVersionNames = [] }) => { const { activeVersions, availableVersions, onVersionSelect } = useContext(VersionContext); - - const [options, setOptions] = useState(buildChoices(availableVersions[versionedProject])); + const [options, setOptions] = useState(buildChoices(availableVersions[versionedProject], tocVersionNames)); useEffect(() => { - setOptions(buildChoices(availableVersions[versionedProject])); - }, [availableVersions, versionedProject]); + setOptions(buildChoices(availableVersions[versionedProject], tocVersionNames)); + }, [availableVersions, tocVersionNames, versionedProject]); const onChange = useCallback( ({ value }) => { diff --git a/src/context/toc-context.js b/src/context/toc-context.js index 743354b0d..ea8489852 100644 --- a/src/context/toc-context.js +++ b/src/context/toc-context.js @@ -2,7 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import { useCallback } from 'react'; import { METADATA_COLLECTION } from '../build-constants'; import { useSiteMetadata } from '../hooks/use-site-metadata'; -import { fetchDocument } from '../utils/realm'; +import { fetchDocuments } from '../utils/realm'; import useSnootyMetadata from '../utils/use-snooty-metadata'; import { VersionContext } from './version-context'; @@ -12,30 +12,36 @@ const TocContext = createContext({ // ToC context that provides ToC content in form of *above* // filters all available ToC by currently selected version via VersionContext -const TocContextProvider = ({ children }) => { - const { activeVersions, setActiveVersions } = useContext(VersionContext); - const { toctree } = useSnootyMetadata(); - const { database, project } = useSiteMetadata(); +const TocContextProvider = ({ children, remoteMetadata }) => { + const { activeVersions, setActiveVersions, showVersionDropdown } = useContext(VersionContext); + const { toctree, associated_products: associatedProducts } = useSnootyMetadata(); + const { database, project, parserBranch } = useSiteMetadata(); const [remoteToc, setRemoteToc] = useState(); - const [activeToc, setActiveToc] = useState(toctree); + const [activeToc, setActiveToc] = useState(remoteMetadata?.toctree || toctree); const getTocMetadata = useCallback(async () => { try { - // TODO: update metadata to have 'project' field. don't have to construct page_id - // NOTE: see snooty_dev.metadata for documents with 'project' field defined. input testing metadata - let filter = { + const filter = { project: `${project}`, + branch: parserBranch, }; - let db = database; - const metadata = await fetchDocument(db, METADATA_COLLECTION, filter); - return metadata.toctree; + const findOptions = { + sort: { build_id: -1 }, + }; + + if (associatedProducts?.length || showVersionDropdown) { + filter['is_merged_toc'] = true; + } + const db = database; + const metadata = await fetchDocuments(db, METADATA_COLLECTION, filter, undefined, findOptions); + return metadata[0]?.toctree ?? toctree; } catch (e) { // fallback to toctree from build time console.error(e); - return toctree; + return remoteMetadata?.toctree || toctree; } // below dependents are server constants - }, [database, project, toctree]); + }, [project, parserBranch, associatedProducts, showVersionDropdown, database, toctree, remoteMetadata]); const getFilteredToc = useCallback(() => { // filter remoteToc by activeVersions and return a copy @@ -64,7 +70,7 @@ const TocContextProvider = ({ children }) => { } } - if (clonedNode.children.length) { + if (clonedNode.children?.length) { clonedToc.children.push(clonedNode); } } diff --git a/src/init/DocumentDatabase.js b/src/init/DocumentDatabase.js index 3fcdc9de1..ee23cd9a9 100644 --- a/src/init/DocumentDatabase.js +++ b/src/init/DocumentDatabase.js @@ -39,8 +39,14 @@ class StitchInterface { return this.stitchClient.callFunction('fetchDocuments', [DB, collection, buildFilter]); } - async getMetadata(buildFilter) { - return this.stitchClient.callFunction('fetchDocument', [DB, METADATA_COLLECTION, buildFilter]); + async getMetadata(buildFilter, findOptions) { + return this.stitchClient.callFunction('fetchDocument', [ + DB, + METADATA_COLLECTION, + buildFilter, + undefined, + findOptions, + ]); } } diff --git a/src/layouts/index.js b/src/layouts/index.js index 9171ec9a1..77cc2f26b 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -76,7 +76,7 @@ const GlobalGrid = styled('div')` const DefaultLayout = ({ children, - pageContext: { page, slug, repoBranches, template, associatedReposInfo, isAssociatedProduct }, + pageContext: { page, slug, repoBranches, template, associatedReposInfo, isAssociatedProduct, remoteMetadata }, }) => { const { sidenav } = getTemplate(template); const { chapters, guides, publishedBranches, slugToTitle, title, toctree, eol } = useSnootyMetadata(); @@ -95,6 +95,7 @@ const DefaultLayout = ({ headingNodes={page?.options?.headings} selectors={page?.options?.selectors} isAssociatedProduct={isAssociatedProduct} + remoteMetadata={remoteMetadata} >
diff --git a/src/utils/realm.js b/src/utils/realm.js index 93c710c7b..a5403c8e6 100644 --- a/src/utils/realm.js +++ b/src/utils/realm.js @@ -47,10 +47,10 @@ export const fetchOASFile = async (apiName, database) => { return fetchData('fetchOASFile', apiName, database); }; -export const fetchDocument = async (database, collectionName, query) => { - return fetchData('fetchDocument', database, collectionName, query); +export const fetchDocument = async (database, collectionName, query, projections) => { + return fetchData('fetchDocument', database, collectionName, query, projections); }; -export const fetchDocuments = async (database, collectionName, query) => { - return fetchData('fetchDocuments', database, collectionName, query); +export const fetchDocuments = async (database, collectionName, query, projections, options) => { + return fetchData('fetchDocuments', database, collectionName, query, projections, options); }; diff --git a/tests/context/toc-context.test.js b/tests/context/toc-context.test.js index 0fc9dd5f7..75c9fbe39 100644 --- a/tests/context/toc-context.test.js +++ b/tests/context/toc-context.test.js @@ -11,7 +11,7 @@ import { TocContext, TocContextProvider } from '../../src/context/toc-context'; // <------------------ START test data mocks ------------------> const siteMetadataMock = jest.spyOn(siteMetadata, 'useSiteMetadata'); const snootyMetadataMock = jest.spyOn(snootyMetadata, 'default'); -const realmMock = jest.spyOn(realm, 'fetchDocument'); +const realmMock = jest.spyOn(realm, 'fetchDocuments'); const project = 'cloud-docs'; let sampleTocTree, responseTree, mockedResponse; @@ -33,10 +33,12 @@ const setResponse = () => { }, { title: [{ type: 'text', position: { start: { line: 1 } }, value: 'sample page3' }], - slug: 'sample-page3', options: { versions: ['v1.0'], project: 'atlas-cli', + urls: { + 'v1.0': 'sample-page3-v1.0', + }, }, children: [ { @@ -56,6 +58,11 @@ const setResponse = () => { responseTree.children[responseTree.children.length - 1].options = { versions: ['v1.0', 'v1.2', 'v0.8'], project: 'atlas-cli', + urls: { + 'v1.0': 'sample-page3-v1.0', + 'v1.2': 'sample-page3-v1.2', + 'v0.8': 'sample-page3-v0.8', + }, }; responseTree.children[responseTree.children.length - 1].children.push({ title: [{ type: 'text', position: { start: { line: 1 } }, value: 'sample child v1.2' }], @@ -73,9 +80,11 @@ const setResponse = () => { }, }); - mockedResponse = { - toctree: responseTree, - }; + mockedResponse = [ + { + toctree: responseTree, + }, + ]; }; const setMocks = () => {