diff --git a/src/main/frontend/app/router.tsx b/src/main/frontend/app/router.tsx index 323490d1..d84bd9dd 100644 --- a/src/main/frontend/app/router.tsx +++ b/src/main/frontend/app/router.tsx @@ -1,13 +1,20 @@ -import { createBrowserRouter } from 'react-router' +import { createBrowserRouter, isRouteErrorResponse, useRouteError } from 'react-router' import ConfigurationOverview from '~/routes/configurations/configuration-overview' import CodeEditor from '~/routes/editor/editor' import Help from '~/routes/help/help' +import NotFound from '~/routes/notfound/not-found' import Settings from '~/routes/settings/settings' import Studio from '~/routes/studio/studio' import ProjectLanding from './routes/projectlanding/project-landing' import AppLayout from './routes/app-layout' function RootErrorBoundary() { + const error = useRouteError() + + if (isRouteErrorResponse(error) && error.status === 404) { + return + } + return (

Oops!

@@ -49,7 +56,7 @@ export const router = createBrowserRouter([ element: , }, ], - }, + } ], }, ]) diff --git a/src/main/frontend/app/routes/notfound/not-found.test.tsx b/src/main/frontend/app/routes/notfound/not-found.test.tsx new file mode 100644 index 00000000..dc1b25f7 --- /dev/null +++ b/src/main/frontend/app/routes/notfound/not-found.test.tsx @@ -0,0 +1,65 @@ +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { navigateMock } = vi.hoisted(() => ({ navigateMock: vi.fn() })) + +vi.mock('react-router', () => ({ useNavigate: () => navigateMock })) +vi.mock('/icons/custom/ff!-icon.svg?react', () => ({ default: () => null })) + +import NotFound from './not-found' + +const STORAGE_ROOT_PATH_KEY = 'active-project-root-path' + +beforeEach(() => { + navigateMock.mockClear() + localStorage.clear() +}) + +afterEach(() => { + cleanup() +}) + +describe('NotFound', () => { + it('renders the not-found message', () => { + render() + + expect(screen.getByText('404')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Page not found' })).toBeInTheDocument() + }) + + describe('when no project has been opened', () => { + it('offers a way back to the project landing', () => { + render() + + expect(screen.getByRole('button', { name: 'Back to projects' })).toBeInTheDocument() + }) + + it('navigates to the project landing on click', () => { + render() + + fireEvent.click(screen.getByRole('button', { name: 'Back to projects' })) + + expect(navigateMock).toHaveBeenCalledWith('/') + }) + }) + + describe('when a project has been opened', () => { + beforeEach(() => { + localStorage.setItem(STORAGE_ROOT_PATH_KEY, '/some/project/path') + }) + + it('offers a way back to the configuration overview', () => { + render() + + expect(screen.getByRole('button', { name: 'Back to overview' })).toBeInTheDocument() + }) + + it('navigates to the configuration overview on click', () => { + render() + + fireEvent.click(screen.getByRole('button', { name: 'Back to overview' })) + + expect(navigateMock).toHaveBeenCalledWith('/configurations') + }) + }) +}) diff --git a/src/main/frontend/app/routes/notfound/not-found.tsx b/src/main/frontend/app/routes/notfound/not-found.tsx new file mode 100644 index 00000000..84772529 --- /dev/null +++ b/src/main/frontend/app/routes/notfound/not-found.tsx @@ -0,0 +1,28 @@ +import { useNavigate } from 'react-router' +import FfIcon from '/icons/custom/ff!-icon.svg?react' +import Button from '~/components/inputs/button' +import { getStoredProjectRootPath } from '~/stores/project-store' + +export default function NotFound() { + const navigate = useNavigate() + + const hasProject = Boolean(getStoredProjectRootPath()) + const destination = hasProject ? '/configurations' : '/' + const buttonLabel = hasProject ? 'Back to overview' : 'Back to projects' + + return ( +
+ + +
+

404

+

Page not found

+

+ The page you're looking for doesn't exist or may have been moved. +

+
+ + +
+ ) +}