diff --git a/src/components/NavEntry.tsx b/src/components/NavEntry.tsx index e9fa8a2..d6dd01a 100644 --- a/src/components/NavEntry.tsx +++ b/src/components/NavEntry.tsx @@ -1,10 +1,12 @@ import { NavItem } from '@patternfly/react-core' +import { kebabCase } from 'change-case' export interface TextContentEntry { id: string data: { id: string section: string + tab?: string } collection: string } @@ -18,8 +20,17 @@ export const NavEntry = ({ entry, isActive }: NavEntryProps) => { const { id } = entry const { id: entryTitle, section } = entry.data + const _id = + section === 'components' || section === 'layouts' + ? kebabCase(entryTitle) + : id return ( - + {entryTitle} ) diff --git a/src/components/NavSection.tsx b/src/components/NavSection.tsx index b6b8869..11b5422 100644 --- a/src/components/NavSection.tsx +++ b/src/components/NavSection.tsx @@ -1,6 +1,7 @@ import { NavExpandable } from '@patternfly/react-core' import { sentenceCase } from 'change-case' import { NavEntry, type TextContentEntry } from './NavEntry' +import { kebabCase } from 'change-case' interface NavSectionProps { entries: TextContentEntry[] @@ -21,8 +22,30 @@ export const NavSection = ({ const isActive = sortedNavEntries.some((entry) => entry.id === activeItem) - const items = sortedNavEntries.map((entry) => ( - + let navItems = sortedNavEntries + if (sectionId === 'components' || sectionId === 'layouts') { + // only display unique entry.data.id in the nav list if the section is components + navItems = [ + ...sortedNavEntries + .reduce((map, entry) => { + if (!map.has(entry.data.id)) { + map.set(entry.data.id, entry) + } + return map + }, new Map()) + .values(), + ] + } + + const items = navItems.map((entry) => ( + )) return ( diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index fb25860..f055015 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -13,7 +13,8 @@ export const Navigation: React.FunctionComponent = ({ const [activeItem, setActiveItem] = useState('') useEffect(() => { - setActiveItem(window.location.pathname.split('/').reverse()[0]) + // TODO: Needs an alternate solution because of /tab in the path + setActiveItem(window.location.pathname.split('/').reverse()[0]) }, []) const onNavSelect = ( diff --git a/src/components/Page.astro b/src/components/Page.astro index 91e99d1..609fe87 100644 --- a/src/components/Page.astro +++ b/src/components/Page.astro @@ -1,6 +1,6 @@ --- import styles from '@patternfly/react-styles/css/components/Page/page' -import { Content, PageSection } from '@patternfly/react-core' +import { PageSection } from '@patternfly/react-core' ---
@@ -10,9 +10,7 @@ import { Content, PageSection } from '@patternfly/react-core'
- - - +
diff --git a/src/components/__tests__/NavSection.test.tsx b/src/components/__tests__/NavSection.test.tsx index c3e97dd..73dee3c 100644 --- a/src/components/__tests__/NavSection.test.tsx +++ b/src/components/__tests__/NavSection.test.tsx @@ -20,6 +20,24 @@ const mockEntries: TextContentEntry[] = [ }, ] +const dupedEntries: TextContentEntry[] = [ + { + id: 'entry1', + data: { id: 'Entry1', section: 'components' }, + collection: 'react', + }, + { + id: 'entry2', + data: { id: 'Entry1', section: 'components' }, + collection: 'react', + }, + { + id: 'entry3', + data: { id: 'Entry2', section: 'components' }, + collection: 'react', + }, +] + it('renders without crashing', () => { render( { ) expect(asFragment()).toMatchSnapshot() }) + +it('dedupes and renders correct number of entries for components section', () => { + Object.defineProperty(window, 'location', { + value: { + pathname: '/foo/components', + }, + writable: true, + }) + + render( + , + ) + + expect(screen.getAllByRole('listitem')).toHaveLength(3) + expect(screen.getByRole('link', { name: 'Entry1' })).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Entry2' })).toBeInTheDocument() +}) + +it('dedupes and renders correct number of entries for layouts section', () => { + Object.defineProperty(window, 'location', { + value: { + pathname: '/foo/layouts', + }, + writable: true, + }) + + render( + , + ) + + expect(screen.getAllByRole('listitem')).toHaveLength(3) + expect(screen.getByRole('link', { name: 'Entry1' })).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Entry2' })).toBeInTheDocument() +}) diff --git a/src/content.config.ts b/src/content.config.ts index c62c59a..2f65ced 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -14,12 +14,20 @@ function defineContent(contentObj: CollectionDefinition) { return } + // TODO: Expand for other packages that remain under the react umbrella (Table, CodeEditor, etc) + const tabMap: any = { + 'react-component-docs': 'react', + 'core-component-docs': 'html' + }; + return defineCollection({ loader: glob({ base: dir, pattern }), schema: z.object({ id: z.string(), section: z.string(), + subsection: z.string().optional(), title: z.string().optional(), + tab: z.string().optional().default(tabMap[name]) }), }) } diff --git a/src/content.ts b/src/content.ts index 6a079ae..8d6f5f7 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1 +1,14 @@ -export const content = [{ base: 'textContent', pattern: '*.md', name: "textContent" }, { base: 'dir', pattern: '*.md', name: "dir" }] +export const content = [ + { base: 'textContent', pattern: '*.md', name: "textContent" } + // TODO: Remove. Uncomment for local testing. + // { + // "packageName":"@patternfly/react-core", + // "pattern":"**/examples/**/*.md", // had to update this pattern to bring in demos docs + // "name":"react-component-docs" + // }, + // { + // "packageName":"@patternfly/patternfly", + // "pattern":"**/examples/**/*.md", // had to update this pattern to bring in demos docs + // "name":"core-component-docs" + // } +] diff --git a/src/globals.ts b/src/globals.ts new file mode 100644 index 0000000..a2d80d9 --- /dev/null +++ b/src/globals.ts @@ -0,0 +1,53 @@ +export const componentTabs: any = {}; + +export const tabNames: any = { + 'react': 'React', + 'react-demos': 'React demos', + 'react-deprecated': 'React deprecated', + 'html': 'HTML', + 'html-demos': 'HTML demos' +}; + +export const buildTab = (entry: any, tab: string) => { + const tabEntry = componentTabs[entry.data.id] + + // if no dictionary entry exists, and tab data exists + if(tabEntry === undefined && tab) { + componentTabs[entry.data.id] = [tab] + // if dictionary entry & tab data exists, and entry does not include tab + } else if (tabEntry && tab && !tabEntry.includes(tab)) { + componentTabs[entry.data.id] = [...tabEntry, tab]; + } +} + +export const sortTabs = () => { + const defaultOrder = 50; + const sourceOrder: any = { + react: 1, + 'react-next': 1.1, + 'react-demos': 2, + 'react-deprecated': 2.1, + html: 3, + 'html-demos': 4, + 'design-guidelines': 99, + 'accessibility': 100, + 'upgrade-guide': 101, + 'release-notes': 102, + }; + + const sortSources = (s1: string, s2: string) => { + const s1Index = sourceOrder[s1] || defaultOrder; + const s2Index = sourceOrder[s2] || defaultOrder; + if (s1Index === defaultOrder && s2Index === defaultOrder) { + return s1.localeCompare(s2); + } + + return s1Index > s2Index ? 1 : -1; + } + + // Sort tabs entries based on above sort order + // Ensures all tabs are displayed in a consistent order & which tab gets displayed for a component route without a tab + Object.values(componentTabs).map((tabs: any) => { + tabs.sort(sortSources) + }) +} \ No newline at end of file diff --git a/src/pages/[section]/[...id].astro b/src/pages/[section]/[...page].astro similarity index 62% rename from src/pages/[section]/[...id].astro rename to src/pages/[section]/[...page].astro index 74470a4..550ff12 100644 --- a/src/pages/[section]/[...id].astro +++ b/src/pages/[section]/[...page].astro @@ -3,27 +3,34 @@ import { getCollection, render } from 'astro:content' import { Title } from '@patternfly/react-core' import MainLayout from '../../layouts/Main.astro' import { content } from "../../content" +import { kebabCase } from 'change-case' +import { componentTabs } from '../../globals' export async function getStaticPaths() { const collections = await Promise.all(content.map(async (entry) => await getCollection(entry.name as 'textContent'))) return collections.flat().map((entry) => ({ - params: { id: entry.id, section: entry.data.section }, + params: { page: kebabCase(entry.data.id), section: entry.data.section }, props: { entry, title: entry.data.title }, }) ) } + const { entry } = Astro.props -const { title } = entry.data +const { title, id, section } = entry.data const { Content } = await render(entry) + +if(section === 'components') { // if section is components, rewrite to first tab content + return Astro.rewrite(`/components/${kebabCase(id)}/${componentTabs[id][0]}`); +} --- { title && ( - {title} + {title} ) } diff --git a/src/pages/[section]/[page]/[...tab].astro b/src/pages/[section]/[page]/[...tab].astro new file mode 100644 index 0000000..6756e3b --- /dev/null +++ b/src/pages/[section]/[page]/[...tab].astro @@ -0,0 +1,71 @@ +--- +import { getCollection, render } from 'astro:content' +import { Title, PageSection, Content as PFContent } from '@patternfly/react-core' +import MainLayout from '../../../layouts/Main.astro' +import { content } from "../../../content" +import { kebabCase } from 'change-case' +import { componentTabs, tabNames, buildTab, sortTabs } from '../../../globals'; + +export async function getStaticPaths() { + const collections = await Promise.all(content.map(async (entry) => await getCollection(entry.name as 'textContent'))) + + const flatCol = collections.flat().map((entry) => { + // Build tabs dictionary + let tab = entry.data.tab; + if(tab) { // check for demos/deprecated + if(entry.id.includes('demos')) { + tab = `${tab}-demos`; + } else if (entry.id.includes('deprecated')) { + tab = `${tab}-deprecated`; + } + } + buildTab(entry, tab); + + return { + params: { page: kebabCase(entry.data.id), section: entry.data.section, tab }, + props: { entry, ...entry.data }, + } + }) + + sortTabs() + return flatCol; +} + +const { entry } = Astro.props +const { title, id, section } = entry.data +const { Content } = await render(entry) +const currentPath = Astro.url.pathname; +--- + + + { + title && ( + + {title} + + ) + } + {componentTabs[id] && ( + +
+
    + {componentTabs[id].map((tab: string) => ( + // eslint-disable-next-line react/jsx-key +
  • + + {tabNames[tab]} + +
  • + ))} +
+
+
+ )} + + + + + +