From cc320af73dfea2e945f29099226ae6ae0e51cffb Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 11 Jul 2023 21:22:48 +0800 Subject: [PATCH] Refactor error handling for profile mutations --- .../data/profile/profile-create-mutation.ts | 16 +++- studio/data/profile/profile-query.ts | 12 +-- .../data/profile/profile-update-mutation.ts | 16 +++- studio/lib/profile.tsx | 30 +++++- studio/pages/account/me.tsx | 92 +++++++++++-------- 5 files changed, 111 insertions(+), 55 deletions(-) diff --git a/studio/data/profile/profile-create-mutation.ts b/studio/data/profile/profile-create-mutation.ts index 01f1ffff75c..a6a3f358a4c 100644 --- a/studio/data/profile/profile-create-mutation.ts +++ b/studio/data/profile/profile-create-mutation.ts @@ -1,6 +1,9 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + import { post } from 'lib/common/fetch' import { API_URL } from 'lib/constants' +import { ResponseError } from 'types' import { profileKeys } from './keys' import { Profile } from './types' @@ -19,16 +22,23 @@ type ProfileCreateData = Awaited> export const useProfileCreateMutation = ({ onSuccess, + onError, ...options -}: Omit, 'mutationFn'> = {}) => { +}: Omit, 'mutationFn'> = {}) => { const queryClient = useQueryClient() - return useMutation(() => createProfile(), { + return useMutation(() => createProfile(), { async onSuccess(data, variables, context) { await queryClient.invalidateQueries(profileKeys.profile()) - await onSuccess?.(data, variables, context) }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to create profile: ${data.message}`) + } else { + onError(data, variables, context) + } + }, ...options, }) } diff --git a/studio/data/profile/profile-query.ts b/studio/data/profile/profile-query.ts index a6e990485a4..a11e5050e16 100644 --- a/studio/data/profile/profile-query.ts +++ b/studio/data/profile/profile-query.ts @@ -4,25 +4,21 @@ import { API_URL } from 'lib/constants' import { useCallback } from 'react' import { profileKeys } from './keys' import { Profile } from './types' +import { ResponseError } from 'types' export type ProfileResponse = Profile export async function getProfile(signal?: AbortSignal) { const response = await get(`${API_URL}/profile`, { signal, - headers: { - Version: '2', - }, + headers: { Version: '2' }, }) - if (response.error) { - throw response.error - } - + if (response.error) throw response.error return response as ProfileResponse } export type ProfileData = Awaited> -export type ProfileError = unknown +export type ProfileError = ResponseError export const useProfileQuery = ({ enabled = true, diff --git a/studio/data/profile/profile-update-mutation.ts b/studio/data/profile/profile-update-mutation.ts index d4633f184bb..39cc5bd1c6c 100644 --- a/studio/data/profile/profile-update-mutation.ts +++ b/studio/data/profile/profile-update-mutation.ts @@ -1,6 +1,9 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'react-hot-toast' + import { patch } from 'lib/common/fetch' import { API_URL } from 'lib/constants' +import { ResponseError } from 'types' import { profileKeys } from './keys' export type ProfileUpdateVariables = { @@ -24,21 +27,28 @@ type ProfileUpdateData = Awaited> export const useProfileUpdateMutation = ({ onSuccess, + onError, ...options }: Omit< - UseMutationOptions, + UseMutationOptions, 'mutationFn' > = {}) => { const queryClient = useQueryClient() - return useMutation( + return useMutation( (vars) => updateProfile(vars), { async onSuccess(data, variables, context) { await queryClient.invalidateQueries(profileKeys.profile()) - await onSuccess?.(data, variables, context) }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to create profile: ${data.message}`) + } else { + onError(data, variables, context) + } + }, ...options, } ) diff --git a/studio/lib/profile.tsx b/studio/lib/profile.tsx index 18f91d45900..d8feec8dcbc 100644 --- a/studio/lib/profile.tsx +++ b/studio/lib/profile.tsx @@ -10,15 +10,22 @@ import { useProfileQuery } from 'data/profile/profile-query' import { Profile } from 'data/profile/types' import { useStore } from 'hooks' import Telemetry from 'lib/telemetry' +import { ResponseError } from 'types' export type ProfileContextType = { profile: Profile | undefined + error: ResponseError | null isLoading: boolean + isError: boolean + isSuccess: boolean } export const ProfileContext = createContext({ profile: undefined, + error: null, isLoading: true, + isError: false, + isSuccess: false, }) export const ProfileProvider = ({ children }: PropsWithChildren<{}>) => { @@ -37,7 +44,7 @@ export const ProfileProvider = ({ children }: PropsWithChildren<{}>) => { await invalidateOrganizationsQuery(queryClient) }, - onError(err) { + onError() { ui.setNotification({ category: 'error', message: 'Failed to create your profile. Please refresh to try again.', @@ -46,7 +53,13 @@ export const ProfileProvider = ({ children }: PropsWithChildren<{}>) => { }) // Track telemetry for the current user - const { data: profile, isLoading: isLoadingProfile } = useProfileQuery({ + const { + error, + data: profile, + isLoading: isLoadingProfile, + isError, + isSuccess, + } = useProfileQuery({ onSuccess(profile) { Telemetry.sendIdentify(profile, telemetryProps) }, @@ -64,10 +77,21 @@ export const ProfileProvider = ({ children }: PropsWithChildren<{}>) => { const isLoading = isLoadingProfile || isCreatingProfile || isLoadingPermissions return { + error, profile, isLoading, + isError, + isSuccess, } - }, [isLoadingProfile, isCreatingProfile, isLoadingPermissions, profile]) + }, [ + isLoadingProfile, + isCreatingProfile, + isLoadingPermissions, + profile, + error, + isError, + isSuccess, + ]) return {children} } diff --git a/studio/pages/account/me.tsx b/studio/pages/account/me.tsx index 0484fd25730..e6f0bc5d9cd 100644 --- a/studio/pages/account/me.tsx +++ b/studio/pages/account/me.tsx @@ -12,6 +12,8 @@ import { useSession } from 'lib/auth' import { useProfile } from 'lib/profile' import { NextPageWithLayout } from 'types' import { Button, IconMoon, IconSun, Input, Listbox } from 'ui' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import AlertError from 'components/ui/AlertError' const User: NextPageWithLayout = () => { return ( @@ -39,53 +41,67 @@ export default User const ProfileCard = observer(() => { const { ui } = useStore() - const { mutateAsync } = useProfileUpdateMutation() - - const { profile } = useProfile() - // TODO: ^ handle loading state - - const updateUser = async (model: any) => { - try { - await mutateAsync({ - firstName: model.first_name, - lastName: model.last_name, - }) - - ui.setNotification({ category: 'success', message: 'Successfully saved profile' }) - } catch (error) { + const { profile, error, isLoading, isError, isSuccess } = useProfile() + const { mutateAsync: updateProfile } = useProfileUpdateMutation({ + onError: (error) => { ui.setNotification({ error, category: 'error', message: "Couldn't update profile. Please try again later.", }) - } + }, + }) + + const updateUser = async (model: any) => { + await updateProfile({ + firstName: model.first_name, + lastName: model.last_name, + }) + ui.setNotification({ category: 'success', message: 'Successfully saved profile' }) } return (
-
- -
- -
- {/* @ts-ignore */} - -
+ {isLoading && ( + +
+ +
+
+ )} + {isError && ( + +
+ +
+
+ )} + {isSuccess && ( + <> +
+ +
+
+ {/* @ts-ignore */} + +
+ + )}