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 />,
children: [
{
loader: getServerSettings,
path: "/aliases",
element: <AliasesRoute />,
},

View File

@ -1 +1,2 @@
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"
import {useMutation} from "@tanstack/react-query"
import {CreateAliasData, createAlias} from "~/apis"
import {createAlias} from "~/apis"
import {Alias, AliasType} from "~/server-types"
export interface CreateRandomAliasButtonProps {
onCreated: (alias: Alias) => void
export interface CreateAliasButtonProps {
onRandomCreated: (alias: Alias) => void
onCustomCreated: () => void
}
export default function CreateRandomAliasButton({
onCreated,
}: CreateRandomAliasButtonProps): ReactElement {
const {mutate, isLoading} = useMutation<Alias, AxiosError, CreateAliasData>(
createAlias,
export default function CreateAliasButton({
onRandomCreated,
onCustomCreated,
}: CreateAliasButtonProps): ReactElement {
const {mutate, isLoading} = useMutation<Alias, AxiosError, void>(
() =>
createAlias({
type: AliasType.RANDOM,
}),
{
onSuccess: onCreated,
onSuccess: onRandomCreated,
},
)
@ -41,11 +46,7 @@ export default function CreateRandomAliasButton({
<Button
disabled={isLoading}
startIcon={<BsArrowClockwise />}
onClick={() =>
mutate({
type: AliasType.RANDOM,
})
}
onClick={() => mutate()}
>
Create random alias
</Button>
@ -62,7 +63,12 @@ export default function CreateRandomAliasButton({
onClose={() => setAnchorElement(null)}
>
<MenuList>
<MenuItem>
<MenuItem
onClick={() => {
setAnchorElement(null)
onCustomCreated()
}}
>
<ListItemIcon>
<FaPen />
</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
name="code"
id="code"
label="code"
label="Code"
value={formik.values.code}
onChange={formik.handleChange}
disabled={formik.isSubmitting}

View File

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

View File

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