diff --git a/cli/__tests__/symLinkConfig.test.ts b/cli/__tests__/symLinkConfig.test.ts
new file mode 100644
index 0000000..e9f5152
--- /dev/null
+++ b/cli/__tests__/symLinkConfig.test.ts
@@ -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`,
+ )
+})
diff --git a/cli/cli.ts b/cli/cli.ts
index 0fb3950..caa0f83 100755
--- a/cli/cli.ts
+++ b/cli/cli.ts
@@ -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'
import { buildPropsData } from './buildPropsData.js'
import { hasFile } from './hasFile.js'
@@ -80,6 +81,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',
)
diff --git a/cli/symLinkConfig.ts b/cli/symLinkConfig.ts
new file mode 100644
index 0000000..9f267c0
--- /dev/null
+++ b/cli/symLinkConfig.ts
@@ -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`)
+ }
+}
diff --git a/cli/templates/pf-docs.config.mjs b/cli/templates/pf-docs.config.mjs
index 2a44d1e..c769289 100644
--- a/cli/templates/pf-docs.config.mjs
+++ b/cli/templates/pf-docs.config.mjs
@@ -16,6 +16,7 @@ export const config = {
// name: "react-component-docs",
// },
],
+ navSectionOrder: ["get-started", "design-foundations"],
outputDir: './dist/docs',
propsGlobs: [
// {
diff --git a/src/components/Navigation.astro b/src/components/Navigation.astro
index c07c7a0..02feb92 100644
--- a/src/components/Navigation.astro
+++ b/src/components/Navigation.astro
@@ -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()
---
-
+
diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx
index f055015..b7fb42c 100644
--- a/src/components/Navigation.tsx
+++ b/src/components/Navigation.tsx
@@ -5,10 +5,12 @@ import { type TextContentEntry } from './NavEntry'
interface NavigationProps {
navEntries: TextContentEntry[]
+ navSectionOrder?: string[]
}
export const Navigation: React.FunctionComponent = ({
navEntries,
+ navSectionOrder,
}: NavigationProps) => {
const [activeItem, setActiveItem] = useState('')
@@ -24,9 +26,31 @@ export const Navigation: React.FunctionComponent = ({
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 (
diff --git a/src/components/__tests__/Navigation.test.tsx b/src/components/__tests__/Navigation.test.tsx
index 553f1fe..13893c5 100644
--- a/src/components/__tests__/Navigation.test.tsx
+++ b/src/components/__tests__/Navigation.test.tsx
@@ -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()
- 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()
- 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,
})
@@ -48,10 +58,17 @@ 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()
+ const sectionTwo = screen.getByRole('button', { name: 'Section two' })
+
+ await user.click(sectionTwo)
+
const entryLink = screen.getByRole('link', { name: 'Entry2' })
await user.click(entryLink)
@@ -59,6 +76,42 @@ it('updates the active item on selection', async () => {
expect(entryLink).toHaveClass('pf-m-current')
})
+it('sorts all sections alphabetically by default', () => {
+ render()
+
+ 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(
+ ,
+ )
+
+ 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(
+ ,
+ )
+
+ const sections = screen.getAllByRole('button')
+
+ expect(sections[2]).toHaveTextContent('Section one')
+ expect(sections[3]).toHaveTextContent('Section three')
+})
+
it('matches snapshot', () => {
const { asFragment } = render()
expect(asFragment()).toMatchSnapshot()
diff --git a/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap b/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap
index fbf910f..47283dd 100644
--- a/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap
@@ -9,7 +9,7 @@ exports[`matches snapshot 1`] = `