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

### 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,