diff --git a/package.json b/package.json index 75a34e2..6121f04 100755 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "immutability-helper": "^3.1.1", "in-milliseconds": "^1.2.0", "in-seconds": "^1.2.0", + "notistack": "^2.0.8", "openpgp": "^5.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/App.tsx b/src/App.tsx index 9536418..64853e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,13 @@ import {RouterProvider, createBrowserRouter} from "react-router-dom" +import {SnackbarProvider} from "notistack" import React, {ReactElement} from "react" import {QueryClientProvider} from "@tanstack/react-query" import {CssBaseline, ThemeProvider} from "@mui/material" import {queryClient} from "~/constants/react-query" -import {lightTheme} from "~/constants/themes" import {getServerSettings} from "~/apis" +import {lightTheme} from "~/constants/themes" import AliasDetailRoute from "~/routes/AliasDetailRoute" import AliasesRoute from "~/routes/AliasesRoute" import AuthContextProvider from "~/AuthContext/AuthContextProvider" @@ -96,10 +97,12 @@ export default function App(): ReactElement { - - - - + + + + + + diff --git a/src/LockNavigationContext/FormikAutoLockNavigation.tsx b/src/LockNavigationContext/FormikAutoLockNavigation.tsx index cb26872..01d35cc 100644 --- a/src/LockNavigationContext/FormikAutoLockNavigation.tsx +++ b/src/LockNavigationContext/FormikAutoLockNavigation.tsx @@ -1,5 +1,7 @@ import {FormikContextType} from "formik" -import {useContext, useEffect} from "react" +import {useContext} from "react" +import {useDeepCompareEffect} from "react-use" +import deepEqual from "deep-equal" import LockNavigationContext from "./LockNavigationContext" @@ -12,16 +14,14 @@ export default function FormikAutoLockNavigation({ }: LockNavigationContextProviderProps): null { const {lock, release} = useContext(LockNavigationContext) - const valuesStringified = JSON.stringify(formik.values) - const initialValuesStringified = JSON.stringify(formik.initialValues) - - useEffect(() => { - if (valuesStringified !== initialValuesStringified) { + // TODO: Not working yet + useDeepCompareEffect(() => { + if (!deepEqual(formik.values, formik.initialValues)) { lock() } else { release() } - }, [lock, release, valuesStringified, initialValuesStringified]) + }, [lock, release, formik.values, formik.initialValues]) return null } diff --git a/src/constants/values.ts b/src/constants/values.ts index 93d31f2..594fe50 100644 --- a/src/constants/values.ts +++ b/src/constants/values.ts @@ -12,3 +12,5 @@ export const DEFAULT_ALIAS_NOTE: AliasNote = { websites: [], }, } +export const ERROR_SNACKBAR_SHOW_DURATION = 5000 +export const SUCCESS_SNACKBAR_SHOW_DURATION = 2000 diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 121e998..02edbd6 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -10,3 +10,5 @@ export * from "./use-ui-state" export {default as useUIState} from "./use-ui-state" export * from "./use-navigate-to-next" export {default as useNavigateToNext} from "./use-navigate-to-next" +export * from "./use-error-success-snacks" +export {default as useErrorSuccessSnacks} from "./use-error-success-snacks" diff --git a/src/hooks/use-error-success-snacks.ts b/src/hooks/use-error-success-snacks.ts new file mode 100644 index 0000000..9640ed4 --- /dev/null +++ b/src/hooks/use-error-success-snacks.ts @@ -0,0 +1,48 @@ +import {AxiosError} from "axios" +import {useRef} from "react" +import {SnackbarKey, useSnackbar} from "notistack" +import {useTranslation} from "react-i18next" + +import {ERROR_SNACKBAR_SHOW_DURATION, SUCCESS_SNACKBAR_SHOW_DURATION} from "~/constants/values" +import {parseFastAPIError} from "~/utils" + +export interface UseErrorSuccessSnacksResult { + showSuccess: (message: string) => void + showError: (error: Error) => void +} + +export default function useErrorSuccessSnacks(): UseErrorSuccessSnacksResult { + const {t} = useTranslation() + const {enqueueSnackbar, closeSnackbar} = useSnackbar() + const $errorSnackbarKey = useRef(null) + + const showSuccess = (message: string) => { + if ($errorSnackbarKey.current) { + closeSnackbar($errorSnackbarKey.current) + $errorSnackbarKey.current = null + } + + enqueueSnackbar(message, { + variant: "success", + autoHideDuration: SUCCESS_SNACKBAR_SHOW_DURATION, + }) + } + const showError = (error: Error) => { + const parsedError = parseFastAPIError(error as AxiosError) + + if ("detail" in parsedError) { + $errorSnackbarKey.current = enqueueSnackbar( + parsedError.detail || t("general.defaultError"), + { + variant: "error", + autoHideDuration: ERROR_SNACKBAR_SHOW_DURATION, + }, + ) + } + } + + return { + showSuccess, + showError, + } +} diff --git a/src/route-widgets/AliasDetailRoute/AliasDetails.tsx b/src/route-widgets/AliasDetailRoute/AliasDetails.tsx index b2cbd78..8b0a41f 100644 --- a/src/route-widgets/AliasDetailRoute/AliasDetails.tsx +++ b/src/route-widgets/AliasDetailRoute/AliasDetails.tsx @@ -1,19 +1,16 @@ import {useParams} from "react-router" -import {ReactElement, useContext, useState} from "react" +import {ReactElement, useContext} from "react" import {useTranslation} from "react-i18next" import {MdContentCopy} from "react-icons/md" +import {useSnackbar} from "notistack" import copy from "copy-to-clipboard" import {Button, Grid} from "@mui/material" -import { - AliasTypeIndicator, - DecryptionPasswordMissingAlert, - SimplePageBuilder, - SuccessSnack, -} from "~/components" +import {AliasTypeIndicator, DecryptionPasswordMissingAlert, SimplePageBuilder} from "~/components" import {Alias, DecryptedAlias} from "~/server-types" import {useUIState} from "~/hooks" +import {SUCCESS_SNACKBAR_SHOW_DURATION} from "~/constants/values" import AliasNotesForm from "~/route-widgets/AliasDetailRoute/AliasNotesForm" import AliasPreferencesForm from "~/route-widgets/AliasDetailRoute/AliasPreferencesForm" import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext" @@ -25,69 +22,67 @@ export interface AliasDetailsProps { export default function AliasDetails({alias: aliasValue}: AliasDetailsProps): ReactElement { const {t} = useTranslation() + const {enqueueSnackbar} = useSnackbar() const params = useParams() const {encryptionStatus} = useContext(AuthContext) const address = atob(params.addressInBase64 as string) const [aliasUIState, setAliasUIState] = useUIState(aliasValue) - const [hasCopiedToClipboard, setHasCopiedToClipboard] = useState(false) return ( - <> - - {[ - - - - - - } - variant="text" - color="inherit" - onClick={() => { - copy(address) - setHasCopiedToClipboard(true) - }} - sx={{textTransform: "none", fontWeight: "normal"}} - > - {address} - - - - - - , - - {encryptionStatus === EncryptionStatus.Available ? ( - - ) : ( - - )} - , - - - , - ]} - - setHasCopiedToClipboard(false)} - message={ - hasCopiedToClipboard && - t("relations.alias.mutations.success.addressCopiedToClipboard") - } - /> - > + + {[ + + + + + + } + variant="text" + color="inherit" + onClick={() => { + copy(address) + + enqueueSnackbar( + t("relations.alias.mutations.success.addressCopiedToClipboard"), + { + variant: "success", + autoHideDuration: SUCCESS_SNACKBAR_SHOW_DURATION, + }, + ) + }} + sx={{textTransform: "none", fontWeight: "normal"}} + > + {address} + + + + + + , + + {encryptionStatus === EncryptionStatus.Available ? ( + + ) : ( + + )} + , + + + , + ]} + ) } diff --git a/src/route-widgets/AliasDetailRoute/AliasNotesForm.tsx b/src/route-widgets/AliasDetailRoute/AliasNotesForm.tsx index 7b53d04..9cb8391 100644 --- a/src/route-widgets/AliasDetailRoute/AliasNotesForm.tsx +++ b/src/route-widgets/AliasDetailRoute/AliasNotesForm.tsx @@ -6,6 +6,7 @@ import {MdCheckCircle, MdEditCalendar} from "react-icons/md" import {RiStickyNoteFill} from "react-icons/ri" import {FieldArray, FormikProvider, useFormik} from "formik" import {FaPen} from "react-icons/fa" +import {useTranslation} from "react-i18next" import deepEqual from "deep-equal" import format from "date-fns/format" import update from "immutability-helper" @@ -26,12 +27,13 @@ import { } from "@mui/material" import {parseFastAPIError} from "~/utils" -import {ErrorSnack, FaviconImage, SimpleOverlayInformation, SuccessSnack} from "~/components" +import {FaviconImage, SimpleOverlayInformation} from "~/components" import {Alias, AliasNote, DecryptedAlias} from "~/server-types" import {UpdateAliasData, updateAlias} from "~/apis" -import {useTranslation} from "react-i18next" +import {useErrorSuccessSnacks} from "~/hooks" import AddWebsiteField from "~/route-widgets/AliasDetailRoute/AddWebsiteField" import AuthContext from "~/AuthContext/AuthContext" +import FormikAutoLockNavigation from "~/LockNavigationContext/FormikAutoLockNavigation" import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes" export interface AliasNotesFormProps { @@ -60,8 +62,9 @@ const SCHEMA = yup.object().shape({ export default function AliasNotesForm({id, notes, onChanged}: AliasNotesFormProps): ReactElement { const {t} = useTranslation() + const {showError, showSuccess} = useErrorSuccessSnacks() const {_encryptUsingMasterPassword, _decryptUsingMasterPassword} = useContext(AuthContext) - const {mutateAsync, isSuccess} = useMutation( + const {mutateAsync} = useMutation( values => updateAlias(id, values), { onSuccess: newAlias => { @@ -70,8 +73,11 @@ export default function AliasNotesForm({id, notes, onChanged}: AliasNotesFormPro _decryptUsingMasterPassword, ) + showSuccess(t("relations.alias.mutations.success.notesUpdated")) + onChanged(newAlias as any as DecryptedAlias) }, + onError: showError, }, ) const initialValues = useMemo( @@ -287,10 +293,7 @@ export default function AliasNotesForm({id, notes, onChanged}: AliasNotesFormPro - - + > ) } diff --git a/src/route-widgets/AliasDetailRoute/AliasPreferencesForm.tsx b/src/route-widgets/AliasDetailRoute/AliasPreferencesForm.tsx index 6e7d9be..655c2a3 100644 --- a/src/route-widgets/AliasDetailRoute/AliasPreferencesForm.tsx +++ b/src/route-widgets/AliasDetailRoute/AliasPreferencesForm.tsx @@ -1,5 +1,5 @@ import * as yup from "yup" -import {ReactElement} from "react" +import {ReactElement, useContext} from "react" import {BsImage, BsShieldShaded} from "react-icons/bs" import {useFormik} from "formik" import {FaFile} from "react-icons/fa" @@ -15,14 +15,16 @@ import Icon from "@mdi/react" import {Alias, DecryptedAlias, ImageProxyFormatType, ProxyUserAgentType} from "~/server-types" import {UpdateAliasData, updateAlias} from "~/apis" -import {ErrorSnack, SuccessSnack} from "~/components" import {parseFastAPIError} from "~/utils" import { IMAGE_PROXY_FORMAT_TYPE_NAME_MAP, IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP, } from "~/constants/enum-mappings" +import {useErrorSuccessSnacks} from "~/hooks" +import AuthContext from "~/AuthContext/AuthContext" import FormikAutoLockNavigation from "~/LockNavigationContext/FormikAutoLockNavigation" import SelectField from "~/route-widgets/SettingsRoute/SelectField" +import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes" export interface AliasPreferencesFormProps { alias: Alias | DecryptedAlias @@ -45,6 +47,8 @@ export default function AliasPreferencesForm({ onChanged, }: AliasPreferencesFormProps): ReactElement { const {t} = useTranslation() + const {showSuccess, showError} = useErrorSuccessSnacks() + const {_decryptUsingMasterPassword} = useContext(AuthContext) const SCHEMA = yup.object().shape({ removeTrackers: yup .mixed() @@ -64,10 +68,19 @@ export default function AliasPreferencesForm({ .oneOf([null, ...Object.values(ProxyUserAgentType)]) .label(t("relations.alias.settings.imageProxyUserAgent.label")), }) - const {mutateAsync, isSuccess} = useMutation( + const {mutateAsync} = useMutation( data => updateAlias(alias.id, data), { - onSuccess: onChanged, + onSuccess: alias => { + showSuccess(t("relations.alias.mutations.success.aliasUpdated")) + ;(alias as any as DecryptedAlias).notes = decryptAliasNotes( + alias.encryptedNotes, + _decryptUsingMasterPassword, + ) + + onChanged(alias) + }, + onError: showError, }, ) const formik = useFormik({ @@ -186,10 +199,6 @@ export default function AliasPreferencesForm({ - - > ) } diff --git a/src/route-widgets/AliasDetailRoute/ChangeAliasActivationStatusSwitch.tsx b/src/route-widgets/AliasDetailRoute/ChangeAliasActivationStatusSwitch.tsx index 5a44d08..60bf631 100644 --- a/src/route-widgets/AliasDetailRoute/ChangeAliasActivationStatusSwitch.tsx +++ b/src/route-widgets/AliasDetailRoute/ChangeAliasActivationStatusSwitch.tsx @@ -1,4 +1,4 @@ -import {ReactElement, useContext, useState} from "react" +import {ReactElement, useContext} from "react" import {AxiosError} from "axios" import {useTranslation} from "react-i18next" @@ -7,9 +7,7 @@ import {useMutation} from "@tanstack/react-query" import {Alias, DecryptedAlias} from "~/server-types" import {UpdateAliasData, updateAlias} from "~/apis" -import {parseFastAPIError} from "~/utils" -import {ErrorSnack, SuccessSnack} from "~/components" -import {useUIState} from "~/hooks" +import {useErrorSuccessSnacks, useUIState} from "~/hooks" import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext" import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes" @@ -26,64 +24,47 @@ export default function ChangeAliasActivationStatusSwitch({ onChanged, }: ChangeAliasActivationStatusSwitchProps): ReactElement { const {t} = useTranslation() - const {_decryptUsingMasterPassword, encryptionStatus} = - useContext(AuthContext) + const {showError, showSuccess} = useErrorSuccessSnacks() + const {_decryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext) const [isActiveUIState, setIsActiveUIState] = useUIState(isActive) - const [successMessage, setSuccessMessage] = useState("") - const [errorMessage, setErrorMessage] = useState("") + const {mutateAsync, isLoading} = useMutation( + values => updateAlias(id, values), + { + onSuccess: newAlias => { + if (encryptionStatus === EncryptionStatus.Available) { + ;(newAlias as any as DecryptedAlias).notes = decryptAliasNotes( + newAlias.encryptedNotes, + _decryptUsingMasterPassword, + ) + } - const {mutateAsync, isLoading} = useMutation< - Alias, - AxiosError, - UpdateAliasData - >(values => updateAlias(id, values), { - onSuccess: newAlias => { - if (encryptionStatus === EncryptionStatus.Available) { - ;(newAlias as any as DecryptedAlias).notes = decryptAliasNotes( - newAlias.encryptedNotes, - _decryptUsingMasterPassword, - ) - } - - onChanged(newAlias) + onChanged(newAlias) + }, + onError: showError, }, - onError: error => - setErrorMessage(parseFastAPIError(error).detail as string), - }) + ) return ( - <> - { - setIsActiveUIState(!isActiveUIState) + { + setIsActiveUIState(!isActiveUIState) - try { - await mutateAsync({ - isActive: !isActiveUIState, - }) + try { + await mutateAsync({ + isActive: !isActiveUIState, + }) - if (!isActiveUIState) { - setSuccessMessage( - t( - "relations.alias.mutations.success.aliasChangedToEnabled", - ) as string, - ) - } else { - setSuccessMessage( - t( - "relations.alias.mutations.success.aliasChangedToDisabled", - ) as string, - ) - } - } catch {} - }} - /> - - - > + showSuccess( + isActiveUIState + ? t("relations.alias.mutations.success.aliasChangedToDisabled") + : t("relations.alias.mutations.success.aliasChangedToEnabled"), + ) + } catch {} + }} + /> ) } diff --git a/src/route-widgets/AliasesRoute/AliasesDetails.tsx b/src/route-widgets/AliasesRoute/AliasesDetails.tsx index 00b83ea..c2ab85f 100644 --- a/src/route-widgets/AliasesRoute/AliasesDetails.tsx +++ b/src/route-widgets/AliasesRoute/AliasesDetails.tsx @@ -1,4 +1,4 @@ -import {ReactElement, useState} from "react" +import {ReactElement} from "react" import {useTranslation} from "react-i18next" import {useKeyPress} from "react-use" import {MdContentCopy} from "react-icons/md" @@ -13,9 +13,11 @@ import { ListItemText, } from "@mui/material" -import {AliasTypeIndicator, SuccessSnack} from "~/components" +import {AliasTypeIndicator} from "~/components" import {AliasList} from "~/server-types" import {useUIState} from "~/hooks" +import {useSnackbar} from "notistack" +import {SUCCESS_SNACKBAR_SHOW_DURATION} from "~/constants/values" import CreateAliasButton from "~/route-widgets/AliasesRoute/CreateAliasButton" export interface AliasesDetailsProps { @@ -26,57 +28,58 @@ const getAddress = (alias: AliasList): string => `${alias.local}@${alias.domain} export default function AliasesDetails({aliases}: AliasesDetailsProps): ReactElement { const {t} = useTranslation() + const {enqueueSnackbar} = useSnackbar() const [isInCopyAddressMode] = useKeyPress("Control") const [aliasesUIState, setAliasesUIState] = useUIState(aliases) - const [hasCopiedToClipboard, setHasCopiedToClipboard] = useState(false) return ( - <> - - - - {aliasesUIState.map(alias => ( - { - if (isInCopyAddressMode) { - event.preventDefault() - event.stopPropagation() - copy(getAddress(alias)) - setHasCopiedToClipboard(true) - } - }} - href={`/aliases/${btoa(getAddress(alias))}`} - > - - - - - {isInCopyAddressMode && ( - - - - )} - - ))} - - - - - setAliasesUIState(currentAliases => [alias, ...currentAliases]) - } - /> - + + + + {aliasesUIState.map(alias => ( + { + if (isInCopyAddressMode) { + event.preventDefault() + event.stopPropagation() + + copy(getAddress(alias)) + + enqueueSnackbar( + t( + "relations.alias.mutations.success.addressCopiedToClipboard", + ), + { + variant: "success", + autoHideDuration: SUCCESS_SNACKBAR_SHOW_DURATION, + }, + ) + } + }} + href={`/aliases/${btoa(getAddress(alias))}`} + > + + + + + {isInCopyAddressMode && ( + + + + )} + + ))} + - setHasCopiedToClipboard(false)} - message={ - hasCopiedToClipboard && - t("relations.alias.mutations.success.addressCopiedToClipboard") - } - /> - > + + + setAliasesUIState(currentAliases => [alias, ...currentAliases]) + } + /> + + ) } diff --git a/src/route-widgets/AliasesRoute/CreateAliasButton.tsx b/src/route-widgets/AliasesRoute/CreateAliasButton.tsx index 2f185b7..1a9ae85 100644 --- a/src/route-widgets/AliasesRoute/CreateAliasButton.tsx +++ b/src/route-widgets/AliasesRoute/CreateAliasButton.tsx @@ -1,8 +1,10 @@ -import {ReactElement, useContext, useState} from "react" +import {ReactElement, useContext, useRef, useState} from "react" import {MdArrowDropDown} from "react-icons/md" import {BsArrowClockwise} from "react-icons/bs" import {FaPen} from "react-icons/fa" import {AxiosError} from "axios" +import {useTranslation} from "react-i18next" +import {SnackbarKey} from "notistack" import update from "immutability-helper" import { @@ -18,10 +20,8 @@ import {useMutation} from "@tanstack/react-query" import {CreateAliasData, createAlias} from "~/apis" import {Alias, AliasType} from "~/server-types" -import {parseFastAPIError} from "~/utils" -import {ErrorSnack, SuccessSnack} from "~/components" import {DEFAULT_ALIAS_NOTE} from "~/constants/values" -import {useTranslation} from "react-i18next" +import {useErrorSuccessSnacks} from "~/hooks" import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext" import CustomAliasDialog from "~/route-widgets/AliasesRoute/CustomAliasDialog" @@ -29,20 +29,14 @@ export interface CreateAliasButtonProps { onCreated: (alias: Alias) => void } -export default function CreateAliasButton({ - onCreated, -}: CreateAliasButtonProps): ReactElement { +export default function CreateAliasButton({onCreated}: CreateAliasButtonProps): ReactElement { const {t} = useTranslation() - const {_encryptUsingMasterPassword, encryptionStatus} = - useContext(AuthContext) + const {showSuccess, showError} = useErrorSuccessSnacks() + const {_encryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext) - const [errorMessage, setErrorMessage] = useState("") + const $errorSnackbarId = useRef(null) - const {mutateAsync, isLoading, isSuccess} = useMutation< - Alias, - AxiosError, - CreateAliasData - >( + const {mutateAsync, isLoading, isSuccess} = useMutation( async values => { if (encryptionStatus === EncryptionStatus.Available) { values.encryptedNotes = await _encryptUsingMasterPassword( @@ -61,14 +55,15 @@ export default function CreateAliasButton({ return createAlias(values) }, { - onSuccess: onCreated, - onError: error => - setErrorMessage(parseFastAPIError(error).detail as string), + onSuccess: alias => { + onCreated(alias) + showSuccess(t("relations.alias.mutations.success.aliasCreation")) + }, + onError: showError, }, ) - const [showCustomCreateDialog, setShowCustomCreateDialog] = - useState(false) + const [showCustomCreateDialog, setShowCustomCreateDialog] = useState(false) const [anchorElement, setAnchorElement] = useState(null) const open = Boolean(anchorElement) @@ -87,18 +82,11 @@ export default function CreateAliasButton({ > {t("routes.AliasesRoute.actions.createRandomAlias.label")} - setAnchorElement(event.currentTarget)} - > + setAnchorElement(event.currentTarget)}> - setAnchorElement(null)} - > + setAnchorElement(null)}> @@ -127,13 +113,6 @@ export default function CreateAliasButton({ }} onClose={() => setShowCustomCreateDialog(false)} /> - - > ) } diff --git a/src/route-widgets/SettingsRoute/AliasesPreferencesForm.tsx b/src/route-widgets/SettingsRoute/AliasesPreferencesForm.tsx index c50c73d..c2b719e 100644 --- a/src/route-widgets/SettingsRoute/AliasesPreferencesForm.tsx +++ b/src/route-widgets/SettingsRoute/AliasesPreferencesForm.tsx @@ -24,15 +24,13 @@ import {LoadingButton} from "@mui/lab" import {ImageProxyFormatType, ProxyUserAgentType, SimpleDetailResponse} from "~/server-types" import {UpdatePreferencesData, updatePreferences} from "~/apis" -import {useUser} from "~/hooks" +import {useErrorSuccessSnacks, useUser} from "~/hooks" import {parseFastAPIError} from "~/utils" -import {SuccessSnack} from "~/components" import { IMAGE_PROXY_FORMAT_TYPE_NAME_MAP, IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP, } from "~/constants/enum-mappings" import AuthContext from "~/AuthContext/AuthContext" -import ErrorSnack from "~/components/ErrorSnack" interface Form { removeTrackers: boolean @@ -47,6 +45,7 @@ interface Form { export default function AliasesPreferencesForm(): ReactElement { const {_updateUser} = useContext(AuthContext) const user = useUser() + const {showError, showSuccess} = useErrorSuccessSnacks() const {t} = useTranslation() const SCHEMA = yup.object().shape({ removeTrackers: yup.boolean().label(t("relations.alias.settings.removeTrackers.label")), @@ -69,7 +68,7 @@ export default function AliasesPreferencesForm(): ReactElement { AxiosError, UpdatePreferencesData >(updatePreferences, { - onSuccess: (_, values) => { + onSuccess: (response, values) => { const newUser = { ...user, preferences: { @@ -78,8 +77,13 @@ export default function AliasesPreferencesForm(): ReactElement { }, } + if (response.detail) { + showSuccess(response?.detail) + } + _updateUser(newUser) }, + onError: showError, }) const formik = useFormik({ validationSchema: SCHEMA, @@ -108,227 +112,220 @@ export default function AliasesPreferencesForm(): ReactElement { const isLarge = useMediaQuery(theme.breakpoints.up("md")) return ( - <> - - - - - {t("routes.SettingsRoute.forms.aliasPreferences.title")} - - - - - {t("routes.SettingsRoute.forms.aliasPreferences.description")} - - - - - - - + + + + {t("routes.SettingsRoute.forms.aliasPreferences.title")} + + + + + {t("routes.SettingsRoute.forms.aliasPreferences.description")} + + + + + + + + } + labelPlacement="start" + label="Remove Trackers" + /> + + {(formik.touched.createMailReport && + formik.errors.createMailReport) || + t("relations.alias.settings.removeTrackers.helperText")} + + + + + + + } + labelPlacement="start" + label="Create Reports" + /> + + {(formik.touched.createMailReport && + formik.errors.createMailReport) || + t("relations.alias.settings.createMailReports.helperText")} + + + + + + + } + labelPlacement="start" + label="Proxy Images" + /> + + {(formik.touched.proxyImages && formik.errors.proxyImages) || + t("relations.alias.settings.proxyImages.helperText")} + + + + + + + + + + ), + }} + name="imageProxyFormat" + id="imageProxyFormat" + label="Image File Type" + value={formik.values.imageProxyFormat} onChange={formik.handleChange} - onBlur={formik.handleBlur} - /> - } - labelPlacement="start" - label="Remove Trackers" - /> - - {(formik.touched.createMailReport && - formik.errors.createMailReport) || - t("relations.alias.settings.removeTrackers.helperText")} - - - - - - - } - labelPlacement="start" - label="Create Reports" - /> - - {(formik.touched.createMailReport && - formik.errors.createMailReport) || - t( - "relations.alias.settings.createMailReports.helperText", - )} - - - - - - - } - labelPlacement="start" - label="Proxy Images" - /> - - {(formik.touched.proxyImages && - formik.errors.proxyImages) || - t("relations.alias.settings.proxyImages.helperText")} - - - - - - - - - - ), - }} - name="imageProxyFormat" - id="imageProxyFormat" - label="Image File Type" - value={formik.values.imageProxyFormat} - onChange={formik.handleChange} - disabled={formik.isSubmitting} - error={ - formik.touched.imageProxyFormat && - Boolean(formik.errors.imageProxyFormat) - } - helperText={ - formik.touched.imageProxyFormat && - formik.errors.imageProxyFormat - } - > - {Object.entries( - IMAGE_PROXY_FORMAT_TYPE_NAME_MAP, - ).map(([value, translationString]) => ( - - {t(translationString)} - - ))} - - - {formik.touched.imageProxyFormat && - formik.errors.imageProxyFormat} - - - - - - - {Object.entries( - IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP, - ).map(([value, translationString]) => ( - - {t(translationString)} - - ))} - - - {(formik.touched.imageProxyUserAgent && - formik.errors.imageProxyUserAgent) || - t( - "relations.alias.settings.imageProxyUserAgent.helperText", - )} - - - + disabled={formik.isSubmitting} + error={ + formik.touched.imageProxyFormat && + Boolean(formik.errors.imageProxyFormat) + } + helperText={ + formik.touched.imageProxyFormat && + formik.errors.imageProxyFormat + } + > + {Object.entries( + IMAGE_PROXY_FORMAT_TYPE_NAME_MAP, + ).map(([value, translationString]) => ( + + {t(translationString)} + + ))} + + + {formik.touched.imageProxyFormat && + formik.errors.imageProxyFormat} + + - - + + + + {Object.entries( + IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP, + ).map(([value, translationString]) => ( + + {t(translationString)} + + ))} + + + {(formik.touched.imageProxyUserAgent && + formik.errors.imageProxyUserAgent) || + t( + "relations.alias.settings.imageProxyUserAgent.helperText", + )} + + + + + - - } - > - Save Preferences - - - - - - > + + } + > + Save Preferences + + + + ) } diff --git a/src/utils/parse-fastapi-error.ts b/src/utils/parse-fastapi-error.ts index 51f4a85..a055b80 100644 --- a/src/utils/parse-fastapi-error.ts +++ b/src/utils/parse-fastapi-error.ts @@ -20,7 +20,7 @@ export default function parseFastAPIError( if (typeof error === "undefined") { return { - detail: "There was an error", + detail: undefined, } }