mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-20 00:05:26 +02:00
improved snacks
This commit is contained in:
parent
8616d4085a
commit
2cb17e5550
@ -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",
|
||||
|
13
src/App.tsx
13
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 {
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<AuthContextProvider>
|
||||
<CssBaseline />
|
||||
<RouterProvider router={router} />
|
||||
</AuthContextProvider>
|
||||
<SnackbarProvider>
|
||||
<AuthContextProvider>
|
||||
<CssBaseline />
|
||||
<RouterProvider router={router} />
|
||||
</AuthContextProvider>
|
||||
</SnackbarProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
48
src/hooks/use-error-success-snacks.ts
Normal file
48
src/hooks/use-error-success-snacks.ts
Normal 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,
|
||||
}
|
||||
}
|
@ -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<Alias | DecryptedAlias>(aliasValue)
|
||||
const [hasCopiedToClipboard, setHasCopiedToClipboard] = useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<SimplePageBuilder.MultipleSections>
|
||||
{[
|
||||
<Grid key="basic" container spacing={1} direction="row" alignItems="center">
|
||||
<Grid item>
|
||||
<AliasTypeIndicator type={aliasUIState.type} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
endIcon={<MdContentCopy />}
|
||||
variant="text"
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
copy(address)
|
||||
setHasCopiedToClipboard(true)
|
||||
}}
|
||||
sx={{textTransform: "none", fontWeight: "normal"}}
|
||||
>
|
||||
{address}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ChangeAliasActivationStatusSwitch
|
||||
id={aliasUIState.id}
|
||||
isActive={aliasUIState.isActive}
|
||||
onChanged={setAliasUIState}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>,
|
||||
<div key="notes">
|
||||
{encryptionStatus === EncryptionStatus.Available ? (
|
||||
<AliasNotesForm
|
||||
id={aliasUIState.id}
|
||||
notes={(aliasUIState as DecryptedAlias).notes}
|
||||
onChanged={setAliasUIState}
|
||||
/>
|
||||
) : (
|
||||
<DecryptionPasswordMissingAlert />
|
||||
)}
|
||||
</div>,
|
||||
<SimplePageBuilder.Section
|
||||
label={t("routes.AliasDetailRoute.sections.settings.title")}
|
||||
key="settings"
|
||||
>
|
||||
<AliasPreferencesForm alias={aliasUIState} onChanged={setAliasUIState} />
|
||||
</SimplePageBuilder.Section>,
|
||||
]}
|
||||
</SimplePageBuilder.MultipleSections>
|
||||
<SuccessSnack
|
||||
onClose={() => setHasCopiedToClipboard(false)}
|
||||
message={
|
||||
hasCopiedToClipboard &&
|
||||
t("relations.alias.mutations.success.addressCopiedToClipboard")
|
||||
}
|
||||
/>
|
||||
</>
|
||||
<SimplePageBuilder.MultipleSections>
|
||||
{[
|
||||
<Grid key="basic" container spacing={1} direction="row" alignItems="center">
|
||||
<Grid item>
|
||||
<AliasTypeIndicator type={aliasUIState.type} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
endIcon={<MdContentCopy />}
|
||||
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}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ChangeAliasActivationStatusSwitch
|
||||
id={aliasUIState.id}
|
||||
isActive={aliasUIState.isActive}
|
||||
onChanged={setAliasUIState}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>,
|
||||
<div key="notes">
|
||||
{encryptionStatus === EncryptionStatus.Available ? (
|
||||
<AliasNotesForm
|
||||
id={aliasUIState.id}
|
||||
notes={(aliasUIState as DecryptedAlias).notes}
|
||||
onChanged={setAliasUIState}
|
||||
/>
|
||||
) : (
|
||||
<DecryptionPasswordMissingAlert />
|
||||
)}
|
||||
</div>,
|
||||
<SimplePageBuilder.Section
|
||||
label={t("routes.AliasDetailRoute.sections.settings.title")}
|
||||
key="settings"
|
||||
>
|
||||
<AliasPreferencesForm alias={aliasUIState} onChanged={setAliasUIState} />
|
||||
</SimplePageBuilder.Section>,
|
||||
]}
|
||||
</SimplePageBuilder.MultipleSections>
|
||||
)
|
||||
}
|
||||
|
@ -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<Alias, AxiosError, UpdateAliasData>(
|
||||
const {mutateAsync} = useMutation<Alias, AxiosError, UpdateAliasData>(
|
||||
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
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
<ErrorSnack message={formik.errors.detail} />
|
||||
<SuccessSnack
|
||||
message={isSuccess && t("relations.alias.mutations.success.notesUpdated")}
|
||||
/>
|
||||
<FormikAutoLockNavigation formik={formik} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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<boolean | null>()
|
||||
@ -64,10 +68,19 @@ export default function AliasPreferencesForm({
|
||||
.oneOf([null, ...Object.values(ProxyUserAgentType)])
|
||||
.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),
|
||||
{
|
||||
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>({
|
||||
@ -186,10 +199,6 @@ export default function AliasPreferencesForm({
|
||||
</Grid>
|
||||
</form>
|
||||
<FormikAutoLockNavigation formik={formik} />
|
||||
<ErrorSnack message={formik.errors.detail} />
|
||||
<SuccessSnack
|
||||
message={isSuccess && t("relations.alias.mutations.success.aliasUpdated")}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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<boolean>(isActive)
|
||||
|
||||
const [successMessage, setSuccessMessage] = useState<string>("")
|
||||
const [errorMessage, setErrorMessage] = useState<string>("")
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Switch
|
||||
checked={isActiveUIState}
|
||||
disabled={isActiveUIState === null || isLoading}
|
||||
onChange={async () => {
|
||||
setIsActiveUIState(!isActiveUIState)
|
||||
<Switch
|
||||
checked={isActiveUIState}
|
||||
disabled={isActiveUIState === null || isLoading}
|
||||
onChange={async () => {
|
||||
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 {}
|
||||
}}
|
||||
/>
|
||||
<SuccessSnack message={successMessage} />
|
||||
<ErrorSnack message={errorMessage} />
|
||||
</>
|
||||
showSuccess(
|
||||
isActiveUIState
|
||||
? t("relations.alias.mutations.success.aliasChangedToDisabled")
|
||||
: t("relations.alias.mutations.success.aliasChangedToEnabled"),
|
||||
)
|
||||
} catch {}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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<AliasList[]>(aliases)
|
||||
const [hasCopiedToClipboard, setHasCopiedToClipboard] = useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container spacing={4} direction="column">
|
||||
<Grid item>
|
||||
<List>
|
||||
{aliasesUIState.map(alias => (
|
||||
<ListItemButton
|
||||
key={alias.id}
|
||||
onClick={event => {
|
||||
if (isInCopyAddressMode) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
copy(getAddress(alias))
|
||||
setHasCopiedToClipboard(true)
|
||||
}
|
||||
}}
|
||||
href={`/aliases/${btoa(getAddress(alias))}`}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AliasTypeIndicator type={alias.type} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={getAddress(alias)} />
|
||||
{isInCopyAddressMode && (
|
||||
<ListItemSecondaryAction>
|
||||
<MdContentCopy />
|
||||
</ListItemSecondaryAction>
|
||||
)}
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<CreateAliasButton
|
||||
onCreated={alias =>
|
||||
setAliasesUIState(currentAliases => [alias, ...currentAliases])
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container spacing={4} direction="column">
|
||||
<Grid item>
|
||||
<List>
|
||||
{aliasesUIState.map(alias => (
|
||||
<ListItemButton
|
||||
key={alias.id}
|
||||
onClick={event => {
|
||||
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))}`}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AliasTypeIndicator type={alias.type} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={getAddress(alias)} />
|
||||
{isInCopyAddressMode && (
|
||||
<ListItemSecondaryAction>
|
||||
<MdContentCopy />
|
||||
</ListItemSecondaryAction>
|
||||
)}
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
<SuccessSnack
|
||||
onClose={() => setHasCopiedToClipboard(false)}
|
||||
message={
|
||||
hasCopiedToClipboard &&
|
||||
t("relations.alias.mutations.success.addressCopiedToClipboard")
|
||||
}
|
||||
/>
|
||||
</>
|
||||
<Grid item>
|
||||
<CreateAliasButton
|
||||
onCreated={alias =>
|
||||
setAliasesUIState(currentAliases => [alias, ...currentAliases])
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
@ -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<string>("")
|
||||
const $errorSnackbarId = useRef<SnackbarKey | null>(null)
|
||||
|
||||
const {mutateAsync, isLoading, isSuccess} = useMutation<
|
||||
Alias,
|
||||
AxiosError,
|
||||
CreateAliasData
|
||||
>(
|
||||
const {mutateAsync, isLoading, isSuccess} = useMutation<Alias, AxiosError, CreateAliasData>(
|
||||
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<boolean>(false)
|
||||
const [showCustomCreateDialog, setShowCustomCreateDialog] = useState<boolean>(false)
|
||||
|
||||
const [anchorElement, setAnchorElement] = useState<HTMLElement | null>(null)
|
||||
const open = Boolean(anchorElement)
|
||||
@ -87,18 +82,11 @@ export default function CreateAliasButton({
|
||||
>
|
||||
{t("routes.AliasesRoute.actions.createRandomAlias.label")}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={event => setAnchorElement(event.currentTarget)}
|
||||
>
|
||||
<Button size="small" onClick={event => setAnchorElement(event.currentTarget)}>
|
||||
<MdArrowDropDown />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Menu
|
||||
anchorEl={anchorElement}
|
||||
open={open}
|
||||
onClose={() => setAnchorElement(null)}
|
||||
>
|
||||
<Menu anchorEl={anchorElement} open={open} onClose={() => setAnchorElement(null)}>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
disabled={isLoading}
|
||||
@ -111,9 +99,7 @@ export default function CreateAliasButton({
|
||||
<FaPen />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t(
|
||||
"routes.AliasesRoute.actions.createCustomAlias.label",
|
||||
)}
|
||||
primary={t("routes.AliasesRoute.actions.createCustomAlias.label")}
|
||||
/>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
@ -127,13 +113,6 @@ export default function CreateAliasButton({
|
||||
}}
|
||||
onClose={() => setShowCustomCreateDialog(false)}
|
||||
/>
|
||||
<ErrorSnack message={errorMessage} />
|
||||
<SuccessSnack
|
||||
message={
|
||||
isSuccess &&
|
||||
t("relations.alias.mutations.success.aliasCreation")
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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<Form>({
|
||||
validationSchema: SCHEMA,
|
||||
@ -108,227 +112,220 @@ export default function AliasesPreferencesForm(): ReactElement {
|
||||
const isLarge = useMediaQuery(theme.breakpoints.up("md"))
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Grid container spacing={4} flexDirection="column" alignItems="center">
|
||||
<Grid item>
|
||||
<Typography variant="h6" component="h3">
|
||||
{t("routes.SettingsRoute.forms.aliasPreferences.title")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="body1" component="p">
|
||||
{t("routes.SettingsRoute.forms.aliasPreferences.description")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
container
|
||||
spacing={4}
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Grid item md={6} xs={12}>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
disabled={formik.isSubmitting}
|
||||
control={
|
||||
<Checkbox
|
||||
name="removeTrackers"
|
||||
id="removeTrackers"
|
||||
checked={formik.values.removeTrackers}
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Grid container spacing={4} flexDirection="column" alignItems="center">
|
||||
<Grid item>
|
||||
<Typography variant="h6" component="h3">
|
||||
{t("routes.SettingsRoute.forms.aliasPreferences.title")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="body1" component="p">
|
||||
{t("routes.SettingsRoute.forms.aliasPreferences.description")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
container
|
||||
spacing={4}
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Grid item md={6} xs={12}>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
disabled={formik.isSubmitting}
|
||||
control={
|
||||
<Checkbox
|
||||
name="removeTrackers"
|
||||
id="removeTrackers"
|
||||
checked={formik.values.removeTrackers}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
}
|
||||
labelPlacement="start"
|
||||
label="Remove Trackers"
|
||||
/>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport) ||
|
||||
t("relations.alias.settings.removeTrackers.helperText")}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item md={6} xs={12}>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
disabled={formik.isSubmitting}
|
||||
control={
|
||||
<Checkbox
|
||||
name="createMailReport"
|
||||
id="createMailReport"
|
||||
checked={formik.values.createMailReport}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
}
|
||||
labelPlacement="start"
|
||||
label="Create Reports"
|
||||
/>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport) ||
|
||||
t("relations.alias.settings.createMailReports.helperText")}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
disabled={formik.isSubmitting}
|
||||
control={
|
||||
<Checkbox
|
||||
name="proxyImages"
|
||||
id="proxyImages"
|
||||
checked={formik.values.proxyImages}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
}
|
||||
labelPlacement="start"
|
||||
label="Proxy Images"
|
||||
/>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.proxyImages && formik.errors.proxyImages,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.proxyImages && formik.errors.proxyImages) ||
|
||||
t("relations.alias.settings.proxyImages.helperText")}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
<Collapse in={formik.values.proxyImages}>
|
||||
<Grid
|
||||
display="flex"
|
||||
flexDirection={isLarge ? "row" : "column"}
|
||||
container
|
||||
marginY={2}
|
||||
spacing={4}
|
||||
alignItems={isLarge ? "flex-start" : "flex-end"}
|
||||
>
|
||||
<Grid item md={6} xs={12}>
|
||||
<FormGroup>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<MdImage />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
name="imageProxyFormat"
|
||||
id="imageProxyFormat"
|
||||
label="Image File Type"
|
||||
value={formik.values.imageProxyFormat}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
}
|
||||
labelPlacement="start"
|
||||
label="Remove Trackers"
|
||||
/>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport) ||
|
||||
t("relations.alias.settings.removeTrackers.helperText")}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item md={6} xs={12}>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
disabled={formik.isSubmitting}
|
||||
control={
|
||||
<Checkbox
|
||||
name="createMailReport"
|
||||
id="createMailReport"
|
||||
checked={formik.values.createMailReport}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
}
|
||||
labelPlacement="start"
|
||||
label="Create Reports"
|
||||
/>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport) ||
|
||||
t(
|
||||
"relations.alias.settings.createMailReports.helperText",
|
||||
)}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
disabled={formik.isSubmitting}
|
||||
control={
|
||||
<Checkbox
|
||||
name="proxyImages"
|
||||
id="proxyImages"
|
||||
checked={formik.values.proxyImages}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
}
|
||||
labelPlacement="start"
|
||||
label="Proxy Images"
|
||||
/>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.proxyImages && formik.errors.proxyImages,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.proxyImages &&
|
||||
formik.errors.proxyImages) ||
|
||||
t("relations.alias.settings.proxyImages.helperText")}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
<Collapse in={formik.values.proxyImages}>
|
||||
<Grid
|
||||
display="flex"
|
||||
flexDirection={isLarge ? "row" : "column"}
|
||||
container
|
||||
marginY={2}
|
||||
spacing={4}
|
||||
alignItems={isLarge ? "flex-start" : "flex-end"}
|
||||
>
|
||||
<Grid item md={6} xs={12}>
|
||||
<FormGroup>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<MdImage />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
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]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{t(translationString)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.imageProxyFormat &&
|
||||
formik.errors.imageProxyFormat,
|
||||
)}
|
||||
>
|
||||
{formik.touched.imageProxyFormat &&
|
||||
formik.errors.imageProxyFormat}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item md={6} xs={12}>
|
||||
<FormGroup>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
name="imageProxyUserAgent"
|
||||
id="imageProxyUserAgent"
|
||||
label="Image Proxy User Agent"
|
||||
value={formik.values.imageProxyUserAgent}
|
||||
onChange={formik.handleChange}
|
||||
disabled={formik.isSubmitting}
|
||||
error={
|
||||
formik.touched.imageProxyUserAgent &&
|
||||
Boolean(formik.errors.imageProxyUserAgent)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.imageProxyUserAgent &&
|
||||
formik.errors.imageProxyUserAgent
|
||||
}
|
||||
>
|
||||
{Object.entries(
|
||||
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP,
|
||||
).map(([value, translationString]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{t(translationString)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.imageProxyUserAgent &&
|
||||
formik.errors.imageProxyUserAgent,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.imageProxyUserAgent &&
|
||||
formik.errors.imageProxyUserAgent) ||
|
||||
t(
|
||||
"relations.alias.settings.imageProxyUserAgent.helperText",
|
||||
)}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
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]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{t(translationString)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.imageProxyFormat &&
|
||||
formik.errors.imageProxyFormat,
|
||||
)}
|
||||
>
|
||||
{formik.touched.imageProxyFormat &&
|
||||
formik.errors.imageProxyFormat}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
<Grid item md={6} xs={12}>
|
||||
<FormGroup>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
name="imageProxyUserAgent"
|
||||
id="imageProxyUserAgent"
|
||||
label="Image Proxy User Agent"
|
||||
value={formik.values.imageProxyUserAgent}
|
||||
onChange={formik.handleChange}
|
||||
disabled={formik.isSubmitting}
|
||||
error={
|
||||
formik.touched.imageProxyUserAgent &&
|
||||
Boolean(formik.errors.imageProxyUserAgent)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.imageProxyUserAgent &&
|
||||
formik.errors.imageProxyUserAgent
|
||||
}
|
||||
>
|
||||
{Object.entries(
|
||||
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP,
|
||||
).map(([value, translationString]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{t(translationString)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.imageProxyUserAgent &&
|
||||
formik.errors.imageProxyUserAgent,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.imageProxyUserAgent &&
|
||||
formik.errors.imageProxyUserAgent) ||
|
||||
t(
|
||||
"relations.alias.settings.imageProxyUserAgent.helperText",
|
||||
)}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<LoadingButton
|
||||
loading={formik.isSubmitting}
|
||||
variant="contained"
|
||||
type="submit"
|
||||
startIcon={<MdCheckCircle />}
|
||||
>
|
||||
Save Preferences
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
<ErrorSnack message={formik.errors.detail} />
|
||||
<SuccessSnack message={data?.detail} />
|
||||
</>
|
||||
<Grid item>
|
||||
<LoadingButton
|
||||
loading={formik.isSubmitting}
|
||||
variant="contained"
|
||||
type="submit"
|
||||
startIcon={<MdCheckCircle />}
|
||||
>
|
||||
Save Preferences
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ export default function parseFastAPIError(
|
||||
|
||||
if (typeof error === "undefined") {
|
||||
return {
|
||||
detail: "There was an error",
|
||||
detail: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user