From 6ec75e894b8e5e07cde7d9d97bab6a6fe5b8fcff Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Thu, 25 Apr 2024 13:45:31 -0700 Subject: [PATCH] support breadcrumb style catch-all routes --- .../next/src/server/app-render/app-render.tsx | 17 +++++-- .../app/@slot/[...catchAll]/page.tsx | 12 +++++ .../app/@slot/default.tsx | 3 ++ .../app/[artist]/[album]/[track]/page.tsx | 10 ++++ .../app/[artist]/[album]/page.tsx | 20 ++++++++ .../app/[artist]/page.tsx | 17 +++++++ .../app/layout.tsx | 18 +++++++ .../parallel-routes-breadcrumbs/app/page.tsx | 17 +++++++ .../next.config.js | 6 +++ .../parallel-routes-breadcrumbs.test.ts | 47 +++++++++++++++++++ 10 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/[...catchAll]/page.tsx create mode 100644 test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/default.tsx create mode 100644 test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/[album]/[track]/page.tsx create mode 100644 test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/[album]/page.tsx create mode 100644 test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/page.tsx create mode 100644 test/e2e/app-dir/parallel-routes-breadcrumbs/app/layout.tsx create mode 100644 test/e2e/app-dir/parallel-routes-breadcrumbs/app/page.tsx create mode 100644 test/e2e/app-dir/parallel-routes-breadcrumbs/next.config.js create mode 100644 test/e2e/app-dir/parallel-routes-breadcrumbs/parallel-routes-breadcrumbs.test.ts diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 95560e8aa8fc3..8a598b6cc3737 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -234,15 +234,24 @@ function makeGetDynamicParamFromSegment( } if (!value) { - // Handle case where optional catchall does not have a value, e.g. `/dashboard/[...slug]` when requesting `/dashboard` - if (segmentParam.type === 'optional-catchall') { + // Handle case where optional catchall does not have a value, e.g. `/dashboard/[[...slug]]` when requesting `/dashboard` + if ( + segmentParam.type === 'optional-catchall' || + segmentParam.type === 'catchall' + ) { + // If we weren't able to match the segment to a URL param, and we have a catch-all route, + // provide all of the known params (in array format) to the route + // It should be safe to assume the order of these params is consistent with the order of the segments. + // However, if not, we could re-parse the `pagePath` with `getRouteRegex` and iterate over the positional order. + value = Object.values(params).map((i) => encodeURIComponent(i)) + const hasValues = value.length > 0 const type = dynamicParamTypes[segmentParam.type] return { param: key, - value: null, + value: hasValues ? value : null, type: type, // This value always has to be a string. - treeSegment: [key, '', type], + treeSegment: [key, hasValues ? value.join('/') : '', type], } } return findDynamicParamFromRouterState(flightRouterState, segment) diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/[...catchAll]/page.tsx b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/[...catchAll]/page.tsx new file mode 100644 index 0000000000000..f9c359f5c0712 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/[...catchAll]/page.tsx @@ -0,0 +1,12 @@ +export default function Page({ params: { catchAll } }) { + return ( +
+

Parallel Route!

+ +
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/default.tsx b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/default.tsx new file mode 100644 index 0000000000000..c17431379f962 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/default.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return null +} diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/[album]/[track]/page.tsx b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/[album]/[track]/page.tsx new file mode 100644 index 0000000000000..c957176c3daf6 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/[album]/[track]/page.tsx @@ -0,0 +1,10 @@ +import Link from 'next/link' + +export default function Page({ params }) { + return ( +
+

Track: {params.track}

+ Back to album +
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/[album]/page.tsx b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/[album]/page.tsx new file mode 100644 index 0000000000000..29184ed8c515f --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/[album]/page.tsx @@ -0,0 +1,20 @@ +import Link from 'next/link' + +export default function Page({ params }) { + const tracks = ['track1', 'track2', 'track3'] + return ( +
+

Album: {params.album}

+ + Back to artist +
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/page.tsx b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/page.tsx new file mode 100644 index 0000000000000..7396f3914b7fd --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/[artist]/page.tsx @@ -0,0 +1,17 @@ +import Link from 'next/link' + +export default function Page({ params }) { + const albums = ['album1', 'album2', 'album3'] + return ( +
+

Artist: {params.artist}

+ +
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/app/layout.tsx b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/layout.tsx new file mode 100644 index 0000000000000..83d72bedde130 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/layout.tsx @@ -0,0 +1,18 @@ +import React from 'react' + +export default function Root({ + children, + slot, +}: { + children: React.ReactNode + slot: React.ReactNode +}) { + return ( + + +
{slot}
+
{children}
+ + + ) +} diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/app/page.tsx b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/page.tsx new file mode 100644 index 0000000000000..5c3feea5964d4 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/page.tsx @@ -0,0 +1,17 @@ +import Link from 'next/link' + +export default async function Home() { + const artists = ['artist1', 'artist2', 'artist3'] + return ( +
+

Artists

+ +
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/next.config.js b/test/e2e/app-dir/parallel-routes-breadcrumbs/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/parallel-routes-breadcrumbs.test.ts b/test/e2e/app-dir/parallel-routes-breadcrumbs/parallel-routes-breadcrumbs.test.ts new file mode 100644 index 0000000000000..63722ed214f11 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/parallel-routes-breadcrumbs.test.ts @@ -0,0 +1,47 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('parallel-routes-breadcrumbs', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should provide an unmatched catch-all route with params', async () => { + const browser = await next.browser('/') + await browser.elementByCss("[href='/artist1']").click() + + const slot = await browser.waitForElementByCss('#slot') + + // verify page is rendering the params + expect(await browser.elementByCss('h2').text()).toBe('Artist: artist1') + + // verify slot is rendering the params + expect(await slot.text()).toContain('Artist: artist1') + expect(await slot.text()).toContain('Album: Select an album') + expect(await slot.text()).toContain('Track: Select a track') + + await browser.elementByCss("[href='/artist1/album2']").click() + + await retry(async () => { + // verify page is rendering the params + expect(await browser.elementByCss('h2').text()).toBe('Album: album2') + }) + + // verify slot is rendering the params + expect(await slot.text()).toContain('Artist: artist1') + expect(await slot.text()).toContain('Album: album2') + expect(await slot.text()).toContain('Track: Select a track') + + await browser.elementByCss("[href='/artist1/album2/track3']").click() + + await retry(async () => { + // verify page is rendering the params + expect(await browser.elementByCss('h2').text()).toBe('Track: track3') + }) + + // verify slot is rendering the params + expect(await slot.text()).toContain('Artist: artist1') + expect(await slot.text()).toContain('Album: album2') + expect(await slot.text()).toContain('Track: track3') + }) +})