improved snacks

This commit is contained in:
Myzel394 2022-11-01 21:13:18 +01:00
parent 8616d4085a
commit 2cb17e5550
14 changed files with 477 additions and 454 deletions

View File

@ -32,6 +32,7 @@
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"in-milliseconds": "^1.2.0", "in-milliseconds": "^1.2.0",
"in-seconds": "^1.2.0", "in-seconds": "^1.2.0",
"notistack": "^2.0.8",
"openpgp": "^5.5.0", "openpgp": "^5.5.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -1,12 +1,13 @@
import {RouterProvider, createBrowserRouter} from "react-router-dom" import {RouterProvider, createBrowserRouter} from "react-router-dom"
import {SnackbarProvider} from "notistack"
import React, {ReactElement} from "react" import React, {ReactElement} from "react"
import {QueryClientProvider} from "@tanstack/react-query" import {QueryClientProvider} from "@tanstack/react-query"
import {CssBaseline, ThemeProvider} from "@mui/material" import {CssBaseline, ThemeProvider} from "@mui/material"
import {queryClient} from "~/constants/react-query" import {queryClient} from "~/constants/react-query"
import {lightTheme} from "~/constants/themes"
import {getServerSettings} from "~/apis" import {getServerSettings} from "~/apis"
import {lightTheme} from "~/constants/themes"
import AliasDetailRoute from "~/routes/AliasDetailRoute" import AliasDetailRoute from "~/routes/AliasDetailRoute"
import AliasesRoute from "~/routes/AliasesRoute" import AliasesRoute from "~/routes/AliasesRoute"
import AuthContextProvider from "~/AuthContext/AuthContextProvider" import AuthContextProvider from "~/AuthContext/AuthContextProvider"
@ -96,10 +97,12 @@ export default function App(): ReactElement {
<React.StrictMode> <React.StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<SnackbarProvider>
<AuthContextProvider> <AuthContextProvider>
<CssBaseline /> <CssBaseline />
<RouterProvider router={router} /> <RouterProvider router={router} />
</AuthContextProvider> </AuthContextProvider>
</SnackbarProvider>
</ThemeProvider> </ThemeProvider>
</QueryClientProvider> </QueryClientProvider>
</React.StrictMode> </React.StrictMode>

View File

@ -1,5 +1,7 @@
import {FormikContextType} from "formik" 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" import LockNavigationContext from "./LockNavigationContext"
@ -12,16 +14,14 @@ export default function FormikAutoLockNavigation({
}: LockNavigationContextProviderProps): null { }: LockNavigationContextProviderProps): null {
const {lock, release} = useContext(LockNavigationContext) const {lock, release} = useContext(LockNavigationContext)
const valuesStringified = JSON.stringify(formik.values) // TODO: Not working yet
const initialValuesStringified = JSON.stringify(formik.initialValues) useDeepCompareEffect(() => {
if (!deepEqual(formik.values, formik.initialValues)) {
useEffect(() => {
if (valuesStringified !== initialValuesStringified) {
lock() lock()
} else { } else {
release() release()
} }
}, [lock, release, valuesStringified, initialValuesStringified]) }, [lock, release, formik.values, formik.initialValues])
return null return null
} }

View File

@ -12,3 +12,5 @@ export const DEFAULT_ALIAS_NOTE: AliasNote = {
websites: [], websites: [],
}, },
} }
export const ERROR_SNACKBAR_SHOW_DURATION = 5000
export const SUCCESS_SNACKBAR_SHOW_DURATION = 2000

View File

@ -10,3 +10,5 @@ export * from "./use-ui-state"
export {default as useUIState} from "./use-ui-state" export {default as useUIState} from "./use-ui-state"
export * from "./use-navigate-to-next" export * from "./use-navigate-to-next"
export {default as useNavigateToNext} 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"

View File

@ -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<SnackbarKey | null>(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,
}
}

View File

@ -1,19 +1,16 @@
import {useParams} from "react-router" import {useParams} from "react-router"
import {ReactElement, useContext, useState} from "react" import {ReactElement, useContext} from "react"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
import {MdContentCopy} from "react-icons/md" import {MdContentCopy} from "react-icons/md"
import {useSnackbar} from "notistack"
import copy from "copy-to-clipboard" import copy from "copy-to-clipboard"
import {Button, Grid} from "@mui/material" import {Button, Grid} from "@mui/material"
import { import {AliasTypeIndicator, DecryptionPasswordMissingAlert, SimplePageBuilder} from "~/components"
AliasTypeIndicator,
DecryptionPasswordMissingAlert,
SimplePageBuilder,
SuccessSnack,
} from "~/components"
import {Alias, DecryptedAlias} from "~/server-types" import {Alias, DecryptedAlias} from "~/server-types"
import {useUIState} from "~/hooks" import {useUIState} from "~/hooks"
import {SUCCESS_SNACKBAR_SHOW_DURATION} from "~/constants/values"
import AliasNotesForm from "~/route-widgets/AliasDetailRoute/AliasNotesForm" import AliasNotesForm from "~/route-widgets/AliasDetailRoute/AliasNotesForm"
import AliasPreferencesForm from "~/route-widgets/AliasDetailRoute/AliasPreferencesForm" import AliasPreferencesForm from "~/route-widgets/AliasDetailRoute/AliasPreferencesForm"
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext" import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
@ -25,15 +22,14 @@ export interface AliasDetailsProps {
export default function AliasDetails({alias: aliasValue}: AliasDetailsProps): ReactElement { export default function AliasDetails({alias: aliasValue}: AliasDetailsProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const {enqueueSnackbar} = useSnackbar()
const params = useParams() const params = useParams()
const {encryptionStatus} = useContext(AuthContext) const {encryptionStatus} = useContext(AuthContext)
const address = atob(params.addressInBase64 as string) const address = atob(params.addressInBase64 as string)
const [aliasUIState, setAliasUIState] = useUIState<Alias | DecryptedAlias>(aliasValue) const [aliasUIState, setAliasUIState] = useUIState<Alias | DecryptedAlias>(aliasValue)
const [hasCopiedToClipboard, setHasCopiedToClipboard] = useState<boolean>(false)
return ( return (
<>
<SimplePageBuilder.MultipleSections> <SimplePageBuilder.MultipleSections>
{[ {[
<Grid key="basic" container spacing={1} direction="row" alignItems="center"> <Grid key="basic" container spacing={1} direction="row" alignItems="center">
@ -47,7 +43,14 @@ export default function AliasDetails({alias: aliasValue}: AliasDetailsProps): Re
color="inherit" color="inherit"
onClick={() => { onClick={() => {
copy(address) copy(address)
setHasCopiedToClipboard(true)
enqueueSnackbar(
t("relations.alias.mutations.success.addressCopiedToClipboard"),
{
variant: "success",
autoHideDuration: SUCCESS_SNACKBAR_SHOW_DURATION,
},
)
}} }}
sx={{textTransform: "none", fontWeight: "normal"}} sx={{textTransform: "none", fontWeight: "normal"}}
> >
@ -81,13 +84,5 @@ export default function AliasDetails({alias: aliasValue}: AliasDetailsProps): Re
</SimplePageBuilder.Section>, </SimplePageBuilder.Section>,
]} ]}
</SimplePageBuilder.MultipleSections> </SimplePageBuilder.MultipleSections>
<SuccessSnack
onClose={() => setHasCopiedToClipboard(false)}
message={
hasCopiedToClipboard &&
t("relations.alias.mutations.success.addressCopiedToClipboard")
}
/>
</>
) )
} }

View File

@ -6,6 +6,7 @@ import {MdCheckCircle, MdEditCalendar} from "react-icons/md"
import {RiStickyNoteFill} from "react-icons/ri" import {RiStickyNoteFill} from "react-icons/ri"
import {FieldArray, FormikProvider, useFormik} from "formik" import {FieldArray, FormikProvider, useFormik} from "formik"
import {FaPen} from "react-icons/fa" import {FaPen} from "react-icons/fa"
import {useTranslation} from "react-i18next"
import deepEqual from "deep-equal" import deepEqual from "deep-equal"
import format from "date-fns/format" import format from "date-fns/format"
import update from "immutability-helper" import update from "immutability-helper"
@ -26,12 +27,13 @@ import {
} from "@mui/material" } from "@mui/material"
import {parseFastAPIError} from "~/utils" import {parseFastAPIError} from "~/utils"
import {ErrorSnack, FaviconImage, SimpleOverlayInformation, SuccessSnack} from "~/components" import {FaviconImage, SimpleOverlayInformation} from "~/components"
import {Alias, AliasNote, DecryptedAlias} from "~/server-types" import {Alias, AliasNote, DecryptedAlias} from "~/server-types"
import {UpdateAliasData, updateAlias} from "~/apis" import {UpdateAliasData, updateAlias} from "~/apis"
import {useTranslation} from "react-i18next" import {useErrorSuccessSnacks} from "~/hooks"
import AddWebsiteField from "~/route-widgets/AliasDetailRoute/AddWebsiteField" import AddWebsiteField from "~/route-widgets/AliasDetailRoute/AddWebsiteField"
import AuthContext from "~/AuthContext/AuthContext" import AuthContext from "~/AuthContext/AuthContext"
import FormikAutoLockNavigation from "~/LockNavigationContext/FormikAutoLockNavigation"
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes" import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
export interface AliasNotesFormProps { export interface AliasNotesFormProps {
@ -60,8 +62,9 @@ const SCHEMA = yup.object().shape({
export default function AliasNotesForm({id, notes, onChanged}: AliasNotesFormProps): ReactElement { export default function AliasNotesForm({id, notes, onChanged}: AliasNotesFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const {showError, showSuccess} = useErrorSuccessSnacks()
const {_encryptUsingMasterPassword, _decryptUsingMasterPassword} = useContext(AuthContext) const {_encryptUsingMasterPassword, _decryptUsingMasterPassword} = useContext(AuthContext)
const {mutateAsync, isSuccess} = useMutation<Alias, AxiosError, UpdateAliasData>( const {mutateAsync} = useMutation<Alias, AxiosError, UpdateAliasData>(
values => updateAlias(id, values), values => updateAlias(id, values),
{ {
onSuccess: newAlias => { onSuccess: newAlias => {
@ -70,8 +73,11 @@ export default function AliasNotesForm({id, notes, onChanged}: AliasNotesFormPro
_decryptUsingMasterPassword, _decryptUsingMasterPassword,
) )
showSuccess(t("relations.alias.mutations.success.notesUpdated"))
onChanged(newAlias as any as DecryptedAlias) onChanged(newAlias as any as DecryptedAlias)
}, },
onError: showError,
}, },
) )
const initialValues = useMemo( const initialValues = useMemo(
@ -287,10 +293,7 @@ export default function AliasNotesForm({id, notes, onChanged}: AliasNotesFormPro
</Grid> </Grid>
</Grid> </Grid>
</form> </form>
<ErrorSnack message={formik.errors.detail} /> <FormikAutoLockNavigation formik={formik} />
<SuccessSnack
message={isSuccess && t("relations.alias.mutations.success.notesUpdated")}
/>
</> </>
) )
} }

View File

@ -1,5 +1,5 @@
import * as yup from "yup" import * as yup from "yup"
import {ReactElement} from "react" import {ReactElement, useContext} from "react"
import {BsImage, BsShieldShaded} from "react-icons/bs" import {BsImage, BsShieldShaded} from "react-icons/bs"
import {useFormik} from "formik" import {useFormik} from "formik"
import {FaFile} from "react-icons/fa" import {FaFile} from "react-icons/fa"
@ -15,14 +15,16 @@ import Icon from "@mdi/react"
import {Alias, DecryptedAlias, ImageProxyFormatType, ProxyUserAgentType} from "~/server-types" import {Alias, DecryptedAlias, ImageProxyFormatType, ProxyUserAgentType} from "~/server-types"
import {UpdateAliasData, updateAlias} from "~/apis" import {UpdateAliasData, updateAlias} from "~/apis"
import {ErrorSnack, SuccessSnack} from "~/components"
import {parseFastAPIError} from "~/utils" import {parseFastAPIError} from "~/utils"
import { import {
IMAGE_PROXY_FORMAT_TYPE_NAME_MAP, IMAGE_PROXY_FORMAT_TYPE_NAME_MAP,
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP, IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP,
} from "~/constants/enum-mappings" } from "~/constants/enum-mappings"
import {useErrorSuccessSnacks} from "~/hooks"
import AuthContext from "~/AuthContext/AuthContext"
import FormikAutoLockNavigation from "~/LockNavigationContext/FormikAutoLockNavigation" import FormikAutoLockNavigation from "~/LockNavigationContext/FormikAutoLockNavigation"
import SelectField from "~/route-widgets/SettingsRoute/SelectField" import SelectField from "~/route-widgets/SettingsRoute/SelectField"
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
export interface AliasPreferencesFormProps { export interface AliasPreferencesFormProps {
alias: Alias | DecryptedAlias alias: Alias | DecryptedAlias
@ -45,6 +47,8 @@ export default function AliasPreferencesForm({
onChanged, onChanged,
}: AliasPreferencesFormProps): ReactElement { }: AliasPreferencesFormProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const {showSuccess, showError} = useErrorSuccessSnacks()
const {_decryptUsingMasterPassword} = useContext(AuthContext)
const SCHEMA = yup.object().shape({ const SCHEMA = yup.object().shape({
removeTrackers: yup removeTrackers: yup
.mixed<boolean | null>() .mixed<boolean | null>()
@ -64,10 +68,19 @@ export default function AliasPreferencesForm({
.oneOf([null, ...Object.values(ProxyUserAgentType)]) .oneOf([null, ...Object.values(ProxyUserAgentType)])
.label(t("relations.alias.settings.imageProxyUserAgent.label")), .label(t("relations.alias.settings.imageProxyUserAgent.label")),
}) })
const {mutateAsync, isSuccess} = useMutation<Alias, AxiosError, UpdateAliasData>( const {mutateAsync} = useMutation<Alias, AxiosError, UpdateAliasData>(
data => updateAlias(alias.id, data), 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<Form>({ const formik = useFormik<Form>({
@ -186,10 +199,6 @@ export default function AliasPreferencesForm({
</Grid> </Grid>
</form> </form>
<FormikAutoLockNavigation formik={formik} /> <FormikAutoLockNavigation formik={formik} />
<ErrorSnack message={formik.errors.detail} />
<SuccessSnack
message={isSuccess && t("relations.alias.mutations.success.aliasUpdated")}
/>
</> </>
) )
} }

View File

@ -1,4 +1,4 @@
import {ReactElement, useContext, useState} from "react" import {ReactElement, useContext} from "react"
import {AxiosError} from "axios" import {AxiosError} from "axios"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
@ -7,9 +7,7 @@ import {useMutation} from "@tanstack/react-query"
import {Alias, DecryptedAlias} from "~/server-types" import {Alias, DecryptedAlias} from "~/server-types"
import {UpdateAliasData, updateAlias} from "~/apis" import {UpdateAliasData, updateAlias} from "~/apis"
import {parseFastAPIError} from "~/utils" import {useErrorSuccessSnacks, useUIState} from "~/hooks"
import {ErrorSnack, SuccessSnack} from "~/components"
import {useUIState} from "~/hooks"
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext" import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes" import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
@ -26,19 +24,14 @@ export default function ChangeAliasActivationStatusSwitch({
onChanged, onChanged,
}: ChangeAliasActivationStatusSwitchProps): ReactElement { }: ChangeAliasActivationStatusSwitchProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const {_decryptUsingMasterPassword, encryptionStatus} = const {showError, showSuccess} = useErrorSuccessSnacks()
useContext(AuthContext) const {_decryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext)
const [isActiveUIState, setIsActiveUIState] = useUIState<boolean>(isActive) const [isActiveUIState, setIsActiveUIState] = useUIState<boolean>(isActive)
const [successMessage, setSuccessMessage] = useState<string>("") const {mutateAsync, isLoading} = useMutation<Alias, AxiosError, UpdateAliasData>(
const [errorMessage, setErrorMessage] = useState<string>("") values => updateAlias(id, values),
{
const {mutateAsync, isLoading} = useMutation<
Alias,
AxiosError,
UpdateAliasData
>(values => updateAlias(id, values), {
onSuccess: newAlias => { onSuccess: newAlias => {
if (encryptionStatus === EncryptionStatus.Available) { if (encryptionStatus === EncryptionStatus.Available) {
;(newAlias as any as DecryptedAlias).notes = decryptAliasNotes( ;(newAlias as any as DecryptedAlias).notes = decryptAliasNotes(
@ -49,12 +42,11 @@ export default function ChangeAliasActivationStatusSwitch({
onChanged(newAlias) onChanged(newAlias)
}, },
onError: error => onError: showError,
setErrorMessage(parseFastAPIError(error).detail as string), },
}) )
return ( return (
<>
<Switch <Switch
checked={isActiveUIState} checked={isActiveUIState}
disabled={isActiveUIState === null || isLoading} disabled={isActiveUIState === null || isLoading}
@ -66,24 +58,13 @@ export default function ChangeAliasActivationStatusSwitch({
isActive: !isActiveUIState, isActive: !isActiveUIState,
}) })
if (!isActiveUIState) { showSuccess(
setSuccessMessage( isActiveUIState
t( ? t("relations.alias.mutations.success.aliasChangedToDisabled")
"relations.alias.mutations.success.aliasChangedToEnabled", : t("relations.alias.mutations.success.aliasChangedToEnabled"),
) as string,
) )
} else {
setSuccessMessage(
t(
"relations.alias.mutations.success.aliasChangedToDisabled",
) as string,
)
}
} catch {} } catch {}
}} }}
/> />
<SuccessSnack message={successMessage} />
<ErrorSnack message={errorMessage} />
</>
) )
} }

View File

@ -1,4 +1,4 @@
import {ReactElement, useState} from "react" import {ReactElement} from "react"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
import {useKeyPress} from "react-use" import {useKeyPress} from "react-use"
import {MdContentCopy} from "react-icons/md" import {MdContentCopy} from "react-icons/md"
@ -13,9 +13,11 @@ import {
ListItemText, ListItemText,
} from "@mui/material" } from "@mui/material"
import {AliasTypeIndicator, SuccessSnack} from "~/components" import {AliasTypeIndicator} from "~/components"
import {AliasList} from "~/server-types" import {AliasList} from "~/server-types"
import {useUIState} from "~/hooks" import {useUIState} from "~/hooks"
import {useSnackbar} from "notistack"
import {SUCCESS_SNACKBAR_SHOW_DURATION} from "~/constants/values"
import CreateAliasButton from "~/route-widgets/AliasesRoute/CreateAliasButton" import CreateAliasButton from "~/route-widgets/AliasesRoute/CreateAliasButton"
export interface AliasesDetailsProps { export interface AliasesDetailsProps {
@ -26,13 +28,12 @@ const getAddress = (alias: AliasList): string => `${alias.local}@${alias.domain}
export default function AliasesDetails({aliases}: AliasesDetailsProps): ReactElement { export default function AliasesDetails({aliases}: AliasesDetailsProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const {enqueueSnackbar} = useSnackbar()
const [isInCopyAddressMode] = useKeyPress("Control") const [isInCopyAddressMode] = useKeyPress("Control")
const [aliasesUIState, setAliasesUIState] = useUIState<AliasList[]>(aliases) const [aliasesUIState, setAliasesUIState] = useUIState<AliasList[]>(aliases)
const [hasCopiedToClipboard, setHasCopiedToClipboard] = useState<boolean>(false)
return ( return (
<>
<Grid container spacing={4} direction="column"> <Grid container spacing={4} direction="column">
<Grid item> <Grid item>
<List> <List>
@ -43,8 +44,18 @@ export default function AliasesDetails({aliases}: AliasesDetailsProps): ReactEle
if (isInCopyAddressMode) { if (isInCopyAddressMode) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
copy(getAddress(alias)) copy(getAddress(alias))
setHasCopiedToClipboard(true)
enqueueSnackbar(
t(
"relations.alias.mutations.success.addressCopiedToClipboard",
),
{
variant: "success",
autoHideDuration: SUCCESS_SNACKBAR_SHOW_DURATION,
},
)
} }
}} }}
href={`/aliases/${btoa(getAddress(alias))}`} href={`/aliases/${btoa(getAddress(alias))}`}
@ -70,13 +81,5 @@ export default function AliasesDetails({aliases}: AliasesDetailsProps): ReactEle
/> />
</Grid> </Grid>
</Grid> </Grid>
<SuccessSnack
onClose={() => setHasCopiedToClipboard(false)}
message={
hasCopiedToClipboard &&
t("relations.alias.mutations.success.addressCopiedToClipboard")
}
/>
</>
) )
} }

View File

@ -1,8 +1,10 @@
import {ReactElement, useContext, useState} from "react" import {ReactElement, useContext, useRef, useState} from "react"
import {MdArrowDropDown} from "react-icons/md" import {MdArrowDropDown} from "react-icons/md"
import {BsArrowClockwise} from "react-icons/bs" import {BsArrowClockwise} from "react-icons/bs"
import {FaPen} from "react-icons/fa" import {FaPen} from "react-icons/fa"
import {AxiosError} from "axios" import {AxiosError} from "axios"
import {useTranslation} from "react-i18next"
import {SnackbarKey} from "notistack"
import update from "immutability-helper" import update from "immutability-helper"
import { import {
@ -18,10 +20,8 @@ import {useMutation} from "@tanstack/react-query"
import {CreateAliasData, createAlias} from "~/apis" import {CreateAliasData, createAlias} from "~/apis"
import {Alias, AliasType} from "~/server-types" import {Alias, AliasType} from "~/server-types"
import {parseFastAPIError} from "~/utils"
import {ErrorSnack, SuccessSnack} from "~/components"
import {DEFAULT_ALIAS_NOTE} from "~/constants/values" import {DEFAULT_ALIAS_NOTE} from "~/constants/values"
import {useTranslation} from "react-i18next" import {useErrorSuccessSnacks} from "~/hooks"
import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext" import AuthContext, {EncryptionStatus} from "~/AuthContext/AuthContext"
import CustomAliasDialog from "~/route-widgets/AliasesRoute/CustomAliasDialog" import CustomAliasDialog from "~/route-widgets/AliasesRoute/CustomAliasDialog"
@ -29,20 +29,14 @@ export interface CreateAliasButtonProps {
onCreated: (alias: Alias) => void onCreated: (alias: Alias) => void
} }
export default function CreateAliasButton({ export default function CreateAliasButton({onCreated}: CreateAliasButtonProps): ReactElement {
onCreated,
}: CreateAliasButtonProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const {_encryptUsingMasterPassword, encryptionStatus} = const {showSuccess, showError} = useErrorSuccessSnacks()
useContext(AuthContext) const {_encryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext)
const [errorMessage, setErrorMessage] = useState<string>("") const $errorSnackbarId = useRef<SnackbarKey | null>(null)
const {mutateAsync, isLoading, isSuccess} = useMutation< const {mutateAsync, isLoading, isSuccess} = useMutation<Alias, AxiosError, CreateAliasData>(
Alias,
AxiosError,
CreateAliasData
>(
async values => { async values => {
if (encryptionStatus === EncryptionStatus.Available) { if (encryptionStatus === EncryptionStatus.Available) {
values.encryptedNotes = await _encryptUsingMasterPassword( values.encryptedNotes = await _encryptUsingMasterPassword(
@ -61,14 +55,15 @@ export default function CreateAliasButton({
return createAlias(values) return createAlias(values)
}, },
{ {
onSuccess: onCreated, onSuccess: alias => {
onError: error => onCreated(alias)
setErrorMessage(parseFastAPIError(error).detail as string), showSuccess(t("relations.alias.mutations.success.aliasCreation"))
},
onError: showError,
}, },
) )
const [showCustomCreateDialog, setShowCustomCreateDialog] = const [showCustomCreateDialog, setShowCustomCreateDialog] = useState<boolean>(false)
useState<boolean>(false)
const [anchorElement, setAnchorElement] = useState<HTMLElement | null>(null) const [anchorElement, setAnchorElement] = useState<HTMLElement | null>(null)
const open = Boolean(anchorElement) const open = Boolean(anchorElement)
@ -87,18 +82,11 @@ export default function CreateAliasButton({
> >
{t("routes.AliasesRoute.actions.createRandomAlias.label")} {t("routes.AliasesRoute.actions.createRandomAlias.label")}
</Button> </Button>
<Button <Button size="small" onClick={event => setAnchorElement(event.currentTarget)}>
size="small"
onClick={event => setAnchorElement(event.currentTarget)}
>
<MdArrowDropDown /> <MdArrowDropDown />
</Button> </Button>
</ButtonGroup> </ButtonGroup>
<Menu <Menu anchorEl={anchorElement} open={open} onClose={() => setAnchorElement(null)}>
anchorEl={anchorElement}
open={open}
onClose={() => setAnchorElement(null)}
>
<MenuList> <MenuList>
<MenuItem <MenuItem
disabled={isLoading} disabled={isLoading}
@ -111,9 +99,7 @@ export default function CreateAliasButton({
<FaPen /> <FaPen />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
primary={t( primary={t("routes.AliasesRoute.actions.createCustomAlias.label")}
"routes.AliasesRoute.actions.createCustomAlias.label",
)}
/> />
</MenuItem> </MenuItem>
</MenuList> </MenuList>
@ -127,13 +113,6 @@ export default function CreateAliasButton({
}} }}
onClose={() => setShowCustomCreateDialog(false)} onClose={() => setShowCustomCreateDialog(false)}
/> />
<ErrorSnack message={errorMessage} />
<SuccessSnack
message={
isSuccess &&
t("relations.alias.mutations.success.aliasCreation")
}
/>
</> </>
) )
} }

View File

@ -24,15 +24,13 @@ import {LoadingButton} from "@mui/lab"
import {ImageProxyFormatType, ProxyUserAgentType, SimpleDetailResponse} from "~/server-types" import {ImageProxyFormatType, ProxyUserAgentType, SimpleDetailResponse} from "~/server-types"
import {UpdatePreferencesData, updatePreferences} from "~/apis" import {UpdatePreferencesData, updatePreferences} from "~/apis"
import {useUser} from "~/hooks" import {useErrorSuccessSnacks, useUser} from "~/hooks"
import {parseFastAPIError} from "~/utils" import {parseFastAPIError} from "~/utils"
import {SuccessSnack} from "~/components"
import { import {
IMAGE_PROXY_FORMAT_TYPE_NAME_MAP, IMAGE_PROXY_FORMAT_TYPE_NAME_MAP,
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP, IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP,
} from "~/constants/enum-mappings" } from "~/constants/enum-mappings"
import AuthContext from "~/AuthContext/AuthContext" import AuthContext from "~/AuthContext/AuthContext"
import ErrorSnack from "~/components/ErrorSnack"
interface Form { interface Form {
removeTrackers: boolean removeTrackers: boolean
@ -47,6 +45,7 @@ interface Form {
export default function AliasesPreferencesForm(): ReactElement { export default function AliasesPreferencesForm(): ReactElement {
const {_updateUser} = useContext(AuthContext) const {_updateUser} = useContext(AuthContext)
const user = useUser() const user = useUser()
const {showError, showSuccess} = useErrorSuccessSnacks()
const {t} = useTranslation() const {t} = useTranslation()
const SCHEMA = yup.object().shape({ const SCHEMA = yup.object().shape({
removeTrackers: yup.boolean().label(t("relations.alias.settings.removeTrackers.label")), removeTrackers: yup.boolean().label(t("relations.alias.settings.removeTrackers.label")),
@ -69,7 +68,7 @@ export default function AliasesPreferencesForm(): ReactElement {
AxiosError, AxiosError,
UpdatePreferencesData UpdatePreferencesData
>(updatePreferences, { >(updatePreferences, {
onSuccess: (_, values) => { onSuccess: (response, values) => {
const newUser = { const newUser = {
...user, ...user,
preferences: { preferences: {
@ -78,8 +77,13 @@ export default function AliasesPreferencesForm(): ReactElement {
}, },
} }
if (response.detail) {
showSuccess(response?.detail)
}
_updateUser(newUser) _updateUser(newUser)
}, },
onError: showError,
}) })
const formik = useFormik<Form>({ const formik = useFormik<Form>({
validationSchema: SCHEMA, validationSchema: SCHEMA,
@ -108,7 +112,6 @@ export default function AliasesPreferencesForm(): ReactElement {
const isLarge = useMediaQuery(theme.breakpoints.up("md")) const isLarge = useMediaQuery(theme.breakpoints.up("md"))
return ( return (
<>
<form onSubmit={formik.handleSubmit}> <form onSubmit={formik.handleSubmit}>
<Grid container spacing={4} flexDirection="column" alignItems="center"> <Grid container spacing={4} flexDirection="column" alignItems="center">
<Grid item> <Grid item>
@ -181,9 +184,7 @@ export default function AliasesPreferencesForm(): ReactElement {
> >
{(formik.touched.createMailReport && {(formik.touched.createMailReport &&
formik.errors.createMailReport) || formik.errors.createMailReport) ||
t( t("relations.alias.settings.createMailReports.helperText")}
"relations.alias.settings.createMailReports.helperText",
)}
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
@ -208,8 +209,7 @@ export default function AliasesPreferencesForm(): ReactElement {
formik.touched.proxyImages && formik.errors.proxyImages, formik.touched.proxyImages && formik.errors.proxyImages,
)} )}
> >
{(formik.touched.proxyImages && {(formik.touched.proxyImages && formik.errors.proxyImages) ||
formik.errors.proxyImages) ||
t("relations.alias.settings.proxyImages.helperText")} t("relations.alias.settings.proxyImages.helperText")}
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
@ -327,8 +327,5 @@ export default function AliasesPreferencesForm(): ReactElement {
</Grid> </Grid>
</Grid> </Grid>
</form> </form>
<ErrorSnack message={formik.errors.detail} />
<SuccessSnack message={data?.detail} />
</>
) )
} }

View File

@ -20,7 +20,7 @@ export default function parseFastAPIError(
if (typeof error === "undefined") { if (typeof error === "undefined") {
return { return {
detail: "There was an error", detail: undefined,
} }
} }