Skip to content

Commit 90f7fc2

Browse files
Merge pull request #35 from patternfly/customize-nav-ordering
feat(Navigation): Add support for custom nav section ordering
2 parents fb6a78e + e17b966 commit 90f7fc2

File tree

9 files changed

+335
-30
lines changed

9 files changed

+335
-30
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { symlink } from 'fs/promises'
2+
import { symLinkConfig } from '../symLinkConfig'
3+
4+
jest.mock('fs/promises')
5+
6+
// suppress console.log so that it doesn't clutter the test output
7+
jest.spyOn(console, 'log').mockImplementation(() => {})
8+
9+
it('should create a symlink successfully', async () => {
10+
;(symlink as jest.Mock).mockResolvedValue(undefined)
11+
12+
await symLinkConfig('/astro', '/consumer')
13+
14+
expect(symlink).toHaveBeenCalledWith(
15+
'/consumer/pf-docs.config.mjs',
16+
'/astro/pf-docs.config.mjs',
17+
)
18+
})
19+
20+
it('should log an error if symlink creation fails', async () => {
21+
const consoleErrorSpy = jest
22+
.spyOn(console, 'error')
23+
.mockImplementation(() => {})
24+
25+
const error = new Error('Symlink creation failed')
26+
;(symlink as jest.Mock).mockRejectedValue(error)
27+
28+
await symLinkConfig('/astro', '/consumer')
29+
30+
expect(consoleErrorSpy).toHaveBeenCalledWith(
31+
`Error creating symlink to /consumer/pf-docs.config.mjs in /astro`,
32+
error,
33+
)
34+
})
35+
36+
it('should log a success message after creating the symlink', async () => {
37+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
38+
39+
await symLinkConfig('/astro', '/consumer')
40+
41+
expect(consoleLogSpy).toHaveBeenCalledWith(
42+
`Symlink to /consumer/pf-docs.config.mjs in /astro created`,
43+
)
44+
})

cli/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { setFsRootDir } from './setFsRootDir.js'
88
import { createConfigFile } from './createConfigFile.js'
99
import { updatePackageFile } from './updatePackageFile.js'
1010
import { getConfig } from './getConfig.js'
11+
import { symLinkConfig } from './symLinkConfig.js'
1112
import { buildPropsData } from './buildPropsData.js'
1213
import { hasFile } from './hasFile.js'
1314

@@ -80,6 +81,7 @@ program.command('setup').action(async () => {
8081

8182
program.command('init').action(async () => {
8283
await setFsRootDir(astroRoot, currentDir)
84+
await symLinkConfig(astroRoot, currentDir)
8385
console.log(
8486
'\nInitialization complete, next update your pf-docs.config.mjs file and then run the `start` script to start the dev server',
8587
)

cli/symLinkConfig.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* eslint-disable no-console */
2+
import { symlink } from 'fs/promises'
3+
4+
export async function symLinkConfig(
5+
astroRootDir: string,
6+
consumerRootDir: string,
7+
) {
8+
const configFileName = '/pf-docs.config.mjs'
9+
const docsConfigFile = consumerRootDir + configFileName
10+
11+
try {
12+
await symlink(docsConfigFile, astroRootDir + configFileName)
13+
} catch (e: any) {
14+
console.error(
15+
`Error creating symlink to ${docsConfigFile} in ${astroRootDir}`,
16+
e,
17+
)
18+
} finally {
19+
console.log(`Symlink to ${docsConfigFile} in ${astroRootDir} created`)
20+
}
21+
}

cli/templates/pf-docs.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const config = {
1616
// name: "react-component-docs",
1717
// },
1818
],
19+
navSectionOrder: ["get-started", "design-foundations"],
1920
outputDir: './dist/docs',
2021
propsGlobs: [
2122
// {

src/components/Navigation.astro

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,22 @@ import { getCollection } from 'astro:content'
33
44
import { Navigation as ReactNav } from './Navigation.tsx'
55
6-
import { content } from "../content"
6+
import { content } from '../content'
77
8-
const collections = await Promise.all(content.map(async (entry) => await getCollection(entry.name as 'textContent')))
8+
import { config } from '../pf-docs.config.mjs'
99
10-
const navEntries = collections.flat();
10+
const collections = await Promise.all(
11+
content.map(
12+
async (entry) => await getCollection(entry.name as 'textContent'),
13+
),
14+
)
15+
16+
const navEntries = collections.flat()
1117
---
1218

13-
<ReactNav client:only="react" navEntries={navEntries} transition:animate="fade" />
19+
<ReactNav
20+
client:only="react"
21+
navEntries={navEntries}
22+
navSectionOrder={config.navSectionOrder}
23+
transition:animate="fade"
24+
/>

src/components/Navigation.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { type TextContentEntry } from './NavEntry'
55

66
interface NavigationProps {
77
navEntries: TextContentEntry[]
8+
navSectionOrder?: string[]
89
}
910

1011
export const Navigation: React.FunctionComponent<NavigationProps> = ({
1112
navEntries,
13+
navSectionOrder,
1214
}: NavigationProps) => {
1315
const [activeItem, setActiveItem] = useState('')
1416

@@ -24,9 +26,31 @@ export const Navigation: React.FunctionComponent<NavigationProps> = ({
2426
setActiveItem(selectedItem.itemId.toString())
2527
}
2628

27-
const sections = new Set(navEntries.map((entry) => entry.data.section))
29+
const uniqueSections = Array.from(
30+
new Set(navEntries.map((entry) => entry.data.section)),
31+
)
32+
33+
// We want to list any ordered sections first, followed by any unordered sections sorted alphabetically
34+
const [orderedSections, unorderedSections] = uniqueSections.reduce(
35+
(acc, section) => {
36+
if (!navSectionOrder) {
37+
acc[1].push(section)
38+
return acc
39+
}
40+
41+
const index = navSectionOrder.indexOf(section)
42+
if (index > -1) {
43+
acc[0][index] = section
44+
} else {
45+
acc[1].push(section)
46+
}
47+
return acc
48+
},
49+
[[], []] as [string[], string[]],
50+
)
51+
const sortedSections = [...orderedSections, ...unorderedSections.sort()]
2852

29-
const navSections = Array.from(sections).map((section) => {
53+
const navSections = sortedSections.map((section) => {
3054
const entries = navEntries.filter((entry) => entry.data.section === section)
3155

3256
return (

src/components/__tests__/Navigation.test.tsx

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,46 @@ import { TextContentEntry } from '../NavEntry'
66
const mockEntries: TextContentEntry[] = [
77
{
88
id: 'entry1',
9-
data: { id: 'Entry1', section: 'section1' },
9+
data: { id: 'Entry1', section: 'section-one' },
1010
collection: 'textContent',
1111
},
1212
{
1313
id: 'entry2',
14-
data: { id: 'Entry2', section: 'section1' },
14+
data: { id: 'Entry2', section: 'section-two' },
1515
collection: 'textContent',
1616
},
1717
{
1818
id: 'entry3',
19-
data: { id: 'Entry3', section: 'section2' },
19+
data: { id: 'Entry3', section: 'section-two' },
20+
collection: 'textContent',
21+
},
22+
{
23+
id: 'entry4',
24+
data: { id: 'Entry4', section: 'section-three' },
25+
collection: 'textContent',
26+
},
27+
{
28+
id: 'entry5',
29+
data: { id: 'Entry5', section: 'section-four' },
2030
collection: 'textContent',
2131
},
2232
]
2333

2434
it('renders without crashing', () => {
2535
render(<Navigation navEntries={mockEntries} />)
26-
expect(screen.getByText('Section1')).toBeInTheDocument()
27-
expect(screen.getByText('Section2')).toBeInTheDocument()
36+
expect(screen.getByText('Section one')).toBeInTheDocument()
37+
expect(screen.getByText('Section two')).toBeInTheDocument()
2838
})
2939

3040
it('renders the correct number of sections', () => {
3141
render(<Navigation navEntries={mockEntries} />)
32-
expect(screen.getAllByRole('listitem')).toHaveLength(2)
42+
expect(screen.getAllByRole('listitem')).toHaveLength(4)
3343
})
3444

3545
it('sets the active item based on the current pathname', () => {
3646
Object.defineProperty(window, 'location', {
3747
value: {
38-
pathname: '/section1/entry1',
48+
pathname: '/section-one/entry1',
3949
},
4050
writable: true,
4151
})
@@ -48,17 +58,60 @@ it('sets the active item based on the current pathname', () => {
4858
})
4959

5060
it('updates the active item on selection', async () => {
61+
// prevent errors when trying to navigate from logging in the console and cluttering the test output
62+
jest.spyOn(console, 'error').mockImplementation(() => {})
63+
5164
const user = userEvent.setup()
5265

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

68+
const sectionTwo = screen.getByRole('button', { name: 'Section two' })
69+
70+
await user.click(sectionTwo)
71+
5572
const entryLink = screen.getByRole('link', { name: 'Entry2' })
5673

5774
await user.click(entryLink)
5875

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

79+
it('sorts all sections alphabetically by default', () => {
80+
render(<Navigation navEntries={mockEntries} />)
81+
82+
const sections = screen.getAllByRole('button')
83+
84+
expect(sections[0]).toHaveTextContent('Section four')
85+
expect(sections[1]).toHaveTextContent('Section one')
86+
expect(sections[2]).toHaveTextContent('Section three')
87+
expect(sections[3]).toHaveTextContent('Section two')
88+
})
89+
90+
it('sorts sections based on the order provided', () => {
91+
render(
92+
<Navigation
93+
navEntries={mockEntries}
94+
navSectionOrder={['section-two', 'section-one']}
95+
/>,
96+
)
97+
98+
const sections = screen.getAllByRole('button')
99+
100+
expect(sections[0]).toHaveTextContent('Section two')
101+
expect(sections[1]).toHaveTextContent('Section one')
102+
})
103+
104+
it('sorts unordered sections alphabetically after ordered sections', () => {
105+
render(
106+
<Navigation navEntries={mockEntries} navSectionOrder={['section-two']} />,
107+
)
108+
109+
const sections = screen.getAllByRole('button')
110+
111+
expect(sections[2]).toHaveTextContent('Section one')
112+
expect(sections[3]).toHaveTextContent('Section three')
113+
})
114+
62115
it('matches snapshot', () => {
63116
const { asFragment } = render(<Navigation navEntries={mockEntries} />)
64117
expect(asFragment()).toMatchSnapshot()

0 commit comments

Comments
 (0)