mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-20 08:15: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_one": "Forwards to one user",
|
||||||
"userAmount_other": "Forwards to {{count}} users"
|
"userAmount_other": "Forwards to {{count}} users"
|
||||||
},
|
},
|
||||||
|
"ReservedAliasDetailRoute": {
|
||||||
|
"title": "Reserved Alias Details",
|
||||||
|
"sections": {
|
||||||
|
"users": {
|
||||||
|
"title": "Users",
|
||||||
|
"fields": {
|
||||||
|
"users": {
|
||||||
|
"label": "Users",
|
||||||
|
"me": "{{email}} (Me)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"AdminRoute": {
|
"AdminRoute": {
|
||||||
"title": "Site configuration",
|
"title": "Site configuration",
|
||||||
"forms": {
|
"forms": {
|
||||||
|
@ -21,6 +21,7 @@ import LogoutRoute from "~/routes/LogoutRoute"
|
|||||||
import OverviewRoute from "~/routes/OverviewRoute"
|
import OverviewRoute from "~/routes/OverviewRoute"
|
||||||
import ReportDetailRoute from "~/routes/ReportDetailRoute"
|
import ReportDetailRoute from "~/routes/ReportDetailRoute"
|
||||||
import ReportsRoute from "~/routes/ReportsRoute"
|
import ReportsRoute from "~/routes/ReportsRoute"
|
||||||
|
import ReservedAliasDetailRoute from "~/routes/ReservedAliasDetailRoute"
|
||||||
import ReservedAliasesRoute from "~/routes/ReservedAliasesRoute"
|
import ReservedAliasesRoute from "~/routes/ReservedAliasesRoute"
|
||||||
import RootRoute from "~/routes/Root"
|
import RootRoute from "~/routes/Root"
|
||||||
import SettingsRoute from "~/routes/SettingsRoute"
|
import SettingsRoute from "~/routes/SettingsRoute"
|
||||||
@ -104,6 +105,10 @@ const router = createBrowserRouter([
|
|||||||
path: "/admin/reserved-aliases",
|
path: "/admin/reserved-aliases",
|
||||||
element: <ReservedAliasesRoute />,
|
element: <ReservedAliasesRoute />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/admin/reserved-aliases/:id",
|
||||||
|
element: <ReservedAliasDetailRoute />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/admin/reserved-aliases/create",
|
path: "/admin/reserved-aliases/create",
|
||||||
loader: getServerSettings,
|
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 {default as getReservedAliases} from "./get-reserved-aliases"
|
||||||
export * from "./create-reserved-alias"
|
export * from "./create-reserved-alias"
|
||||||
export {default as createReservedAlias} 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
|
local: string
|
||||||
users: Array<{
|
users: Array<{
|
||||||
id: string
|
id: string
|
||||||
email: string
|
email: {
|
||||||
|
address: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,10 @@ export default function parseFastAPIError(
|
|||||||
return {detail: error.detail}
|
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) => {
|
return error.detail.reduce((acc, error) => {
|
||||||
const [location, field] = error.loc
|
const [location, field] = error.loc
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user