mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 15:55:26 +02:00
added ReservedAliasDetailRoute.tsx
This commit is contained in:
parent
dac14af539
commit
90dd7ca180
@ -296,6 +296,20 @@
|
||||
"userAmount_one": "Forwards to one user",
|
||||
"userAmount_other": "Forwards to {{count}} users"
|
||||
},
|
||||
"ReservedAliasDetailRoute": {
|
||||
"title": "Reserved Alias Details",
|
||||
"sections": {
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"fields": {
|
||||
"users": {
|
||||
"label": "Users",
|
||||
"me": "{{email}} (Me)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AdminRoute": {
|
||||
"title": "Site configuration",
|
||||
"forms": {
|
||||
|
@ -21,6 +21,7 @@ import LogoutRoute from "~/routes/LogoutRoute"
|
||||
import OverviewRoute from "~/routes/OverviewRoute"
|
||||
import ReportDetailRoute from "~/routes/ReportDetailRoute"
|
||||
import ReportsRoute from "~/routes/ReportsRoute"
|
||||
import ReservedAliasDetailRoute from "~/routes/ReservedAliasDetailRoute"
|
||||
import ReservedAliasesRoute from "~/routes/ReservedAliasesRoute"
|
||||
import RootRoute from "~/routes/Root"
|
||||
import SettingsRoute from "~/routes/SettingsRoute"
|
||||
@ -104,6 +105,10 @@ const router = createBrowserRouter([
|
||||
path: "/admin/reserved-aliases",
|
||||
element: <ReservedAliasesRoute />,
|
||||
},
|
||||
{
|
||||
path: "/admin/reserved-aliases/:id",
|
||||
element: <ReservedAliasDetailRoute />,
|
||||
},
|
||||
{
|
||||
path: "/admin/reserved-aliases/create",
|
||||
loader: getServerSettings,
|
||||
|
13
src/apis/get-reserved-alias.ts
Normal file
13
src/apis/get-reserved-alias.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {ReservedAlias} from "~/server-types"
|
||||
import {client} from "~/constants/axios-client"
|
||||
|
||||
export default async function getReservedAlias(id: string): Promise<ReservedAlias> {
|
||||
const {data} = await client.get(
|
||||
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/reserved-alias/${id}`,
|
||||
{
|
||||
withCredentials: true,
|
||||
},
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
@ -44,3 +44,7 @@ export * from "./get-reserved-aliases"
|
||||
export {default as getReservedAliases} from "./get-reserved-aliases"
|
||||
export * from "./create-reserved-alias"
|
||||
export {default as createReservedAlias} from "./create-reserved-alias"
|
||||
export * from "./get-reserved-alias"
|
||||
export {default as getReservedAlias} from "./get-reserved-alias"
|
||||
export * from "./update-reserved-alias"
|
||||
export {default as updateReservedAlias} from "./update-reserved-alias"
|
||||
|
27
src/apis/update-reserved-alias.ts
Normal file
27
src/apis/update-reserved-alias.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {ReservedAlias} from "~/server-types"
|
||||
import {client} from "~/constants/axios-client"
|
||||
|
||||
export interface UpdateReservedAliasData {
|
||||
isActive?: boolean
|
||||
users?: Array<{
|
||||
id: string
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function updateReservedAlias(
|
||||
id: string,
|
||||
{isActive, users}: UpdateReservedAliasData,
|
||||
): Promise<ReservedAlias> {
|
||||
const {data} = await client.patch(
|
||||
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/reserved-alias/${id}`,
|
||||
{
|
||||
isActive,
|
||||
users,
|
||||
},
|
||||
{
|
||||
withCredentials: true,
|
||||
},
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import {AxiosError} from "axios"
|
||||
import {useTranslation} from "react-i18next"
|
||||
import {ReactElement} from "react"
|
||||
|
||||
import {useQuery} from "@tanstack/react-query"
|
||||
import {MenuItem, TextField} from "@mui/material"
|
||||
|
||||
import {GetAdminUsersResponse, getAdminUsers} from "~/apis"
|
||||
import {useUser} from "~/hooks"
|
||||
|
||||
export interface AdminUserPickerProps {
|
||||
onPick: (user: GetAdminUsersResponse["users"][0]) => void
|
||||
alreadyPicked: GetAdminUsersResponse["users"]
|
||||
}
|
||||
|
||||
export default function AdminUserPicker({
|
||||
onPick,
|
||||
alreadyPicked,
|
||||
}: AdminUserPickerProps): ReactElement {
|
||||
const {t} = useTranslation()
|
||||
const meUser = useUser()
|
||||
const {data: {users: availableUsers} = {}} = useQuery<GetAdminUsersResponse, AxiosError>(
|
||||
["getAdminUsers"],
|
||||
getAdminUsers,
|
||||
)
|
||||
|
||||
if (!availableUsers) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const users = availableUsers.filter(
|
||||
user => !alreadyPicked.find(picked => picked.id === user.id),
|
||||
)
|
||||
|
||||
if (users.length === 0) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
value={null}
|
||||
label="Admin User"
|
||||
onChange={event => {
|
||||
const user = users.find(user => user.id === event.target.value)
|
||||
if (user) {
|
||||
onPick(user)
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{users.map(user => (
|
||||
<MenuItem key={user.id} value={user.id}>
|
||||
{user.id === meUser?.id
|
||||
? t("routes.AdminRoute.forms.reservedAliases.fields.users.me", {
|
||||
email: user.email.address,
|
||||
})
|
||||
: user.email.address}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
)
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import {ReactElement} from "react"
|
||||
import {AxiosError} from "axios"
|
||||
import {useTranslation} from "react-i18next"
|
||||
import update from "immutability-helper"
|
||||
|
||||
import {useMutation} from "@tanstack/react-query"
|
||||
import {Switch} from "@mui/material"
|
||||
|
||||
import {useErrorSuccessSnacks} from "~/hooks"
|
||||
import {ReservedAlias} from "~/server-types"
|
||||
import {updateReservedAlias} from "~/apis"
|
||||
import {queryClient} from "~/constants/react-query"
|
||||
|
||||
export interface AliasActivationSwitch {
|
||||
id: string
|
||||
isActive: boolean
|
||||
queryKey: readonly string[]
|
||||
}
|
||||
|
||||
export default function AliasActivationSwitch({
|
||||
id,
|
||||
isActive,
|
||||
queryKey,
|
||||
}: AliasActivationSwitch): ReactElement {
|
||||
const {t} = useTranslation()
|
||||
const {showError, showSuccess} = useErrorSuccessSnacks()
|
||||
const {isLoading, mutateAsync} = useMutation<
|
||||
ReservedAlias,
|
||||
AxiosError,
|
||||
boolean,
|
||||
{previousAlias: ReservedAlias | undefined}
|
||||
>(
|
||||
activeNow =>
|
||||
updateReservedAlias(id, {
|
||||
isActive: activeNow,
|
||||
}),
|
||||
{
|
||||
onMutate: async activeNow => {
|
||||
await queryClient.cancelQueries(queryKey)
|
||||
|
||||
const previousAlias = queryClient.getQueryData<ReservedAlias>(queryKey)
|
||||
|
||||
queryClient.setQueryData<ReservedAlias>(queryKey, old =>
|
||||
update(old, {
|
||||
isActive: {
|
||||
$set: activeNow!,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
return {previousAlias}
|
||||
},
|
||||
onSuccess: newAlias => {
|
||||
queryClient.setQueryData<ReservedAlias>(queryKey, newAlias)
|
||||
},
|
||||
onError: (error, values, context) => {
|
||||
showError(error)
|
||||
|
||||
if (context?.previousAlias) {
|
||||
queryClient.setQueryData<ReservedAlias>(queryKey, context.previousAlias)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={isActive}
|
||||
onChange={async () => {
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await mutateAsync(!isActive)
|
||||
|
||||
showSuccess(
|
||||
isActive
|
||||
? t("relations.alias.mutations.success.aliasChangedToDisabled")
|
||||
: t("relations.alias.mutations.success.aliasChangedToEnabled"),
|
||||
)
|
||||
} catch {}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
205
src/route-widgets/ReservedAliasDetailRoute/AliasUsersList.tsx
Normal file
205
src/route-widgets/ReservedAliasDetailRoute/AliasUsersList.tsx
Normal file
@ -0,0 +1,205 @@
|
||||
import * as yup from "yup"
|
||||
import {ReactElement, useState} from "react"
|
||||
import {AxiosError} from "axios"
|
||||
import {MdCheckCircle} from "react-icons/md"
|
||||
import {FaPen} from "react-icons/fa"
|
||||
import {useTranslation} from "react-i18next"
|
||||
import {FieldArray, FormikProvider, useFormik} from "formik"
|
||||
import {TiDelete} from "react-icons/ti"
|
||||
import deepEqual from "deep-equal"
|
||||
import update from "immutability-helper"
|
||||
|
||||
import {useMutation} from "@tanstack/react-query"
|
||||
import {
|
||||
Divider,
|
||||
FormHelperText,
|
||||
Grid,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
Typography,
|
||||
} from "@mui/material"
|
||||
|
||||
import {ReservedAlias} from "~/server-types"
|
||||
import {updateReservedAlias} from "~/apis"
|
||||
import {parseFastAPIError} from "~/utils"
|
||||
import {queryClient} from "~/constants/react-query"
|
||||
import {useErrorSuccessSnacks} from "~/hooks"
|
||||
import AdminUserPicker from "~/route-widgets/ReservedAliasDetailRoute/AdminUserPicker"
|
||||
|
||||
export interface AliasUsersListProps {
|
||||
users: ReservedAlias["users"]
|
||||
id: string
|
||||
queryKey: readonly string[]
|
||||
}
|
||||
|
||||
interface Form {
|
||||
users: ReservedAlias["users"]
|
||||
}
|
||||
|
||||
export default function AliasUsersList({users, queryKey, id}: AliasUsersListProps): ReactElement {
|
||||
const {t} = useTranslation()
|
||||
const {showError, showSuccess} = useErrorSuccessSnacks()
|
||||
const {mutateAsync} = useMutation<
|
||||
ReservedAlias,
|
||||
AxiosError,
|
||||
ReservedAlias["users"],
|
||||
{previousAlias?: ReservedAlias}
|
||||
>(
|
||||
users =>
|
||||
updateReservedAlias(id, {
|
||||
users: users.map(user => ({
|
||||
id: user.id,
|
||||
})),
|
||||
}),
|
||||
{
|
||||
onMutate: async users => {
|
||||
await queryClient.cancelQueries(queryKey)
|
||||
|
||||
const previousAlias = queryClient.getQueryData<ReservedAlias>(queryKey)
|
||||
|
||||
queryClient.setQueryData<ReservedAlias>(queryKey, old =>
|
||||
update(old, {
|
||||
users: {
|
||||
$set: users,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
previousAlias,
|
||||
}
|
||||
},
|
||||
onSuccess: async newAlias => {
|
||||
showSuccess(t("relations.alias.mutations.success.aliasUpdated"))
|
||||
|
||||
await queryClient.cancelQueries(queryKey)
|
||||
|
||||
queryClient.setQueryData<ReservedAlias>(queryKey, newAlias as any as ReservedAlias)
|
||||
},
|
||||
onError: (error, _, context) => {
|
||||
showError(error)
|
||||
|
||||
setIsInEditMode(true)
|
||||
|
||||
if (context?.previousAlias) {
|
||||
queryClient.setQueryData<ReservedAlias>(queryKey, context.previousAlias)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
const schema = yup.object().shape({
|
||||
users: yup
|
||||
.array()
|
||||
.of(
|
||||
yup.object().shape({
|
||||
id: yup.string().required(),
|
||||
email: yup.object().shape({
|
||||
address: yup.string().required(),
|
||||
id: yup.string().required(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.label(t("routes.AliasDetailRoute.sections.users.fields.users.label")),
|
||||
})
|
||||
const initialValues: Form = {
|
||||
users: users,
|
||||
}
|
||||
const formik = useFormik<Form>({
|
||||
initialValues,
|
||||
validationSchema: schema,
|
||||
onSubmit: async (values, {setErrors}) => {
|
||||
try {
|
||||
await mutateAsync(values.users)
|
||||
} catch (error) {
|
||||
setErrors(parseFastAPIError(error as AxiosError))
|
||||
}
|
||||
},
|
||||
})
|
||||
const [isInEditMode, setIsInEditMode] = useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<Grid container direction="column" spacing={1}>
|
||||
<Grid item>
|
||||
<Grid container spacing={1} direction="row">
|
||||
<Grid item>
|
||||
<Typography variant="h6" component="h3">
|
||||
{t("routes.ReservedAliasDetailRoute.sections.users.title")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={formik.isSubmitting}
|
||||
onClick={async () => {
|
||||
setIsInEditMode(!isInEditMode)
|
||||
|
||||
if (
|
||||
isInEditMode &&
|
||||
!deepEqual(initialValues, formik.values, {
|
||||
strict: true,
|
||||
})
|
||||
) {
|
||||
await formik.submitForm()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isInEditMode ? <MdCheckCircle /> : <FaPen />}
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{isInEditMode ? (
|
||||
<FormikProvider value={formik}>
|
||||
<FieldArray
|
||||
name="users"
|
||||
render={arrayHelpers => (
|
||||
<List>
|
||||
{formik.values.users.map((user, index) => (
|
||||
<ListItem key={user.id}>
|
||||
<ListItemText primary={user.email.address} />
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="delete"
|
||||
onClick={async () => {
|
||||
arrayHelpers.remove(index)
|
||||
}}
|
||||
>
|
||||
<TiDelete />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<AdminUserPicker
|
||||
alreadyPicked={formik.values.users}
|
||||
onPick={user => arrayHelpers.push(user)}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
)}
|
||||
/>
|
||||
<FormHelperText
|
||||
error={Boolean(formik.touched.users && formik.errors.users)}
|
||||
>
|
||||
{formik.touched.users && (formik.errors.users as string)}
|
||||
</FormHelperText>
|
||||
</FormikProvider>
|
||||
) : (
|
||||
<List>
|
||||
{users.map(user => (
|
||||
<ListItem key={user.id}>
|
||||
<ListItemText primary={user.email.address} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
59
src/routes/ReservedAliasDetailRoute.tsx
Normal file
59
src/routes/ReservedAliasDetailRoute.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import {ReactElement} from "react"
|
||||
import {useTranslation} from "react-i18next"
|
||||
import {useParams} from "react-router-dom"
|
||||
import {AxiosError} from "axios"
|
||||
|
||||
import {useQuery} from "@tanstack/react-query"
|
||||
import {Grid} from "@mui/material"
|
||||
|
||||
import {QueryResult, SimplePage, SimplePageBuilder} from "~/components"
|
||||
import {ReservedAlias} from "~/server-types"
|
||||
import {getReservedAlias} from "~/apis"
|
||||
import AliasActivationSwitch from "~/route-widgets/ReservedAliasDetailRoute/AliasActivationSwitch"
|
||||
import AliasAddress from "~/route-widgets/AliasDetailRoute/AliasAddress"
|
||||
import AliasUsersList from "~/route-widgets/ReservedAliasDetailRoute/AliasUsersList"
|
||||
|
||||
export default function ReservedAliasDetailRoute(): ReactElement {
|
||||
const {t} = useTranslation()
|
||||
const params = useParams()
|
||||
const queryKey = ["get_reserved_alias", params.id!]
|
||||
|
||||
const query = useQuery<ReservedAlias, AxiosError>(queryKey, () => getReservedAlias(params.id!))
|
||||
|
||||
return (
|
||||
<SimplePage title={t("routes.ReservedAliasDetailRoute.title")}>
|
||||
<QueryResult<ReservedAlias, AxiosError> query={query}>
|
||||
{alias => (
|
||||
<SimplePageBuilder.MultipleSections>
|
||||
{[
|
||||
<Grid
|
||||
key="basic"
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item>
|
||||
<AliasAddress address={`${alias.local}@${alias.domain}`} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<AliasActivationSwitch
|
||||
id={alias.id}
|
||||
isActive={alias.isActive}
|
||||
queryKey={queryKey}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>,
|
||||
<AliasUsersList
|
||||
key="users"
|
||||
users={alias.users}
|
||||
id={alias.id}
|
||||
queryKey={queryKey}
|
||||
/>,
|
||||
]}
|
||||
</SimplePageBuilder.MultipleSections>
|
||||
)}
|
||||
</QueryResult>
|
||||
</SimplePage>
|
||||
)
|
||||
}
|
@ -108,7 +108,10 @@ export interface ReservedAlias {
|
||||
local: string
|
||||
users: Array<{
|
||||
id: string
|
||||
email: string
|
||||
email: {
|
||||
address: string
|
||||
id: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,10 @@ export default function parseFastAPIError(
|
||||
return {detail: error.detail}
|
||||
}
|
||||
|
||||
if (error.detail[0].loc[0] === "body" && error.detail[0].loc[1] === "__root__") {
|
||||
return {detail: error.detail[0].msg}
|
||||
}
|
||||
|
||||
return error.detail.reduce((acc, error) => {
|
||||
const [location, field] = error.loc
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user