From 72c0db5de4e6ad18579655b841c1fbe96902b4d2 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Mon, 30 Oct 2023 13:37:19 -0700 Subject: [PATCH] wrap all data mutations in error handling --- .../_components/ActiveProtocolSwitch.tsx | 6 +- .../AnonymousRecruitmentSwitch.tsx | 3 + .../InterviewsTable/InterviewsTable.tsx | 6 +- .../_components/ParticipantsTable/Loader.ts | 16 --- .../ParticipantsTable/ParticipantsTable.tsx | 2 - .../_components/ProtocolUploader.tsx | 10 +- .../ProtocolsTable/ProtocolsTable.tsx | 3 +- .../dashboard/_components/ResetButton.tsx | 6 +- .../_components/DeleteInterviewsDialog.tsx | 6 +- .../DeleteAllParticipantsButton.tsx | 3 + .../_components/DeleteParticipantsDialog.tsx | 6 +- .../_components/ImportCSVModal.tsx | 6 +- .../_components/DeleteProtocolsDialog.tsx | 6 +- app/(dashboard)/dashboard/protocols/page.tsx | 26 ++-- app/(onboard)/_components/OnboardWizard.tsx | 14 +- app/(onboard)/expired/page.tsx | 3 + app/api/error.tsx | 32 +++++ app/error.tsx | 32 +++++ app/layout.tsx | 37 ++--- app/not-found.tsx | 5 +- providers/InterviewProvider.tsx | 6 +- providers/SessionProvider.tsx | 8 +- server/routers/appSettings.ts | 70 ++++++---- server/routers/interview.ts | 40 +++--- server/routers/participant.ts | 30 ++-- server/routers/protocol.ts | 132 +++++++++++------- 26 files changed, 335 insertions(+), 179 deletions(-) delete mode 100644 app/(dashboard)/dashboard/_components/ParticipantsTable/Loader.ts create mode 100644 app/api/error.tsx create mode 100644 app/error.tsx diff --git a/app/(dashboard)/dashboard/_components/ActiveProtocolSwitch.tsx b/app/(dashboard)/dashboard/_components/ActiveProtocolSwitch.tsx index f770f2ff..c8c71c9d 100644 --- a/app/(dashboard)/dashboard/_components/ActiveProtocolSwitch.tsx +++ b/app/(dashboard)/dashboard/_components/ActiveProtocolSwitch.tsx @@ -16,6 +16,9 @@ const ActiveProtocolSwitch = ({ const { data: isActive } = api.protocol.getActive.useQuery(hash, { initialData, + onError: (err) => { + throw new Error(err.message); + }, }); const { mutateAsync: setActive } = api.protocol.setActive.useMutation({ @@ -33,8 +36,7 @@ const ActiveProtocolSwitch = ({ }, onError: (err, _newState, previousState) => { utils.protocol.getActive.setData(hash, previousState); - // eslint-disable-next-line no-console - console.error(err); + throw new Error(err.message); }, onSuccess: () => { router.refresh(); diff --git a/app/(dashboard)/dashboard/_components/AnonymousRecruitmentSwitch.tsx b/app/(dashboard)/dashboard/_components/AnonymousRecruitmentSwitch.tsx index 47961397..8cc7c1f2 100644 --- a/app/(dashboard)/dashboard/_components/AnonymousRecruitmentSwitch.tsx +++ b/app/(dashboard)/dashboard/_components/AnonymousRecruitmentSwitch.tsx @@ -13,6 +13,9 @@ const AnonymousRecruitmentSwitch = ({ const { data: allowAnonymousRecruitment } = api.appSettings.get.allowAnonymousRecruitment.useQuery(undefined, { initialData, + onError(error) { + throw new Error(error.message); + }, }); const { mutateAsync: updateAnonymousRecruitment } = diff --git a/app/(dashboard)/dashboard/_components/InterviewsTable/InterviewsTable.tsx b/app/(dashboard)/dashboard/_components/InterviewsTable/InterviewsTable.tsx index 7458c2ed..87d3a749 100644 --- a/app/(dashboard)/dashboard/_components/InterviewsTable/InterviewsTable.tsx +++ b/app/(dashboard)/dashboard/_components/InterviewsTable/InterviewsTable.tsx @@ -9,7 +9,11 @@ import { useState } from 'react'; import { DeleteInterviewsDialog } from '../../interviews/_components/DeleteInterviewsDialog'; export const InterviewsTable = () => { - const interviews = api.interview.get.all.useQuery(); + const interviews = api.interview.get.all.useQuery(undefined, { + onError(error) { + throw new Error(error.message); + }, + }); const [interviewsToDelete, setInterviewsToDelete] = useState(); const [showDeleteModal, setShowDeleteModal] = useState(false); diff --git a/app/(dashboard)/dashboard/_components/ParticipantsTable/Loader.ts b/app/(dashboard)/dashboard/_components/ParticipantsTable/Loader.ts deleted file mode 100644 index f3a97c06..00000000 --- a/app/(dashboard)/dashboard/_components/ParticipantsTable/Loader.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from 'zod'; -import { api } from '~/trpc/server'; -import { safeLoader } from '~/utils/safeLoader'; - -const ParticipantValidation = z.array( - z.object({ - id: z.string(), - identifier: z.string(), - }), -); - -export const safeLoadParticipants = () => - safeLoader({ - outputValidation: ParticipantValidation, - loader: () => api.participant.get.all.query(), - }); diff --git a/app/(dashboard)/dashboard/_components/ParticipantsTable/ParticipantsTable.tsx b/app/(dashboard)/dashboard/_components/ParticipantsTable/ParticipantsTable.tsx index f1a0a013..5d3a8257 100644 --- a/app/(dashboard)/dashboard/_components/ParticipantsTable/ParticipantsTable.tsx +++ b/app/(dashboard)/dashboard/_components/ParticipantsTable/ParticipantsTable.tsx @@ -22,8 +22,6 @@ export const ParticipantsTable = ({ initialData, refetchOnMount: false, onError(error) { - // eslint-disable-next-line no-console - console.error(error); throw new Error(error.message); }, }, diff --git a/app/(dashboard)/dashboard/_components/ProtocolUploader.tsx b/app/(dashboard)/dashboard/_components/ProtocolUploader.tsx index 602fe1d9..9c51d867 100644 --- a/app/(dashboard)/dashboard/_components/ProtocolUploader.tsx +++ b/app/(dashboard)/dashboard/_components/ProtocolUploader.tsx @@ -132,8 +132,14 @@ export default function ProtocolUploader({ setOpen(false); } - const { data: lastUploadedProtocol } = - api.protocol.get.lastUploaded.useQuery(); + const { data: lastUploadedProtocol } = api.protocol.get.lastUploaded.useQuery( + undefined, + { + onError(error) { + throw new Error(error.message); + }, + }, + ); return ( <> diff --git a/app/(dashboard)/dashboard/_components/ProtocolsTable/ProtocolsTable.tsx b/app/(dashboard)/dashboard/_components/ProtocolsTable/ProtocolsTable.tsx index 1eb8a7f9..6c1e83b4 100644 --- a/app/(dashboard)/dashboard/_components/ProtocolsTable/ProtocolsTable.tsx +++ b/app/(dashboard)/dashboard/_components/ProtocolsTable/ProtocolsTable.tsx @@ -20,8 +20,7 @@ export const ProtocolsTable = ({ initialData, refetchOnMount: false, onError(error) { - // eslint-disable-next-line no-console - console.error(error); + throw new Error(error.message); }, }, ); diff --git a/app/(dashboard)/dashboard/_components/ResetButton.tsx b/app/(dashboard)/dashboard/_components/ResetButton.tsx index d15b2f77..1862261f 100644 --- a/app/(dashboard)/dashboard/_components/ResetButton.tsx +++ b/app/(dashboard)/dashboard/_components/ResetButton.tsx @@ -9,7 +9,11 @@ import { Button } from '~/components/ui/Button'; const ResetButton = () => { const [loading, setLoading] = useState(false); const router = useRouter(); - const { mutateAsync: resetConfigured } = api.appSettings.reset.useMutation(); + const { mutateAsync: resetConfigured } = api.appSettings.reset.useMutation({ + onError(error) { + throw new Error(error.message); + }, + }); const reset = async () => { setLoading(true); diff --git a/app/(dashboard)/dashboard/interviews/_components/DeleteInterviewsDialog.tsx b/app/(dashboard)/dashboard/interviews/_components/DeleteInterviewsDialog.tsx index c6038499..adba413a 100644 --- a/app/(dashboard)/dashboard/interviews/_components/DeleteInterviewsDialog.tsx +++ b/app/(dashboard)/dashboard/interviews/_components/DeleteInterviewsDialog.tsx @@ -27,7 +27,11 @@ export const DeleteInterviewsDialog = ({ }: DeleteInterviewsDialog) => { const [hasUnexported, setHasUnexported] = useState(false); const { mutateAsync: deleteInterviews, isLoading: isDeleting } = - api.interview.delete.useMutation(); + api.interview.delete.useMutation({ + onError(error) { + throw new Error(error.message); + }, + }); const utils = api.useUtils(); useEffect(() => { diff --git a/app/(dashboard)/dashboard/participants/_components/DeleteAllParticipantsButton.tsx b/app/(dashboard)/dashboard/participants/_components/DeleteAllParticipantsButton.tsx index aa81c17e..a269c51e 100644 --- a/app/(dashboard)/dashboard/participants/_components/DeleteAllParticipantsButton.tsx +++ b/app/(dashboard)/dashboard/participants/_components/DeleteAllParticipantsButton.tsx @@ -23,6 +23,9 @@ export const DeleteAllParticipantsButton = () => { await utils.participant.get.all.refetch(); setShowAlertDialog(false); }, + onError(error) { + throw new Error(error.message); + }, }); return ( <> diff --git a/app/(dashboard)/dashboard/participants/_components/DeleteParticipantsDialog.tsx b/app/(dashboard)/dashboard/participants/_components/DeleteParticipantsDialog.tsx index c2853feb..1eb1a302 100644 --- a/app/(dashboard)/dashboard/participants/_components/DeleteParticipantsDialog.tsx +++ b/app/(dashboard)/dashboard/participants/_components/DeleteParticipantsDialog.tsx @@ -44,7 +44,11 @@ export const DeleteParticipantsDialog = ({ }); }, [participantsToDelete]); const { mutateAsync: deleteParticipants, isLoading: isDeleting } = - api.participant.delete.byId.useMutation(); + api.participant.delete.byId.useMutation({ + onError(error) { + throw new Error(error.message); + }, + }); const utils = api.useUtils(); const handleConfirm = async () => { diff --git a/app/(dashboard)/dashboard/participants/_components/ImportCSVModal.tsx b/app/(dashboard)/dashboard/participants/_components/ImportCSVModal.tsx index 36e8cf11..219f1b3b 100644 --- a/app/(dashboard)/dashboard/participants/_components/ImportCSVModal.tsx +++ b/app/(dashboard)/dashboard/participants/_components/ImportCSVModal.tsx @@ -34,7 +34,11 @@ const ImportCSVModal = ({ const methods = useZodForm({ schema: formSchema, shouldUnregister: true }); const utils = api.useUtils(); const { mutateAsync: importParticipants } = - api.participant.create.useMutation(); + api.participant.create.useMutation({ + onError(error) { + throw new Error(error.message); + }, + }); const isSubmitting = methods.formState.isSubmitting; const [showImportDialog, setShowImportDialog] = useState(false); const selectedCSV = methods.watch('csvFile'); diff --git a/app/(dashboard)/dashboard/protocols/_components/DeleteProtocolsDialog.tsx b/app/(dashboard)/dashboard/protocols/_components/DeleteProtocolsDialog.tsx index 72c26d8b..56c59ee8 100644 --- a/app/(dashboard)/dashboard/protocols/_components/DeleteProtocolsDialog.tsx +++ b/app/(dashboard)/dashboard/protocols/_components/DeleteProtocolsDialog.tsx @@ -44,7 +44,11 @@ export const DeleteProtocolsDialog = ({ }); }, [protocolsToDelete]); const { mutateAsync: deleteProtocols, isLoading: isDeleting } = - api.protocol.delete.byHash.useMutation(); + api.protocol.delete.byHash.useMutation({ + onError(error) { + throw new Error(error.message); + }, + }); const utils = api.useUtils(); const handleConfirm = async () => { diff --git a/app/(dashboard)/dashboard/protocols/page.tsx b/app/(dashboard)/dashboard/protocols/page.tsx index b0f84f4d..dbafd54f 100644 --- a/app/(dashboard)/dashboard/protocols/page.tsx +++ b/app/(dashboard)/dashboard/protocols/page.tsx @@ -2,17 +2,21 @@ import { ProtocolsTable } from '~/app/(dashboard)/dashboard/_components/Protocol import { api } from '~/trpc/server'; const ProtocolsPage = async () => { - const protocols = await api.protocol.get.all.query(undefined, { - context: { - revalidate: 0, - }, - }); - return ( -
-

Protocols management view

- -
- ); + try { + const protocols = await api.protocol.get.all.query(undefined, { + context: { + revalidate: 0, + }, + }); + return ( +
+

Protocols management view

+ +
+ ); + } catch (error) { + throw new Error('An error occurred while fetching protocols'); + } }; export default ProtocolsPage; diff --git a/app/(onboard)/_components/OnboardWizard.tsx b/app/(onboard)/_components/OnboardWizard.tsx index 96b96c30..03970606 100644 --- a/app/(onboard)/_components/OnboardWizard.tsx +++ b/app/(onboard)/_components/OnboardWizard.tsx @@ -21,16 +21,12 @@ function OnboardWizard() { const step = searchParams.get('step'); const stepInt = parseInt(step ?? '1', 10); - const { data: expired, error } = api.appSettings.get.expired.useQuery( - undefined, - { - refetchInterval: 1000 * 10, + const { data: expired } = api.appSettings.get.expired.useQuery(undefined, { + refetchInterval: 1000 * 10, + onError(error) { + throw new Error(error.message); }, - ); - - if (error) { - throw new Error(error.message); - } + }); useEffect(() => { if (expired) { diff --git a/app/(onboard)/expired/page.tsx b/app/(onboard)/expired/page.tsx index 8106e261..c86df0ee 100644 --- a/app/(onboard)/expired/page.tsx +++ b/app/(onboard)/expired/page.tsx @@ -15,6 +15,9 @@ export default function Page() { window.location.replace('/setup'); }, + onError(error) { + throw new Error(error.message); + }, }, ); diff --git a/app/api/error.tsx b/app/api/error.tsx new file mode 100644 index 00000000..bb1aca64 --- /dev/null +++ b/app/api/error.tsx @@ -0,0 +1,32 @@ +'use client'; // Error components must be Client components + +import { useEffect } from 'react'; +import { Button } from '~/components/ui/Button'; +import { AlertTriangle } from 'lucide-react'; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + useEffect(() => { + // Log the error to an error reporting service + // eslint-disable-next-line no-console + console.error(error); + }, [error]); + + return ( +
+ +

+ API Error +

+

{error.message}

+
+ +
+
+ ); +} diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 00000000..c152e486 --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,32 @@ +'use client'; // Error components must be Client components + +import { useEffect } from 'react'; +import { Button } from '~/components/ui/Button'; +import { AlertTriangle } from 'lucide-react'; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + useEffect(() => { + // Log the error to an error reporting service + // eslint-disable-next-line no-console + console.error(error); + }, [error]); + + return ( +
+ +

+ Root Layout Error +

+

{error.message}

+
+ +
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index ada70959..1241e072 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -18,23 +18,26 @@ export const dynamic = 'force-dynamic'; async function RootLayout({ children }: { children: React.ReactNode }) { const session = await getServerSession(); - const { configured, expired } = - await api.appSettings.get.allappSettings.query(); - - return ( - - - - {children} - - - - - ); + try { + const { configured, expired } = + await api.appSettings.get.allappSettings.query(); + return ( + + + + {children} + + + + + ); + } catch (error) { + throw new Error('Failed to fetch app settings'); + } } export default RootLayout; diff --git a/app/not-found.tsx b/app/not-found.tsx index 6026fe08..6c12541e 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -11,12 +11,11 @@ export default function NotFound() { }; return (
- +

404

Page not found