diff --git a/src/App.tsx b/src/App.tsx index 62d9b06..67e8221 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,9 @@ import AliasesRoute from "~/routes/AliasesRoute" import AuthenticateRoute from "~/routes/AuthenticateRoute" import AuthenticatedRoute from "~/routes/AuthenticatedRoute" import CompleteAccountRoute from "~/routes/CompleteAccountRoute" +import CreateReservedAliasRoute from "~/routes/CreateReservedAliasRoute" import EnterDecryptionPassword from "~/routes/EnterDecryptionPassword" +import I18nHandler from "./I18nHandler" import LoginRoute from "~/routes/LoginRoute" import LogoutRoute from "~/routes/LogoutRoute" import OverviewRoute from "~/routes/OverviewRoute" @@ -23,9 +25,6 @@ import RootRoute from "~/routes/Root" import SettingsRoute from "~/routes/SettingsRoute" import SignupRoute from "~/routes/SignupRoute" import VerifyEmailRoute from "~/routes/VerifyEmailRoute" - -import AdminRoute from "~/routes/AdminRoute" -import I18nHandler from "./I18nHandler" import "./init-i18n" const router = createBrowserRouter([ @@ -101,9 +100,9 @@ const router = createBrowserRouter([ element: , }, { - path: "/admin", + path: "/admin/reserved-aliases/create", loader: getServerSettings, - element: , + element: , }, ], }, diff --git a/src/route-widgets/AdminPage/AliasExplanation.tsx b/src/route-widgets/CreateReservedAliasRoute/AliasExplanation.tsx similarity index 100% rename from src/route-widgets/AdminPage/AliasExplanation.tsx rename to src/route-widgets/CreateReservedAliasRoute/AliasExplanation.tsx diff --git a/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx b/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx new file mode 100644 index 0000000..feff4b2 --- /dev/null +++ b/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx @@ -0,0 +1,132 @@ +import {ReactElement} from "react" +import {HiUsers} from "react-icons/hi" +import {useTranslation} from "react-i18next" +import {AxiosError} from "axios" + +import {useQuery} from "@tanstack/react-query" +import { + Box, + Checkbox, + Chip, + FormControl, + FormHelperText, + InputAdornment, + InputLabel, + ListItemText, + MenuItem, + Select, + SelectProps, +} from "@mui/material" + +import {GetAdminUsersResponse, getAdminUsers} from "~/apis" +import {useUser} from "~/hooks" + +export interface UsersSelectFieldProps extends Omit { + onChange: SelectProps["onChange"] + value: GetAdminUsersResponse["users"] + + helperText?: string | string[] + error?: boolean +} + +export default function UsersSelectField({ + value, + onChange, + helperText, + error, + ...props +}: UsersSelectFieldProps): ReactElement { + const {t} = useTranslation() + const meUser = useUser() + const {data: {users} = {}} = useQuery( + ["getAdminUsers"], + getAdminUsers, + ) + const findUser = (id: string) => users?.find(user => user.id === id) + const userIds = value?.map(user => user.id) || [] + + return ( + + + {t("routes.AdminRoute.forms.reservedAliases.fields.users.label")} + + + {...props} + multiple + labelId="users-select" + defaultValue={[]} + value={userIds} + startAdornment={ + + + + } + renderValue={(selected: string[]) => ( + + {selected.map(value => ( + + ))} + + )} + onChange={(event, child) => { + if (!Array.isArray(event.target.value)) { + return + } + + console.log(event.target.value) + // Since there will probably only be a few admin users, n^2 is fine + const selectedUsers = (event.target.value as string[]).map(id => + users!.find(user => user.id === id), + ) + console.log(selectedUsers) + + if (!selectedUsers) { + return + } + + onChange!( + // @ts-ignore + { + ...event, + target: { + ...event.target, + value: selectedUsers as GetAdminUsersResponse["users"], + }, + }, + child, + ) + }} + name="users" + id="users" + error={error} + label={t("routes.AdminRoute.forms.reservedAliases.fields.users.label")} + > + {users ? ( + users.map(user => ( + + + { + // Check if user is me + if (user.id === meUser.id) { + return t( + "routes.AdminRoute.forms.reservedAliases.fields.users.me", + { + email: user.email.address, + }, + ) + } + + return user.email.address + })()} + /> + + )) + ) : ( + {t("general.loading")} + )} + + {helperText ? {helperText} : null} + + ) +} diff --git a/src/route-widgets/AdminPage/ReservedAliasesForm.tsx b/src/routes/CreateReservedAliasRoute.tsx similarity index 54% rename from src/route-widgets/AdminPage/ReservedAliasesForm.tsx rename to src/routes/CreateReservedAliasRoute.tsx index e8309ed..f61df89 100644 --- a/src/route-widgets/AdminPage/ReservedAliasesForm.tsx +++ b/src/routes/CreateReservedAliasRoute.tsx @@ -3,44 +3,32 @@ import {ReactElement} from "react" import {AxiosError} from "axios" import {useTranslation} from "react-i18next" import {useFormik} from "formik" - -import {useMutation, useQuery} from "@tanstack/react-query" - -import { - CreateReservedAliasData, - GetAdminUsersResponse, - createReservedAlias, - getAdminUsers, -} from "~/apis" -import {Grid, InputAdornment, MenuItem, TextField} from "@mui/material" import {BiText} from "react-icons/bi" -import {HiUsers} from "react-icons/hi" -import {useErrorSuccessSnacks, useNavigateToNext, useUser} from "~/hooks" -import {ReservedAlias, ServerUser} from "~/server-types" + +import {useMutation} from "@tanstack/react-query" +import {Grid, InputAdornment, TextField} from "@mui/material" + +import {CreateReservedAliasData, GetAdminUsersResponse, createReservedAlias} from "~/apis" +import {useErrorSuccessSnacks, useNavigateToNext} from "~/hooks" +import {ReservedAlias} from "~/server-types" import {parseFastAPIError} from "~/utils" import {SimpleForm} from "~/components" -import AliasExplanation from "~/route-widgets/AdminPage/AliasExplanation" +import AliasExplanation from "~/route-widgets/CreateReservedAliasRoute/AliasExplanation" +import UsersSelectField from "~/route-widgets/CreateReservedAliasRoute/UsersSelectField" interface Form { local: string - users: string[] + users: GetAdminUsersResponse["users"] isActive?: boolean - nonFieldError?: string + detail?: string } -export interface ReservedAliasesFormProps {} - -export default function ReservedAliasesForm({}: ReservedAliasesFormProps): ReactElement { +export default function CreateReservedAliasRoute(): ReactElement { const {t} = useTranslation() - const meUser = useUser() - const {showError, showSuccess} = useErrorSuccessSnacks() + const {showSuccess} = useErrorSuccessSnacks() const navigateToNext = useNavigateToNext("/admin/reserved-aliases") - const {data: {users} = {}} = useQuery( - ["getAdminUsers"], - getAdminUsers, - ) const {mutateAsync: createAlias} = useMutation< ReservedAlias, AxiosError, @@ -63,7 +51,15 @@ export default function ReservedAliasesForm({}: ReservedAliasesFormProps): React // Only store IDs of users, as they provide a reference to the user users: yup .array() - .of(yup.string()) + .of( + yup.object().shape({ + id: yup.string(), + email: yup.object().shape({ + id: yup.string(), + address: yup.string(), + }), + }), + ) .label(t("routes.AdminRoute.forms.reservedAliases.fields.users.label")), }) const formik = useFormik
({ @@ -76,18 +72,16 @@ export default function ReservedAliasesForm({}: ReservedAliasesFormProps): React try { await createAlias({ local: values.local, - users: values.users.map(id => ({ - id, + users: values.users.map(user => ({ + id: user.id, })), }) } catch (error) { + console.log(parseFastAPIError(error as AxiosError)) setErrors(parseFastAPIError(error as AxiosError)) } }, }) - const getUser = (id: string) => users?.find(user => user.id === id) as any as ServerUser - - if (!users) return null return ( @@ -100,9 +94,11 @@ export default function ReservedAliasesForm({}: ReservedAliasesFormProps): React continueActionLabel={t( "routes.AdminRoute.forms.reservedAliases.saveAction", )} - nonFieldError={formik.errors.nonFieldError} + nonFieldError={formik.errors.detail} > {[ + // We can improve this by using a custom component + // that directly shows whether the alias is available or not , - - - - ), - }} - name="users" - id="users" - label={t( - "routes.AdminRoute.forms.reservedAliases.fields.users.label", - )} - SelectProps={{ - multiple: true, - value: formik.values.users, - onChange: formik.handleChange, - }} + value={formik.values.users} + onChange={formik.handleChange} disabled={formik.isSubmitting} error={formik.touched.users && Boolean(formik.errors.users)} - helperText={formik.touched.users && formik.errors.users} - > - {users.map(user => ( - - {(() => { - // Check if user is me - if (user.id === meUser.id) { - return t( - "routes.AdminRoute.forms.reservedAliases.fields.users.me", - { - email: user.email.address, - }, - ) - } - - return user.email.address - })()} - - ))} - , + helperText={formik.errors.users as string} + />, ]}
- getUser(userId).email.address)} - /> + )