Skip to content

feat(Navigation): Add support for custom nav section ordering #35

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
44 changes: 44 additions & 0 deletions cli/__tests__/symLinkConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { symlink } from 'fs/promises'
import { symLinkConfig } from '../symLinkConfig'

jest.mock('fs/promises')

// suppress console.log so that it doesn't clutter the test output
jest.spyOn(console, 'log').mockImplementation(() => {})

it('should create a symlink successfully', async () => {
;(symlink as jest.Mock).mockResolvedValue(undefined)

await symLinkConfig('/astro', '/consumer')

expect(symlink).toHaveBeenCalledWith(
'/consumer/pf-docs.config.mjs',
'/astro/pf-docs.config.mjs',
)
})

it('should log an error if symlink creation fails', async () => {
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {})

const error = new Error('Symlink creation failed')
;(symlink as jest.Mock).mockRejectedValue(error)

await symLinkConfig('/astro', '/consumer')

expect(consoleErrorSpy).toHaveBeenCalledWith(
`Error creating symlink to /consumer/pf-docs.config.mjs in /astro`,
error,
)
})

it('should log a success message after creating the symlink', async () => {
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {})

await symLinkConfig('/astro', '/consumer')

expect(consoleLogSpy).toHaveBeenCalledWith(
`Symlink to /consumer/pf-docs.config.mjs in /astro created`,
)
})
2 changes: 2 additions & 0 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { setFsRootDir } from './setFsRootDir.js'
import { createConfigFile } from './createConfigFile.js'
import { updatePackageFile } from './updatePackageFile.js'
import { getConfig } from './getConfig.js'
import { symLinkConfig } from './symLinkConfig.js'

function updateContent(program: Command) {
const { verbose } = program.opts()
Expand Down Expand Up @@ -47,6 +48,7 @@ program.command('setup').action(async () => {

program.command('init').action(async () => {
await setFsRootDir(astroRoot, currentDir)
await symLinkConfig(astroRoot, currentDir)
console.log(
'\nInitialization complete, next update your pf-docs.config.mjs file and then run the `start` script to start the dev server',
)
Expand Down
21 changes: 21 additions & 0 deletions cli/symLinkConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* eslint-disable no-console */
import { symlink } from 'fs/promises'

export async function symLinkConfig(
astroRootDir: string,
consumerRootDir: string,
) {
const configFileName = '/pf-docs.config.mjs'
const docsConfigFile = consumerRootDir + configFileName

try {
await symlink(docsConfigFile, astroRootDir + configFileName)
} catch (e: any) {
console.error(
`Error creating symlink to ${docsConfigFile} in ${astroRootDir}`,
e,
)
} finally {
console.log(`Symlink to ${docsConfigFile} in ${astroRootDir} created`)
}
}
3 changes: 2 additions & 1 deletion cli/templates/pf-docs.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export const config = {
// name: "react-component-docs",
// },
],
outputDir: "./dist/docs"
outputDir: "./dist/docs",
navSectionOrder: ["get-started", "design-foundations"]
};
19 changes: 15 additions & 4 deletions src/components/Navigation.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@ import { getCollection } from 'astro:content'

import { Navigation as ReactNav } from './Navigation.tsx'

import { content } from "../content"
import { content } from '../content'

const collections = await Promise.all(content.map(async (entry) => await getCollection(entry.name as 'textContent')))
import { config } from '../pf-docs.config.mjs'

const navEntries = collections.flat();
const collections = await Promise.all(
content.map(
async (entry) => await getCollection(entry.name as 'textContent'),
),
)

const navEntries = collections.flat()
---

<ReactNav client:only="react" navEntries={navEntries} transition:animate="fade" />
<ReactNav
client:only="react"
navEntries={navEntries}
navSectionOrder={config.navSectionOrder}
transition:animate="fade"
/>
28 changes: 26 additions & 2 deletions src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { type TextContentEntry } from './NavEntry'

interface NavigationProps {
navEntries: TextContentEntry[]
navSectionOrder?: string[]
}

export const Navigation: React.FunctionComponent<NavigationProps> = ({
navEntries,
navSectionOrder,
}: NavigationProps) => {
const [activeItem, setActiveItem] = useState('')

Expand All @@ -23,9 +25,31 @@ export const Navigation: React.FunctionComponent<NavigationProps> = ({
setActiveItem(selectedItem.itemId.toString())
}

const sections = new Set(navEntries.map((entry) => entry.data.section))
const uniqueSections = Array.from(
new Set(navEntries.map((entry) => entry.data.section)),
)

// We want to list any ordered sections first, followed by any unordered sections sorted alphabetically
const [orderedSections, unorderedSections] = uniqueSections.reduce(
(acc, section) => {
if (!navSectionOrder) {
acc[1].push(section)
return acc
}

const index = navSectionOrder.indexOf(section)
if (index > -1) {
acc[0][index] = section
} else {
acc[1].push(section)
}
return acc
},
[[], []] as [string[], string[]],
)
const sortedSections = [...orderedSections, ...unorderedSections.sort()]

const navSections = Array.from(sections).map((section) => {
const navSections = sortedSections.map((section) => {
const entries = navEntries.filter((entry) => entry.data.section === section)

return (
Expand Down
67 changes: 60 additions & 7 deletions src/components/__tests__/Navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,46 @@ import { TextContentEntry } from '../NavEntry'
const mockEntries: TextContentEntry[] = [
{
id: 'entry1',
data: { id: 'Entry1', section: 'section1' },
data: { id: 'Entry1', section: 'section-one' },
collection: 'textContent',
},
{
id: 'entry2',
data: { id: 'Entry2', section: 'section1' },
data: { id: 'Entry2', section: 'section-two' },
collection: 'textContent',
},
{
id: 'entry3',
data: { id: 'Entry3', section: 'section2' },
data: { id: 'Entry3', section: 'section-two' },
collection: 'textContent',
},
{
id: 'entry4',
data: { id: 'Entry4', section: 'section-three' },
collection: 'textContent',
},
{
id: 'entry5',
data: { id: 'Entry5', section: 'section-four' },
collection: 'textContent',
},
]

it('renders without crashing', () => {
render(<Navigation navEntries={mockEntries} />)
expect(screen.getByText('Section1')).toBeInTheDocument()
expect(screen.getByText('Section2')).toBeInTheDocument()
expect(screen.getByText('Section one')).toBeInTheDocument()
expect(screen.getByText('Section two')).toBeInTheDocument()
})

it('renders the correct number of sections', () => {
render(<Navigation navEntries={mockEntries} />)
expect(screen.getAllByRole('listitem')).toHaveLength(2)
expect(screen.getAllByRole('listitem')).toHaveLength(4)
})

it('sets the active item based on the current pathname', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/section1/entry1',
pathname: '/section-one/entry1',
},
writable: true,
})
Expand All @@ -48,17 +58,60 @@ it('sets the active item based on the current pathname', () => {
})

it('updates the active item on selection', async () => {
// prevent errors when trying to navigate from logging in the console and cluttering the test output
jest.spyOn(console, 'error').mockImplementation(() => {})

const user = userEvent.setup()

render(<Navigation navEntries={mockEntries} />)

const sectionTwo = screen.getByRole('button', { name: 'Section two' })

await user.click(sectionTwo)

const entryLink = screen.getByRole('link', { name: 'Entry2' })

await user.click(entryLink)

expect(entryLink).toHaveClass('pf-m-current')
})

it('sorts all sections alphabetically by default', () => {
render(<Navigation navEntries={mockEntries} />)

const sections = screen.getAllByRole('button')

expect(sections[0]).toHaveTextContent('Section four')
expect(sections[1]).toHaveTextContent('Section one')
expect(sections[2]).toHaveTextContent('Section three')
expect(sections[3]).toHaveTextContent('Section two')
})

it('sorts sections based on the order provided', () => {
render(
<Navigation
navEntries={mockEntries}
navSectionOrder={['section-two', 'section-one']}
/>,
)

const sections = screen.getAllByRole('button')

expect(sections[0]).toHaveTextContent('Section two')
expect(sections[1]).toHaveTextContent('Section one')
})

it('sorts unordered sections alphabetically after ordered sections', () => {
render(
<Navigation navEntries={mockEntries} navSectionOrder={['section-two']} />,
)

const sections = screen.getAllByRole('button')

expect(sections[2]).toHaveTextContent('Section one')
expect(sections[3]).toHaveTextContent('Section three')
})

it('matches snapshot', () => {
const { asFragment } = render(<Navigation navEntries={mockEntries} />)
expect(asFragment()).toMatchSnapshot()
Expand Down
Loading