From 41ceb1d7db598e70d480119064f2df5e6db89e91 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bachorski <60391032+arkadiuszbachorski@users.noreply.github.com> Date: Mon, 11 Nov 2024 23:57:48 +0100 Subject: [PATCH] Improve NotFound handling (#91) # Improve NotFound handling ## :recycle: Current situation & Problem 404 errors uses basic tanstack router not found. ## :gear: Release Notes * Improve NotFound handling ![image](https://github.com/user-attachments/assets/6f5c7cbd-1895-47ae-83b0-408e3c2be3d7) ### Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md). --- components/NotFound/NotFound.tsx | 34 ++++++ components/NotFound/index.tsx | 9 ++ main.tsx | 14 ++- modules/user/queries.tsx | 19 ++-- package-lock.json | 113 +++---------------- package.json | 2 +- routes/~_dashboard/~patients/utils.ts | 4 +- routes/~_dashboard/~patients/~$id/~index.tsx | 12 +- routes/~_dashboard/~users/~$id.tsx | 17 ++- 9 files changed, 109 insertions(+), 115 deletions(-) create mode 100644 components/NotFound/NotFound.tsx create mode 100644 components/NotFound/index.tsx diff --git a/components/NotFound/NotFound.tsx b/components/NotFound/NotFound.tsx new file mode 100644 index 00000000..9e10455f --- /dev/null +++ b/components/NotFound/NotFound.tsx @@ -0,0 +1,34 @@ +// +// This source file is part of the Stanford Biodesign Digital Health ENGAGE-HF open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import { PageTitle } from '@stanfordspezi/spezi-web-design-system/molecules/DashboardLayout' +import { + NotFound as NotFoundBase, + type NotFoundProps, +} from '@stanfordspezi/spezi-web-design-system/molecules/NotFound' +import { useQuery } from '@tanstack/react-query' +import { RouteOff } from 'lucide-react' +import { currentUserQueryOptions } from '@/modules/firebase/UserProvider' +import { DashboardLayout } from '@/routes/~_dashboard/DashboardLayout' + +/** + * NotFound component wrapped with DashboardLayout if user is signed in + * */ +export const NotFound = (props: NotFoundProps) => { + const userQuery = useQuery(currentUserQueryOptions()) + + const notFound = + + return userQuery.data ? + } />} + > + {notFound} + + :
{notFound}
+} diff --git a/components/NotFound/index.tsx b/components/NotFound/index.tsx new file mode 100644 index 00000000..370a3011 --- /dev/null +++ b/components/NotFound/index.tsx @@ -0,0 +1,9 @@ +// +// This source file is part of the Stanford Biodesign Digital Health ENGAGE-HF open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +export * from './NotFound' diff --git a/main.tsx b/main.tsx index 4e53fc93..593dc4f4 100644 --- a/main.tsx +++ b/main.tsx @@ -6,12 +6,22 @@ // SPDX-License-Identifier: MIT // -import { RouterProvider, createRouter } from '@tanstack/react-router' +import { createRouter, RouterProvider } from '@tanstack/react-router' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { NotFound } from '@/components/NotFound/NotFound' +import { routes } from '@/modules/routes' import { routeTree } from './routeTree.gen' -const router = createRouter({ routeTree }) +const router = createRouter({ + routeTree, + defaultNotFoundComponent: () => ( + + ), +}) declare module '@tanstack/react-router' { interface Register { diff --git a/modules/user/queries.tsx b/modules/user/queries.tsx index b4c139dc..40c360b0 100644 --- a/modules/user/queries.tsx +++ b/modules/user/queries.tsx @@ -8,7 +8,6 @@ import { UserType } from '@stanfordbdhg/engagehf-models' import { queryOptions } from '@tanstack/react-query' -import { notFound } from '@tanstack/react-router' import { query, where } from 'firebase/firestore' import { docRefs, getCurrentUser, refs } from '@/modules/firebase/app' import { type Invitation, type Organization } from '@/modules/firebase/models' @@ -88,13 +87,13 @@ const getUserAuthData = async (userId: string) => { displayName: data.auth.displayName, })) const authUser = allAuthData.at(0) - if (!authUser || !user) throw notFound() + if (!authUser || !user) return null return { user, authUser, resourceType: 'user' as const } } const getUserInvitationData = async (userId: string) => { const invitation = await getDocData(docRefs.invitation(userId)) - if (!invitation) throw notFound() + if (!invitation) return null if (!invitation.auth) throw new Error('Incomplete data') return { user: { @@ -129,7 +128,13 @@ export const parseUserId = (userId: string) => export const getUserData = async ( userId: string, resourceType: ResourceType, -) => - resourceType === 'invitation' ? - getUserInvitationData(userId) - : getUserAuthData(userId) + validUserTypes: UserType[], +) => { + const data = + resourceType === 'invitation' ? + await getUserInvitationData(userId) + : await getUserAuthData(userId) + return data && validUserTypes.includes(data.user.type) ? data : null +} + +export type UserData = Exclude>, null> diff --git a/package-lock.json b/package-lock.json index ea4dda0b..52b0f249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@stanfordbdhg/engagehf-models": "^0.4.0", - "@stanfordspezi/spezi-web-design-system": "^0.1.0", + "@stanfordspezi/spezi-web-design-system": "^0.2.1", "@t3-oss/env-core": "^0.11.1", "@tanstack/react-query": "^5.59.19", "@tanstack/react-router": "^1.78.3", @@ -73,91 +73,6 @@ "node": "22" } }, - "../speziwebdesignsystem": { - "name": "@stanfordspezi/spezi-web-design-system", - "version": "0.1.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@hookform/resolvers": "^3.9.0", - "@nextui-org/use-pagination": "^2.0.10", - "@radix-ui/react-checkbox": "^1.1.2", - "@radix-ui/react-dialog": "^1.1.2", - "@radix-ui/react-dropdown-menu": "^2.1.2", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-popover": "^1.1.2", - "@radix-ui/react-radio-group": "^1.2.1", - "@radix-ui/react-select": "^2.1.2", - "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-switch": "^1.1.1", - "@radix-ui/react-tabs": "^1.1.1", - "@radix-ui/react-tooltip": "^1.1.3", - "@tanstack/match-sorter-utils": "^8.19.4", - "@tanstack/react-router": "^1.77.5", - "@tanstack/react-table": "^8.20.5", - "class-variance-authority": "^0.7.0", - "date-fns": "^3.6.0", - "es-toolkit": "^1.26.1", - "firebase": "^10.14.1", - "lucide-react": "^0.453.0", - "react-day-picker": "^8", - "react-hook-form": "^7.53.1", - "sonner": "^1.5.0", - "use-debounce": "^10.0.4", - "zod": "^3.23.8" - }, - "devDependencies": { - "@storybook/addon-essentials": "^8.3.5", - "@storybook/addon-interactions": "^8.3.5", - "@storybook/addon-links": "^8.3.5", - "@storybook/addon-onboarding": "^8.3.5", - "@storybook/blocks": "^8.3.5", - "@storybook/react": "^8.3.5", - "@storybook/react-vite": "^8.3.5", - "@storybook/test": "^8.3.5", - "@testing-library/jest-dom": "^6", - "@testing-library/react": "^16", - "@total-typescript/ts-reset": "^0.6.1", - "@types/node": "^22", - "@types/react": "^18", - "@types/react-dom": "^18", - "@types/react-helmet": "^6.1.11", - "@typescript-eslint/eslint-plugin": "^8", - "@typescript-eslint/parser": "^8", - "@vitejs/plugin-react": "^4.3.3", - "@vitest/coverage-v8": "^2.1.3", - "autoprefixer": "^10", - "esbuild": "^0.24.0", - "eslint": "^8", - "eslint-config-next": "^14", - "eslint-config-prettier": "^9", - "eslint-import-resolver-typescript": "^3.6.3", - "eslint-plugin-import": "^2", - "eslint-plugin-prettier": "^5", - "jsdom": "^25.0.1", - "postcss": "^8", - "postcss-import": "^16.1.0", - "prettier": "^3", - "prettier-plugin-tailwindcss": "^0.6.8", - "storybook": "^8.3.5", - "tailwindcss": "^3", - "tailwindcss-animate": "^1.0.7", - "typedoc": "^0.26", - "typescript": "^5", - "vite": "^5.4.10", - "vite-plugin-dts": "^4.3.0", - "vitest": "^2.1.3" - }, - "engines": { - "node": "22" - }, - "peerDependencies": { - "next-intl": "^3", - "react": "^18", - "react-dom": "^18" - } - }, "node_modules/@adobe/css-tools": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", @@ -3446,9 +3361,9 @@ } }, "node_modules/@stanfordspezi/spezi-web-design-system": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@stanfordspezi/spezi-web-design-system/-/spezi-web-design-system-0.1.0.tgz", - "integrity": "sha512-Au4OjHFcyRcqTeedjbCiAkhoGYzYaTOZmurddETPHnXQ+ZdiIPjO2cbAv1L2tAyGC3TpcChkE1uvIJqr6IFQ+w==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@stanfordspezi/spezi-web-design-system/-/spezi-web-design-system-0.2.1.tgz", + "integrity": "sha512-OYVCBYJplYH+euZfB0xnE+lUcBbw8tG9aGJfaOF00CQGGTq2NdGydr1dF7r/V8XzF6u9KotqNBX22zTvGQ6WBg==", "dependencies": { "@hookform/resolvers": "^3.9.0", "@nextui-org/use-pagination": "^2.0.10", @@ -4533,11 +4448,11 @@ "peer": true }, "node_modules/@swc/helpers": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", - "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@t3-oss/env-core": { @@ -11643,9 +11558,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.53.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz", - "integrity": "sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==", + "version": "7.53.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.2.tgz", + "integrity": "sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==", "engines": { "node": ">=18.0.0" }, @@ -13187,9 +13102,9 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tsx": { "version": "4.19.2", diff --git a/package.json b/package.json index 6c89ccc0..870e2718 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@stanfordbdhg/engagehf-models": "^0.4.0", - "@stanfordspezi/spezi-web-design-system": "^0.1.0", + "@stanfordspezi/spezi-web-design-system": "^0.2.1", "@t3-oss/env-core": "^0.11.1", "@tanstack/react-query": "^5.59.19", "@tanstack/react-router": "^1.78.3", diff --git a/routes/~_dashboard/~patients/utils.ts b/routes/~_dashboard/~patients/utils.ts index dd69fd7a..1db83402 100644 --- a/routes/~_dashboard/~patients/utils.ts +++ b/routes/~_dashboard/~patients/utils.ts @@ -24,7 +24,7 @@ import { mapAuthData } from '@/modules/firebase/user' import { getDocsData, type ResourceType } from '@/modules/firebase/utils' import { queryClient } from '@/modules/query/queryClient' import { - type getUserData, + type UserData, userOrganizationQueryOptions, } from '@/modules/user/queries' import { labsObservationCollections } from '@/routes/~_dashboard/~patients/clientUtils' @@ -221,7 +221,7 @@ export const getUserActivity = async ({ user, resourceType, authUser, -}: Awaited>) => { +}: UserData) => { const latestQuestionnaires = await getDocsData( query( refs.questionnaireResponses({ resourceType, userId: authUser.uid }), diff --git a/routes/~_dashboard/~patients/~$id/~index.tsx b/routes/~_dashboard/~patients/~$id/~index.tsx index 20db3953..29e20d05 100644 --- a/routes/~_dashboard/~patients/~$id/~index.tsx +++ b/routes/~_dashboard/~patients/~$id/~index.tsx @@ -20,6 +20,7 @@ import { createFileRoute, notFound, useRouter } from '@tanstack/react-router' import { Contact } from 'lucide-react' import { Helmet } from 'react-helmet' import { z } from 'zod' +import { NotFound } from '@/components/NotFound' import { callables, db, docRefs, refs } from '@/modules/firebase/app' import { getMedicationRequestData, @@ -30,6 +31,7 @@ import { getDocsData, type ResourceType, } from '@/modules/firebase/utils' +import { routes } from '@/modules/routes' import { getUserData, parseUserId } from '@/modules/user/queries' import { Medications, @@ -245,11 +247,17 @@ export const Route = createFileRoute('/_dashboard/patients/$id/')({ validateSearch: z.object({ tab: z.nativeEnum(PatientPageTab).optional().catch(undefined), }), + notFoundComponent: () => ( + + ), loader: async ({ params }) => { const { userId, resourceType } = parseUserId(params.id) - const userData = await getUserData(userId, resourceType) + const userData = await getUserData(userId, resourceType, [UserType.patient]) + if (!userData) throw notFound() const { user, authUser } = userData - if (user.type !== UserType.patient) throw notFound() return { user, diff --git a/routes/~_dashboard/~users/~$id.tsx b/routes/~_dashboard/~users/~$id.tsx index 922f01e1..ebe39e21 100644 --- a/routes/~_dashboard/~users/~$id.tsx +++ b/routes/~_dashboard/~users/~$id.tsx @@ -13,9 +13,11 @@ import { PageTitle } from '@stanfordspezi/spezi-web-design-system/molecules/Dash import { createFileRoute, notFound, useRouter } from '@tanstack/react-router' import { Users } from 'lucide-react' import { Helmet } from 'react-helmet' +import { NotFound } from '@/components/NotFound' import { callables, docRefs, ensureType } from '@/modules/firebase/app' import { getDocDataOrThrow } from '@/modules/firebase/utils' import { queryClient } from '@/modules/query/queryClient' +import { routes } from '@/modules/routes' import { getUserData, parseUserId, @@ -89,10 +91,21 @@ const UserPage = () => { export const Route = createFileRoute('/_dashboard/users/$id')({ component: UserPage, beforeLoad: () => ensureType([UserType.admin, UserType.owner]), + notFoundComponent: () => ( + + ), loader: async ({ params }) => { const { resourceType, userId } = parseUserId(params.id) - const { user, authUser } = await getUserData(userId, resourceType) - if (user.type === UserType.patient) throw notFound() + const userData = await getUserData(userId, resourceType, [ + UserType.clinician, + UserType.admin, + UserType.owner, + ]) + if (!userData) throw notFound() + const { user, authUser } = userData return { user,