improvements; added CustomAliasDialog.tsx

This commit is contained in:
Myzel394 2022-10-29 17:26:56 +02:00
parent ca9eb78330
commit 3c9918e8b7
9 changed files with 254 additions and 79 deletions

View File

@ -58,6 +58,7 @@ const router = createBrowserRouter([
element: <AuthenticatedRoute />, element: <AuthenticatedRoute />,
children: [ children: [
{ {
loader: getServerSettings,
path: "/aliases", path: "/aliases",
element: <AliasesRoute />, element: <AliasesRoute />,
}, },

View File

@ -1 +1,2 @@
export const MASTER_PASSWORD_LENGTH = 4096 export const MASTER_PASSWORD_LENGTH = 4096
export const LOCAL_REGEX = /^[a-zA-Z0-9!#$%&*+/=?^_`.{|}~-]{1,64}$/g

View File

@ -1,32 +0,0 @@
import {ReactElement} from "react"
import {MdOutlineMoreVert} from "react-icons/md"
import {
IconButton,
ListItemButton,
ListItemSecondaryAction,
ListItemText,
} from "@mui/material"
import {AliasList} from "~/server-types"
export interface AliasListItemProps {
alias: AliasList
}
export default function AliasListItem({
alias,
}: AliasListItemProps): ReactElement {
const address = `${alias.local}@${alias.domain}`
return (
<ListItemButton href={`/aliases/${btoa(address)}`}>
<ListItemText primary={address} />
<ListItemSecondaryAction>
<IconButton edge="end">
<MdOutlineMoreVert />
</IconButton>
</ListItemSecondaryAction>
</ListItemButton>
)
}

View File

@ -0,0 +1,28 @@
import {ReactElement} from "react"
import {FaHashtag, FaRandom} from "react-icons/fa"
import {ListItemButton, ListItemIcon, ListItemText} from "@mui/material"
import {AliasList, AliasType} from "~/server-types"
export interface AliasListItemProps {
alias: AliasList
}
const ALIAS_TYPE_ICON_MAP: Record<AliasType, ReactElement> = {
[AliasType.RANDOM]: <FaRandom />,
[AliasType.CUSTOM]: <FaHashtag />,
}
export default function AliasListItem({
alias,
}: AliasListItemProps): ReactElement {
const address = `${alias.local}@${alias.domain}`
return (
<ListItemButton href={`/aliases/${btoa(address)}`}>
<ListItemIcon>{ALIAS_TYPE_ICON_MAP[alias.type]}</ListItemIcon>
<ListItemText primary={address} />
</ListItemButton>
)
}

View File

@ -15,20 +15,25 @@ import {
} from "@mui/material" } from "@mui/material"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {CreateAliasData, createAlias} from "~/apis" import {createAlias} from "~/apis"
import {Alias, AliasType} from "~/server-types" import {Alias, AliasType} from "~/server-types"
export interface CreateRandomAliasButtonProps { export interface CreateAliasButtonProps {
onCreated: (alias: Alias) => void onRandomCreated: (alias: Alias) => void
onCustomCreated: () => void
} }
export default function CreateRandomAliasButton({ export default function CreateAliasButton({
onCreated, onRandomCreated,
}: CreateRandomAliasButtonProps): ReactElement { onCustomCreated,
const {mutate, isLoading} = useMutation<Alias, AxiosError, CreateAliasData>( }: CreateAliasButtonProps): ReactElement {
createAlias, const {mutate, isLoading} = useMutation<Alias, AxiosError, void>(
() =>
createAlias({
type: AliasType.RANDOM,
}),
{ {
onSuccess: onCreated, onSuccess: onRandomCreated,
}, },
) )
@ -41,11 +46,7 @@ export default function CreateRandomAliasButton({
<Button <Button
disabled={isLoading} disabled={isLoading}
startIcon={<BsArrowClockwise />} startIcon={<BsArrowClockwise />}
onClick={() => onClick={() => mutate()}
mutate({
type: AliasType.RANDOM,
})
}
> >
Create random alias Create random alias
</Button> </Button>
@ -62,7 +63,12 @@ export default function CreateRandomAliasButton({
onClose={() => setAnchorElement(null)} onClose={() => setAnchorElement(null)}
> >
<MenuList> <MenuList>
<MenuItem> <MenuItem
onClick={() => {
setAnchorElement(null)
onCustomCreated()
}}
>
<ListItemIcon> <ListItemIcon>
<FaPen /> <FaPen />
</ListItemIcon> </ListItemIcon>

View File

@ -0,0 +1,164 @@
import * as yup from "yup"
import {ReactElement} from "react"
import {useFormik} from "formik"
import {useLoaderData} from "react-router-dom"
import {AxiosError} from "axios"
import {TiCancel} from "react-icons/ti"
import {FaPen} from "react-icons/fa"
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
InputAdornment,
TextField,
Typography,
} from "@mui/material"
import {useMutation} from "@tanstack/react-query"
import {Alias, AliasType, ServerSettings} from "~/server-types"
import {CreateAliasData, createAlias} from "~/apis"
import {parseFastAPIError} from "~/utils"
import {LOCAL_REGEX} from "~/constants/values"
import {ErrorSnack, SuccessSnack} from "~/components"
export interface CustomAliasDialogProps {
visible: boolean
onCreated: () => void
onClose: () => void
}
interface Form {
local: string
detail?: string
}
export default function CustomAliasDialog({
visible,
onCreated,
onClose,
}: CustomAliasDialogProps): ReactElement {
const serverSettings = useLoaderData() as ServerSettings
const schema = yup.object().shape({
local: yup
.string()
.matches(LOCAL_REGEX)
.required()
.min(1)
.max(64 - serverSettings.customAliasSuffixLength - 1),
})
const {mutateAsync, isLoading, isSuccess, reset} = useMutation<
Alias,
AxiosError,
Omit<CreateAliasData, "type">
>(
values =>
// @ts-ignore
createAlias({
type: AliasType.CUSTOM,
...values,
}),
{
onSuccess: () => {
reset()
onCreated()
},
},
)
const formik = useFormik<Form>({
validationSchema: schema,
initialValues: {
local: "",
},
onSubmit: async (values, {setErrors}) => {
try {
await mutateAsync({
local: values.local,
})
} catch (error) {
setErrors(parseFastAPIError(error as AxiosError))
}
},
})
return (
<>
<Dialog onClose={onClose} open={visible} keepMounted={false}>
<form onSubmit={formik.handleSubmit}>
<DialogTitle>Create Custom Alias</DialogTitle>
<DialogContent>
<DialogContentText>
You can define your own custom alias. Note that a
random suffix will be added at the end to avoid
duplicates.
</DialogContentText>
<Box paddingY={4}>
<TextField
key="local"
fullWidth
autoFocus
name="local"
id="local"
label="Address"
value={formik.values.local}
onChange={formik.handleChange}
disabled={formik.isSubmitting}
error={
formik.touched.local &&
Boolean(formik.errors.local)
}
helperText={
formik.touched.local && formik.errors.local
}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography variant="body2">
<span>
{Array(
serverSettings.customAliasSuffixLength,
)
.fill("#")
.join("")}
</span>
<span>
@{serverSettings.mailDomain}
</span>
</Typography>
</InputAdornment>
),
}}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} startIcon={<TiCancel />}>
Cancel
</Button>
<Button
onClick={() => {}}
disabled={isLoading}
startIcon={<FaPen />}
variant="contained"
type="submit"
>
Create Alias
</Button>
</DialogActions>
</form>
</Dialog>
<ErrorSnack message={formik.errors.detail} />
<SuccessSnack
message={isSuccess && "Created Alias successfully!"}
/>
</>
)
}

View File

@ -95,7 +95,7 @@ export default function ConfirmCodeForm({
fullWidth fullWidth
name="code" name="code"
id="code" id="code"
label="code" label="Code"
value={formik.values.code} value={formik.values.code}
onChange={formik.handleChange} onChange={formik.handleChange}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}

View File

@ -1,12 +1,13 @@
import {ReactElement} from "react" import {ReactElement, useState} from "react"
import {AxiosError} from "axios" import {AxiosError} from "axios"
import {Grid, List, Typography} from "@mui/material" import {List} from "@mui/material"
import {useQuery} from "@tanstack/react-query" import {useQuery} from "@tanstack/react-query"
import {AliasList, PaginationResult} from "~/server-types" import {AliasList, PaginationResult} from "~/server-types"
import AliasListItem from "~/route-widgets/AliasRoute/AliasListItem" import AliasListItem from "~/route-widgets/AliasesRoute/AliasListItem"
import CreateRandomAliasButton from "~/route-widgets/AliasRoute/CreateRandomAliasButton" import CreateAliasButton from "~/route-widgets/AliasesRoute/CreateAliasButton"
import CustomAliasDialog from "~/route-widgets/AliasesRoute/CustomAliasDialog"
import QueryResult from "~/components/QueryResult" import QueryResult from "~/components/QueryResult"
import SimplePage from "~/components/SimplePage" import SimplePage from "~/components/SimplePage"
import getAliases from "~/apis/get-aliases" import getAliases from "~/apis/get-aliases"
@ -17,34 +18,38 @@ export default function AliasesRoute(): ReactElement {
getAliases, getAliases,
) )
const [showCustomCreateDialog, setShowCustomCreateDialog] =
useState<boolean>(false)
return ( return (
<SimplePage title="Aliases"> <>
<Grid container spacing={4} direction="column" alignItems="stretch"> <SimplePage
<Grid item> title="Aliases"
<Typography variant="h6" component="h2"> actions={
Random Aliases <CreateAliasButton
</Typography> onRandomCreated={() => query.refetch()}
</Grid> onCustomCreated={() => setShowCustomCreateDialog(true)}
<Grid item> />
}
>
<QueryResult<PaginationResult<AliasList>> query={query}> <QueryResult<PaginationResult<AliasList>> query={query}>
{result => ( {result => (
<List> <List>
{result.items.map(alias => ( {result.items.map(alias => (
<AliasListItem <AliasListItem key={alias.id} alias={alias} />
key={alias.id}
alias={alias}
/>
))} ))}
</List> </List>
)} )}
</QueryResult> </QueryResult>
</Grid>
<Grid item>
<CreateRandomAliasButton
onCreated={() => query.refetch()}
/>
</Grid>
</Grid>
</SimplePage> </SimplePage>
<CustomAliasDialog
visible={showCustomCreateDialog}
onCreated={() => {
setShowCustomCreateDialog(false)
query.refetch()
}}
onClose={() => setShowCustomCreateDialog(false)}
/>
</>
) )
} }

View File

@ -74,6 +74,7 @@ export interface ServerSettings {
emailLoginTokenChars: string emailLoginTokenChars: string
emailLoginTokenLength: number emailLoginTokenLength: number
emailResendWaitTime: number emailResendWaitTime: number
customAliasSuffixLength: number
} }
export interface Alias { export interface Alias {
@ -95,6 +96,7 @@ export interface AliasList {
domain: string domain: string
local: string local: string
isActive: boolean isActive: boolean
type: AliasType
} }
export interface Report { export interface Report {