From 517b49a5c43269b2215d4c0454dbc46dcdfb38f8 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 1 Feb 2023 19:55:10 +0100 Subject: [PATCH 01/30] current stand --- src/App.tsx | 4 ++++ .../AuthenticateRoute/NavigationButton.tsx | 6 +++++- src/routes/AdminRoute.tsx | 15 +++++++++++++++ src/server-types.ts | 1 + 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/routes/AdminRoute.tsx diff --git a/src/App.tsx b/src/App.tsx index f2578fd..8f2111d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -99,6 +99,10 @@ const router = createBrowserRouter([ loader: getServerSettings, element: , }, + { + path: "/admin", + element: , + }, ], }, ], diff --git a/src/route-widgets/AuthenticateRoute/NavigationButton.tsx b/src/route-widgets/AuthenticateRoute/NavigationButton.tsx index 0ef1b8e..022fc05 100644 --- a/src/route-widgets/AuthenticateRoute/NavigationButton.tsx +++ b/src/route-widgets/AuthenticateRoute/NavigationButton.tsx @@ -1,7 +1,7 @@ import {ReactElement, useContext} from "react" import {BiStats} from "react-icons/bi" import {MdSettings} from "react-icons/md" -import {FaMask} from "react-icons/fa" +import {FaMask, FaServer} from "react-icons/fa" import {Link as RouterLink, useLocation} from "react-router-dom" import {useTranslation} from "react-i18next" @@ -16,6 +16,7 @@ export enum NavigationSection { Aliases, Reports, Settings, + Admin, } export interface NavigationButtonProps { @@ -27,6 +28,7 @@ const SECTION_ICON_MAP: Record = { [NavigationSection.Aliases]: , [NavigationSection.Reports]: , [NavigationSection.Settings]: , + [NavigationSection.Admin]: , } const SECTION_TEXT_MAP: Record = { @@ -34,6 +36,7 @@ const SECTION_TEXT_MAP: Record = { [NavigationSection.Aliases]: "components.NavigationButton.aliases", [NavigationSection.Reports]: "components.NavigationButton.reports", [NavigationSection.Settings]: "components.NavigationButton.settings", + [NavigationSection.Admin]: "components.NavigationButton.admin", } const PATH_SECTION_MAP: Record = { @@ -41,6 +44,7 @@ const PATH_SECTION_MAP: Record = { aliases: NavigationSection.Aliases, reports: NavigationSection.Reports, settings: NavigationSection.Settings, + admin: NavigationSection.Admin, } export default function NavigationButton({section}: NavigationButtonProps): ReactElement { diff --git a/src/routes/AdminRoute.tsx b/src/routes/AdminRoute.tsx new file mode 100644 index 0000000..e2b9f66 --- /dev/null +++ b/src/routes/AdminRoute.tsx @@ -0,0 +1,15 @@ +import {ReactElement, useLayoutEffect} from "react"; +import {useNavigateToNext, useUser} from "~/hooks"; + +export default function AdminRoute(): ReactElement { + const navigateToNext = useNavigateToNext(); + const user = useUser(); + + useLayoutEffect(() => { + if (!user.isAdmin) { + navigateToNext(); + } + }, [user.isAdmin, navigateToNext]) + + +} diff --git a/src/server-types.ts b/src/server-types.ts index 9669ec1..eeef306 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -41,6 +41,7 @@ export interface ServerUser { isDecrypted: false encryptedPassword: string salt: string + isAdmin: boolean email: { address: string isVerified: boolean From fde7705850c04febbd4724e88be38ab15707141b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 2 Feb 2023 21:12:14 +0100 Subject: [PATCH 02/30] added ReservedAliasesForm.tsx --- public/locales/en-US/translation.json | 6 +++- src/App.tsx | 3 +- src/apis/get-admin-users.ts | 19 ++++++++++++ src/apis/get-reserved-aliases.ts | 23 ++++++++++++++ src/apis/index.ts | 4 +++ .../AdminPage/ReservedAliasesForm.tsx | 17 ++++++++++ src/routes/AdminRoute.tsx | 31 +++++++++++++------ src/server-types.ts | 10 ++++++ 8 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 src/apis/get-admin-users.ts create mode 100644 src/apis/get-reserved-aliases.ts create mode 100644 src/route-widgets/AdminPage/ReservedAliasesForm.tsx diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index 8cffdb8..8f0ede7 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -279,6 +279,9 @@ "LogoutRoute": { "title": "Log out", "description": "We are logging you out..." + }, + "AdminRoute": { + "title": "Site configuration" } }, @@ -287,7 +290,8 @@ "overview": "Overview", "aliases": "Aliases", "reports": "Reports", - "settings": "Settings" + "settings": "Settings", + "admin": "Admin" }, "AuthenticateRoute": { "signup": "Sign up", diff --git a/src/App.tsx b/src/App.tsx index 8f2111d..8771a11 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,7 @@ 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" @@ -101,7 +102,7 @@ const router = createBrowserRouter([ }, { path: "/admin", - element: , + element: , }, ], }, diff --git a/src/apis/get-admin-users.ts b/src/apis/get-admin-users.ts new file mode 100644 index 0000000..8d34dc6 --- /dev/null +++ b/src/apis/get-admin-users.ts @@ -0,0 +1,19 @@ +import {client} from "~/constants/axios-client" + +export interface GetAdminUsersResponse { + users: Array<{ + id: string + email: { + id: string + address: string + } + }> +} + +export default async function getAdminUsers(): Promise { + const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/users`, { + withCredentials: true, + }) + + return data +} diff --git a/src/apis/get-reserved-aliases.ts b/src/apis/get-reserved-aliases.ts new file mode 100644 index 0000000..8cfb5ce --- /dev/null +++ b/src/apis/get-reserved-aliases.ts @@ -0,0 +1,23 @@ +import {GetPageData, PaginationResult, ReservedAlias} from "~/server-types" +import {client} from "~/constants/axios-client" + +export interface GetReservedAliasesData extends GetPageData { + query?: string +} + +export default async function getReservedAliases({ + query, + size, + page, +}: GetReservedAliasesData = {}): Promise> { + const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/reserved-alias/`, { + withCredentials: true, + params: { + query, + size, + page, + }, + }) + + return data +} diff --git a/src/apis/index.ts b/src/apis/index.ts index 81a7522..ddf3005 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -38,3 +38,7 @@ export * from "./delete-report" export {default as deleteReport} from "./delete-report" export * from "./get-me" export {default as getMe} from "./get-me" +export * from "./get-admin-users" +export {default as getAdminUsers} from "./get-admin-users" +export * from "./get-reserved-aliases" +export {default as getReservedAliases} from "./get-reserved-aliases" diff --git a/src/route-widgets/AdminPage/ReservedAliasesForm.tsx b/src/route-widgets/AdminPage/ReservedAliasesForm.tsx new file mode 100644 index 0000000..86fac79 --- /dev/null +++ b/src/route-widgets/AdminPage/ReservedAliasesForm.tsx @@ -0,0 +1,17 @@ +import {ReactElement} from "react" +import {AxiosError} from "axios" + +import {useQuery} from "@tanstack/react-query" + +import {GetAdminUsersResponse, getAdminUsers} from "~/apis" + +export interface ReservedAliasesFormProps {} + +export default function ReservedAliasesForm({}: ReservedAliasesFormProps): ReactElement { + const {data: {users} = {}} = useQuery( + ["getAdminUsers"], + getAdminUsers, + ) + + console.log(users) +} diff --git a/src/routes/AdminRoute.tsx b/src/routes/AdminRoute.tsx index e2b9f66..c34fda8 100644 --- a/src/routes/AdminRoute.tsx +++ b/src/routes/AdminRoute.tsx @@ -1,15 +1,26 @@ -import {ReactElement, useLayoutEffect} from "react"; -import {useNavigateToNext, useUser} from "~/hooks"; +import {ReactElement, useLayoutEffect} from "react" +import {useTranslation} from "react-i18next" + +import {SimplePageBuilder} from "~/components" +import {useNavigateToNext, useUser} from "~/hooks" +import ReservedAliasesForm from "~/route-widgets/AdminPage/ReservedAliasesForm" +import ReservedAliasesList from "~/route-widgets/AdminPage/ReservedAliasesList" export default function AdminRoute(): ReactElement { - const navigateToNext = useNavigateToNext(); - const user = useUser(); - - useLayoutEffect(() => { - if (!user.isAdmin) { - navigateToNext(); - } - }, [user.isAdmin, navigateToNext]) + const {t} = useTranslation() + const navigateToNext = useNavigateToNext() + const user = useUser() + useLayoutEffect(() => { + if (!user.isAdmin) { + navigateToNext() + } + }, [user.isAdmin, navigateToNext]) + return ( + + + + + ) } diff --git a/src/server-types.ts b/src/server-types.ts index eeef306..8b6cb7c 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -101,6 +101,16 @@ export interface Alias { prefExpandUrlShorteners: boolean | null } +export interface ReservedAlias { + id: string + domain: string + local: string + users: Array<{ + id: string + email: string + }> +} + export interface AliasNote { version: "1.0" data: { From cd1fe2005af4d7fcae63ec5fe2455653779777ce Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 2 Feb 2023 21:23:50 +0100 Subject: [PATCH 03/30] added ReservedAliasesList.tsx --- .../AdminPage/ReservedAliasesList.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/route-widgets/AdminPage/ReservedAliasesList.tsx diff --git a/src/route-widgets/AdminPage/ReservedAliasesList.tsx b/src/route-widgets/AdminPage/ReservedAliasesList.tsx new file mode 100644 index 0000000..b856845 --- /dev/null +++ b/src/route-widgets/AdminPage/ReservedAliasesList.tsx @@ -0,0 +1,32 @@ +import {ReactElement} from "react" +import {AxiosError} from "axios" + +import {useQuery} from "@tanstack/react-query" +import {List, ListItem, ListItemText} from "@mui/material" + +import {getReservedAliases} from "~/apis" +import {PaginationResult, ReservedAlias} from "~/server-types" +import {QueryResult} from "~/components" + +export interface ReservedAliasesListProps {} + +export default function ReservedAliasesList({}: ReservedAliasesListProps): ReactElement { + const query = useQuery, AxiosError>( + ["getReservedAliases"], + () => getReservedAliases(), + ) + + return ( + , AxiosError> query={query}> + {({items}) => ( + + {items.map(alias => ( + + + + ))} + + )} + + ) +} From 32f8d17418a0b454145258e9cba310726385bdf3 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 4 Feb 2023 12:12:33 +0100 Subject: [PATCH 04/30] added Reserved Alias creation --- public/locales/en-US/translation.json | 23 ++- src/App.tsx | 1 + src/apis/create-reserved-alias.ts | 25 +++ src/apis/index.ts | 2 + src/components/widgets/SimpleForm.tsx | 2 +- .../AdminPage/AliasExplanation.tsx | 99 ++++++++++ .../AdminPage/ReservedAliasesForm.tsx | 171 +++++++++++++++++- 7 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 src/apis/create-reserved-alias.ts create mode 100644 src/route-widgets/AdminPage/AliasExplanation.tsx diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index 8f0ede7..4ff93a5 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -281,7 +281,28 @@ "description": "We are logging you out..." }, "AdminRoute": { - "title": "Site configuration" + "title": "Site configuration", + "forms": { + "reservedAliases": { + "title": "Reserved Aliases", + "description": "Define which aliases will be reserved for your domain.", + "saveAction": "Create Alias", + "fields": { + "local": { + "label": "Local" + }, + "users": { + "label": "Users", + "me": "{{email}} (Me)" + } + }, + "explanation": { + "step1": "User from outside", + "step2": "Sends mail to", + "step4": "KleckRelay forwards to" + } + } + } } }, diff --git a/src/App.tsx b/src/App.tsx index 8771a11..62d9b06 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -102,6 +102,7 @@ const router = createBrowserRouter([ }, { path: "/admin", + loader: getServerSettings, element: , }, ], diff --git a/src/apis/create-reserved-alias.ts b/src/apis/create-reserved-alias.ts new file mode 100644 index 0000000..28c1b96 --- /dev/null +++ b/src/apis/create-reserved-alias.ts @@ -0,0 +1,25 @@ +import {ReservedAlias} from "~/server-types" +import {client} from "~/constants/axios-client" + +export interface CreateReservedAliasData { + local: string + users: Array<{ + id: string + }> + + isActive?: boolean +} + +export default async function createReservedAlias( + aliasData: CreateReservedAliasData, +): Promise { + const {data} = await client.post( + `${import.meta.env.VITE_SERVER_BASE_URL}/v1/reserved-alias`, + aliasData, + { + withCredentials: true, + }, + ) + + return data +} diff --git a/src/apis/index.ts b/src/apis/index.ts index ddf3005..438a419 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -42,3 +42,5 @@ export * from "./get-admin-users" export {default as getAdminUsers} from "./get-admin-users" 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" diff --git a/src/components/widgets/SimpleForm.tsx b/src/components/widgets/SimpleForm.tsx index d8524d4..36fc510 100644 --- a/src/components/widgets/SimpleForm.tsx +++ b/src/components/widgets/SimpleForm.tsx @@ -73,7 +73,7 @@ export default function SimpleForm({ {children.map(input => ( - + {input} ))} diff --git a/src/route-widgets/AdminPage/AliasExplanation.tsx b/src/route-widgets/AdminPage/AliasExplanation.tsx new file mode 100644 index 0000000..70d3528 --- /dev/null +++ b/src/route-widgets/AdminPage/AliasExplanation.tsx @@ -0,0 +1,99 @@ +import {Grid, List, ListItem, ListItemText, Typography, useTheme} from "@mui/material" +import {ReactElement} from "react" +import {MdMail} from "react-icons/md" +import {useLoaderData} from "react-router" +import {ServerSettings} from "~/server-types" +import {useTranslation} from "react-i18next" +import {BsArrowDown} from "react-icons/bs" +import {FaMask} from "react-icons/fa" +import {HiUsers} from "react-icons/hi" + +export interface AliasExplanationProps { + local: string + emails: string[] +} + +export default function AliasExplanation({local, emails}: AliasExplanationProps): ReactElement { + const {t} = useTranslation() + const theme = useTheme() + const serverSettings = useLoaderData() as ServerSettings + + return ( + + + + + + + + + {t("routes.AdminRoute.forms.reservedAliases.explanation.step1")} + + + + + + + + + + + + {t("routes.AdminRoute.forms.reservedAliases.explanation.step2")} + + + + + + + + + + + + {local} + @{serverSettings.mailDomain} + + + + + + + + + + + + {t("routes.AdminRoute.forms.reservedAliases.explanation.step4")} + + + + + + + + + + + + {emails.map(email => ( + + + + ))} + + + + + + ) +} diff --git a/src/route-widgets/AdminPage/ReservedAliasesForm.tsx b/src/route-widgets/AdminPage/ReservedAliasesForm.tsx index 86fac79..e8309ed 100644 --- a/src/route-widgets/AdminPage/ReservedAliasesForm.tsx +++ b/src/route-widgets/AdminPage/ReservedAliasesForm.tsx @@ -1,17 +1,182 @@ +import * as yup from "yup" import {ReactElement} from "react" import {AxiosError} from "axios" +import {useTranslation} from "react-i18next" +import {useFormik} from "formik" -import {useQuery} from "@tanstack/react-query" +import {useMutation, useQuery} from "@tanstack/react-query" -import {GetAdminUsersResponse, getAdminUsers} from "~/apis" +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 {parseFastAPIError} from "~/utils" +import {SimpleForm} from "~/components" +import AliasExplanation from "~/route-widgets/AdminPage/AliasExplanation" + +interface Form { + local: string + users: string[] + + isActive?: boolean + + nonFieldError?: string +} export interface ReservedAliasesFormProps {} export default function ReservedAliasesForm({}: ReservedAliasesFormProps): ReactElement { + const {t} = useTranslation() + const meUser = useUser() + const {showError, showSuccess} = useErrorSuccessSnacks() + const navigateToNext = useNavigateToNext("/admin/reserved-aliases") const {data: {users} = {}} = useQuery( ["getAdminUsers"], getAdminUsers, ) + const {mutateAsync: createAlias} = useMutation< + ReservedAlias, + AxiosError, + CreateReservedAliasData + >(createReservedAlias, { + onSuccess: () => { + showSuccess(t("relations.alias.mutations.success.aliasCreation")) + navigateToNext() + }, + }) - console.log(users) + const schema = yup.object().shape({ + local: yup + .string() + .required() + .label(t("routes.AdminRoute.forms.reservedAliases.fields.local.label")), + isActive: yup + .boolean() + .label(t("routes.AdminRoute.forms.reservedAliases.fields.isActive.label")), + // Only store IDs of users, as they provide a reference to the user + users: yup + .array() + .of(yup.string()) + .label(t("routes.AdminRoute.forms.reservedAliases.fields.users.label")), + }) + const formik = useFormik
({ + validationSchema: schema, + initialValues: { + local: "", + users: [], + }, + onSubmit: async (values, {setErrors, resetForm}) => { + try { + await createAlias({ + local: values.local, + users: values.users.map(id => ({ + id, + })), + }) + } catch (error) { + setErrors(parseFastAPIError(error as AxiosError)) + } + }, + }) + const getUser = (id: string) => users?.find(user => user.id === id) as any as ServerUser + + if (!users) return null + + return ( + + + + + {[ + + + + ), + }} + name="local" + id="local" + label={t( + "routes.AdminRoute.forms.reservedAliases.fields.local.label", + )} + 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} + />, + + + + ), + }} + name="users" + id="users" + label={t( + "routes.AdminRoute.forms.reservedAliases.fields.users.label", + )} + SelectProps={{ + multiple: true, + 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 + })()} + + ))} + , + ]} + + + + + getUser(userId).email.address)} + /> + +
+ ) } From dead769e88a6ae9fe3c9439fd247962259f2b5c2 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 4 Feb 2023 13:11:50 +0100 Subject: [PATCH 05/30] improved CreateReservedAliasRoute.tsx --- src/App.tsx | 9 +- .../AliasExplanation.tsx | 0 .../UsersSelectField.tsx | 132 ++++++++++++++++++ .../CreateReservedAliasRoute.tsx} | 110 +++++---------- 4 files changed, 170 insertions(+), 81 deletions(-) rename src/route-widgets/{AdminPage => CreateReservedAliasRoute}/AliasExplanation.tsx (100%) create mode 100644 src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx rename src/{route-widgets/AdminPage/ReservedAliasesForm.tsx => routes/CreateReservedAliasRoute.tsx} (54%) 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)} - /> +
) From dac14af539589da003926c63f15dce675c009db6 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 4 Feb 2023 14:36:26 +0100 Subject: [PATCH 06/30] created ReservedAliasesRoute.tsx --- public/locales/en-US/translation.json | 16 +++ src/App.tsx | 5 + .../AliasExplanation.tsx | 21 ++-- src/routes/CreateReservedAliasRoute.tsx | 8 +- src/routes/ReservedAliasesRoute.tsx | 113 ++++++++++++++++++ src/server-types.ts | 1 + 6 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 src/routes/ReservedAliasesRoute.tsx diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index 4ff93a5..cf01f8f 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -280,6 +280,22 @@ "title": "Log out", "description": "We are logging you out..." }, + "ReservedAliasesRoute": { + "title": "Reserved Aliases", + "pageActions": { + "search": { + "label": "Search", + "placeholder": "Search for aliases" + } + }, + "actions": { + "create": { + "label": "Create new Reserved Alias" + } + }, + "userAmount_one": "Forwards to one user", + "userAmount_other": "Forwards to {{count}} users" + }, "AdminRoute": { "title": "Site configuration", "forms": { diff --git a/src/App.tsx b/src/App.tsx index 67e8221..217ee90 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ReservedAliasesRoute from "~/routes/ReservedAliasesRoute" import RootRoute from "~/routes/Root" import SettingsRoute from "~/routes/SettingsRoute" import SignupRoute from "~/routes/SignupRoute" @@ -99,6 +100,10 @@ const router = createBrowserRouter([ loader: getServerSettings, element: , }, + { + path: "/admin/reserved-aliases", + element: , + }, { path: "/admin/reserved-aliases/create", loader: getServerSettings, diff --git a/src/route-widgets/CreateReservedAliasRoute/AliasExplanation.tsx b/src/route-widgets/CreateReservedAliasRoute/AliasExplanation.tsx index 70d3528..5462f1f 100644 --- a/src/route-widgets/CreateReservedAliasRoute/AliasExplanation.tsx +++ b/src/route-widgets/CreateReservedAliasRoute/AliasExplanation.tsx @@ -4,7 +4,7 @@ import {MdMail} from "react-icons/md" import {useLoaderData} from "react-router" import {ServerSettings} from "~/server-types" import {useTranslation} from "react-i18next" -import {BsArrowDown} from "react-icons/bs" +import {BsArrowRight} from "react-icons/bs" import {FaMask} from "react-icons/fa" import {HiUsers} from "react-icons/hi" @@ -21,13 +21,14 @@ export default function AliasExplanation({local, emails}: AliasExplanationProps) return ( @@ -35,7 +36,7 @@ export default function AliasExplanation({local, emails}: AliasExplanationProps) - + {t("routes.AdminRoute.forms.reservedAliases.explanation.step1")} @@ -44,10 +45,10 @@ export default function AliasExplanation({local, emails}: AliasExplanationProps) - + - + {t("routes.AdminRoute.forms.reservedAliases.explanation.step2")} @@ -59,9 +60,11 @@ export default function AliasExplanation({local, emails}: AliasExplanationProps) - + {local} - @{serverSettings.mailDomain} + + @{serverSettings.mailDomain} + @@ -69,10 +72,10 @@ export default function AliasExplanation({local, emails}: AliasExplanationProps) - + - + {t("routes.AdminRoute.forms.reservedAliases.explanation.step4")} diff --git a/src/routes/CreateReservedAliasRoute.tsx b/src/routes/CreateReservedAliasRoute.tsx index f61df89..7e9f933 100644 --- a/src/routes/CreateReservedAliasRoute.tsx +++ b/src/routes/CreateReservedAliasRoute.tsx @@ -77,14 +77,13 @@ export default function CreateReservedAliasRoute(): ReactElement { })), }) } catch (error) { - console.log(parseFastAPIError(error as AxiosError)) setErrors(parseFastAPIError(error as AxiosError)) } }, }) return ( - +
- + user.email.address)} + /> ) diff --git a/src/routes/ReservedAliasesRoute.tsx b/src/routes/ReservedAliasesRoute.tsx new file mode 100644 index 0000000..493f141 --- /dev/null +++ b/src/routes/ReservedAliasesRoute.tsx @@ -0,0 +1,113 @@ +import {ReactElement, useState, useTransition} from "react" +import {AxiosError} from "axios" +import {useTranslation} from "react-i18next" +import {MdAdd, MdSearch} from "react-icons/md" +import {Link} from "react-router-dom" + +import { + Button, + InputAdornment, + List, + ListItemButton, + ListItemSecondaryAction, + ListItemText, + Switch, + TextField, +} from "@mui/material" +import {useQuery} from "@tanstack/react-query" + +import {PaginationResult, ReservedAlias} from "~/server-types" +import {getReservedAliases} from "~/apis" +import {QueryResult, SimplePage} from "~/components" + +export default function ReservedAliasesRoute(): ReactElement { + const {t} = useTranslation() + const [showSearch, setShowSearch] = useState(false) + const [searchValue, setSearchValue] = useState("") + const [queryValue, setQueryValue] = useState("") + const [, startTransition] = useTransition() + const query = useQuery, AxiosError>( + ["getReservedAliases", {queryValue}], + () => + getReservedAliases({ + query: queryValue, + }), + { + onSuccess: () => { + setShowSearch(true) + }, + }, + ) + + return ( + { + setSearchValue(event.target.value) + startTransition(() => { + setQueryValue(event.target.value) + }) + }} + label={t("routes.ReservedAliasesRoute.pageActions.search.label")} + placeholder={t( + "routes.ReservedAliasesRoute.pageActions.search.placeholder", + )} + id="search" + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + ) + } + actions={ + + } + > + , AxiosError> query={query}> + {({items: aliases}) => ( + + {aliases.map(alias => ( + + + {alias.local} + @{alias.domain} + + } + secondary={t("routes.ReservedAliasesRoute.userAmount", { + count: alias.users.length, + })} + /> + + + + + ))} + + )} + + + ) +} diff --git a/src/server-types.ts b/src/server-types.ts index 8b6cb7c..2723c40 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -103,6 +103,7 @@ export interface Alias { export interface ReservedAlias { id: string + isActive: boolean domain: string local: string users: Array<{ From 90dd7ca180cae307fc771df2796a5b402ff19fbe Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 4 Feb 2023 16:43:38 +0100 Subject: [PATCH 07/30] added ReservedAliasDetailRoute.tsx --- public/locales/en-US/translation.json | 14 ++ src/App.tsx | 5 + src/apis/get-reserved-alias.ts | 13 ++ src/apis/index.ts | 4 + src/apis/update-reserved-alias.ts | 27 +++ .../AdminUserPicker.tsx | 65 ++++++ .../AliasActivationSwitch.tsx | 86 ++++++++ .../AliasUsersList.tsx | 205 ++++++++++++++++++ src/routes/ReservedAliasDetailRoute.tsx | 59 +++++ src/server-types.ts | 5 +- src/utils/parse-fastapi-error.ts | 4 + 11 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 src/apis/get-reserved-alias.ts create mode 100644 src/apis/update-reserved-alias.ts create mode 100644 src/route-widgets/ReservedAliasDetailRoute/AdminUserPicker.tsx create mode 100644 src/route-widgets/ReservedAliasDetailRoute/AliasActivationSwitch.tsx create mode 100644 src/route-widgets/ReservedAliasDetailRoute/AliasUsersList.tsx create mode 100644 src/routes/ReservedAliasDetailRoute.tsx diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index cf01f8f..c414c7e 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -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": { diff --git a/src/App.tsx b/src/App.tsx index 217ee90..f2cfed2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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: , }, + { + path: "/admin/reserved-aliases/:id", + element: , + }, { path: "/admin/reserved-aliases/create", loader: getServerSettings, diff --git a/src/apis/get-reserved-alias.ts b/src/apis/get-reserved-alias.ts new file mode 100644 index 0000000..89a547b --- /dev/null +++ b/src/apis/get-reserved-alias.ts @@ -0,0 +1,13 @@ +import {ReservedAlias} from "~/server-types" +import {client} from "~/constants/axios-client" + +export default async function getReservedAlias(id: string): Promise { + const {data} = await client.get( + `${import.meta.env.VITE_SERVER_BASE_URL}/v1/reserved-alias/${id}`, + { + withCredentials: true, + }, + ) + + return data +} diff --git a/src/apis/index.ts b/src/apis/index.ts index 438a419..e2ae2ad 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -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" diff --git a/src/apis/update-reserved-alias.ts b/src/apis/update-reserved-alias.ts new file mode 100644 index 0000000..f7d217e --- /dev/null +++ b/src/apis/update-reserved-alias.ts @@ -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 { + const {data} = await client.patch( + `${import.meta.env.VITE_SERVER_BASE_URL}/v1/reserved-alias/${id}`, + { + isActive, + users, + }, + { + withCredentials: true, + }, + ) + + return data +} diff --git a/src/route-widgets/ReservedAliasDetailRoute/AdminUserPicker.tsx b/src/route-widgets/ReservedAliasDetailRoute/AdminUserPicker.tsx new file mode 100644 index 0000000..a33e68f --- /dev/null +++ b/src/route-widgets/ReservedAliasDetailRoute/AdminUserPicker.tsx @@ -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( + ["getAdminUsers"], + getAdminUsers, + ) + + if (!availableUsers) { + return <> + } + + const users = availableUsers.filter( + user => !alreadyPicked.find(picked => picked.id === user.id), + ) + + if (users.length === 0) { + return <> + } + + return ( + { + const user = users.find(user => user.id === event.target.value) + if (user) { + onPick(user) + } + + event.preventDefault() + }} + > + {users.map(user => ( + + {user.id === meUser?.id + ? t("routes.AdminRoute.forms.reservedAliases.fields.users.me", { + email: user.email.address, + }) + : user.email.address} + + ))} + + ) +} diff --git a/src/route-widgets/ReservedAliasDetailRoute/AliasActivationSwitch.tsx b/src/route-widgets/ReservedAliasDetailRoute/AliasActivationSwitch.tsx new file mode 100644 index 0000000..6057d11 --- /dev/null +++ b/src/route-widgets/ReservedAliasDetailRoute/AliasActivationSwitch.tsx @@ -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(queryKey) + + queryClient.setQueryData(queryKey, old => + update(old, { + isActive: { + $set: activeNow!, + }, + }), + ) + + return {previousAlias} + }, + onSuccess: newAlias => { + queryClient.setQueryData(queryKey, newAlias) + }, + onError: (error, values, context) => { + showError(error) + + if (context?.previousAlias) { + queryClient.setQueryData(queryKey, context.previousAlias) + } + }, + }, + ) + + return ( + { + if (isLoading) { + return + } + + try { + await mutateAsync(!isActive) + + showSuccess( + isActive + ? t("relations.alias.mutations.success.aliasChangedToDisabled") + : t("relations.alias.mutations.success.aliasChangedToEnabled"), + ) + } catch {} + }} + /> + ) +} diff --git a/src/route-widgets/ReservedAliasDetailRoute/AliasUsersList.tsx b/src/route-widgets/ReservedAliasDetailRoute/AliasUsersList.tsx new file mode 100644 index 0000000..a5ca752 --- /dev/null +++ b/src/route-widgets/ReservedAliasDetailRoute/AliasUsersList.tsx @@ -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(queryKey) + + queryClient.setQueryData(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(queryKey, newAlias as any as ReservedAlias) + }, + onError: (error, _, context) => { + showError(error) + + setIsInEditMode(true) + + if (context?.previousAlias) { + queryClient.setQueryData(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({ + initialValues, + validationSchema: schema, + onSubmit: async (values, {setErrors}) => { + try { + await mutateAsync(values.users) + } catch (error) { + setErrors(parseFastAPIError(error as AxiosError)) + } + }, + }) + const [isInEditMode, setIsInEditMode] = useState(false) + + return ( + + + + + + {t("routes.ReservedAliasDetailRoute.sections.users.title")} + + + + { + setIsInEditMode(!isInEditMode) + + if ( + isInEditMode && + !deepEqual(initialValues, formik.values, { + strict: true, + }) + ) { + await formik.submitForm() + } + }} + > + {isInEditMode ? : } + + + + + + {isInEditMode ? ( + + ( + + {formik.values.users.map((user, index) => ( + + + + { + arrayHelpers.remove(index) + }} + > + + + + + ))} + + + arrayHelpers.push(user)} + /> + + + )} + /> + + {formik.touched.users && (formik.errors.users as string)} + + + ) : ( + + {users.map(user => ( + + + + ))} + + )} + + + ) +} diff --git a/src/routes/ReservedAliasDetailRoute.tsx b/src/routes/ReservedAliasDetailRoute.tsx new file mode 100644 index 0000000..b3c9eac --- /dev/null +++ b/src/routes/ReservedAliasDetailRoute.tsx @@ -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(queryKey, () => getReservedAlias(params.id!)) + + return ( + + query={query}> + {alias => ( + + {[ + + + + + + + + , + , + ]} + + )} + + + ) +} diff --git a/src/server-types.ts b/src/server-types.ts index 2723c40..42e284e 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -108,7 +108,10 @@ export interface ReservedAlias { local: string users: Array<{ id: string - email: string + email: { + address: string + id: string + } }> } diff --git a/src/utils/parse-fastapi-error.ts b/src/utils/parse-fastapi-error.ts index 9d32c50..554c020 100644 --- a/src/utils/parse-fastapi-error.ts +++ b/src/utils/parse-fastapi-error.ts @@ -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 From 6a1629114cadf0163ee3fb8bd1cb400047293e46 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 4 Feb 2023 21:59:03 +0100 Subject: [PATCH 08/30] added AdminRoute.tsx --- public/locales/en-US/translation.json | 5 ++++- src/App.tsx | 5 +++++ src/routes/AdminRoute.tsx | 16 ++++++++++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index c414c7e..75c8a21 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -312,10 +312,13 @@ }, "AdminRoute": { "title": "Site configuration", + "routes": { + "reservedAliases": "Reserved Aliases" + }, "forms": { "reservedAliases": { "title": "Reserved Aliases", - "description": "Define which aliases will be reserved for your domain.", + "description": "Define what alias should forward to whom.", "saveAction": "Create Alias", "fields": { "local": { diff --git a/src/App.tsx b/src/App.tsx index f2cfed2..e692d4b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import {CssBaseline, ThemeProvider} from "@mui/material" import {queryClient} from "~/constants/react-query" import {getServerSettings} from "~/apis" import {lightTheme} from "~/constants/themes" +import AdminRoute from "~/routes/AdminRoute" import AliasDetailRoute from "~/routes/AliasDetailRoute" import AliasesRoute from "~/routes/AliasesRoute" import AuthenticateRoute from "~/routes/AuthenticateRoute" @@ -101,6 +102,10 @@ const router = createBrowserRouter([ loader: getServerSettings, element: , }, + { + path: "/admin", + element: , + }, { path: "/admin/reserved-aliases", element: , diff --git a/src/routes/AdminRoute.tsx b/src/routes/AdminRoute.tsx index c34fda8..c19b2aa 100644 --- a/src/routes/AdminRoute.tsx +++ b/src/routes/AdminRoute.tsx @@ -1,10 +1,12 @@ import {ReactElement, useLayoutEffect} from "react" import {useTranslation} from "react-i18next" +import {BsStarFill} from "react-icons/bs" +import {Link} from "react-router-dom" + +import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material" import {SimplePageBuilder} from "~/components" import {useNavigateToNext, useUser} from "~/hooks" -import ReservedAliasesForm from "~/route-widgets/AdminPage/ReservedAliasesForm" -import ReservedAliasesList from "~/route-widgets/AdminPage/ReservedAliasesList" export default function AdminRoute(): ReactElement { const {t} = useTranslation() @@ -19,8 +21,14 @@ export default function AdminRoute(): ReactElement { return ( - - + + + + + + + + ) } From e6cdfcbc5adfa5e79158817ab88c8292c9541a13 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 9 Feb 2023 21:51:38 +0100 Subject: [PATCH 09/30] feat(settings): Add admin settings page (draft) --- public/locales/en-US/translation.json | 45 ++++++++- src/App.tsx | 5 + src/apis/get-admin-settings.ts | 10 ++ src/apis/index.ts | 4 + src/apis/update-admin-settings.ts | 16 ++++ .../GlobalSettingsRoute/SettingForm.tsx | 92 +++++++++++++++++++ src/routes/AdminRoute.tsx | 9 ++ src/routes/GlobalSettingsRoute.tsx | 19 ++++ src/server-types.ts | 13 +++ 9 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/apis/get-admin-settings.ts create mode 100644 src/apis/update-admin-settings.ts create mode 100644 src/route-widgets/GlobalSettingsRoute/SettingForm.tsx create mode 100644 src/routes/GlobalSettingsRoute.tsx diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index 75c8a21..6db885a 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -313,7 +313,8 @@ "AdminRoute": { "title": "Site configuration", "routes": { - "reservedAliases": "Reserved Aliases" + "reservedAliases": "Reserved Aliases", + "settings": "Global Settings" }, "forms": { "reservedAliases": { @@ -334,6 +335,48 @@ "step2": "Sends mail to", "step4": "KleckRelay forwards to" } + }, + "settings": { + "randomEmailIdMinLength": { + "label": "Minimum random alias ID length", + "description": "The minimum length for randomly generated emails. The server will automatically increase the length if required so." + }, + "randomEmailIdChars": { + "label": "Random alias character pool", + "description": "Characters that are used to generate random emails." + }, + "randomEmailIdLengthIncreaseOnPercentage": { + "label": "Percentage of used aliases", + "description": "If the percentage of used random email IDs is higher than this value, the length of the random email ID will be increased. This is used to prevent spammers from guessing the email ID." + }, + "customEmailSuffixLength": { + "label": "Custom email suffix length", + "description": "The length of the custom email suffix." + }, + "customEmailSuffixChars": { + "label": "Custom email suffix character pool", + "description": "Characters that are used to generate custom email suffixes." + }, + "imageProxyStorageLifeTimeInHours": { + "label": "Image proxy storage lifetime", + "description": "The lifetime of images that are stored on the server. After this time, the image will be deleted." + }, + "enableImageProxy": { + "label": "Enable image proxy", + "description": "If enabled, images will be stored on the server and forwarded to the user. This is useful if you want to prevent the user from seeing the IP address of the server. This will only affect new images." + }, + "userEmailEnableDisposableEmails": { + "label": "Enable disposable emails for new accounts", + "description": "If enabled, users will be able to use disposable emails when creating a new account. This will only affect new accounts." + }, + "userEmailEnableOtherRelays": { + "label": "Enable other relays for new accounts", + "description": "If enabled, users will be able to use other relays (such as SimpleLogin or DuckDuckGo's Email Tracking Protection) when creating a new account. This will only affect new accounts." + }, + "allowStatistics": { + "label": "Allow statistics", + "description": "If enabled, your instance will collect anonymous statistics and share them. They will only be stored locally on this instance but made public." + } } } } diff --git a/src/App.tsx b/src/App.tsx index e692d4b..0e4e335 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import AuthenticatedRoute from "~/routes/AuthenticatedRoute" import CompleteAccountRoute from "~/routes/CompleteAccountRoute" import CreateReservedAliasRoute from "~/routes/CreateReservedAliasRoute" import EnterDecryptionPassword from "~/routes/EnterDecryptionPassword" +import GlobalSettingsRoute from "~/routes/GlobalSettingsRoute" import I18nHandler from "./I18nHandler" import LoginRoute from "~/routes/LoginRoute" import LogoutRoute from "~/routes/LogoutRoute" @@ -119,6 +120,10 @@ const router = createBrowserRouter([ loader: getServerSettings, element: , }, + { + path: "/admin/settings", + element: , + }, ], }, ], diff --git a/src/apis/get-admin-settings.ts b/src/apis/get-admin-settings.ts new file mode 100644 index 0000000..b262a91 --- /dev/null +++ b/src/apis/get-admin-settings.ts @@ -0,0 +1,10 @@ +import {client} from "~/constants/axios-client" +import {AdminSettings} from "~/server-types" + +export default async function getAdminSettings(): Promise { + const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`, { + withCredentials: true, + }) + + return data +} diff --git a/src/apis/index.ts b/src/apis/index.ts index e2ae2ad..bfb40f1 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -48,3 +48,7 @@ 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" +export * from "./get-admin-settings" +export {default as getAdminSettings} from "./get-admin-settings" +export * from "./update-admin-settings" +export {default as updateAdminSettings} from "./update-admin-settings" diff --git a/src/apis/update-admin-settings.ts b/src/apis/update-admin-settings.ts new file mode 100644 index 0000000..5be9fd9 --- /dev/null +++ b/src/apis/update-admin-settings.ts @@ -0,0 +1,16 @@ +import {client} from "~/constants/axios-client" +import {AdminSettings} from "~/server-types" + +export default async function updateAdminSettings( + settings: Partial, +): Promise { + const {data} = await client.patch( + `${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`, + settings, + { + withCredentials: true, + }, + ) + + return data +} diff --git a/src/route-widgets/GlobalSettingsRoute/SettingForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingForm.tsx new file mode 100644 index 0000000..4b643c1 --- /dev/null +++ b/src/route-widgets/GlobalSettingsRoute/SettingForm.tsx @@ -0,0 +1,92 @@ +import * as yup from "yup" +import {AdminSettings} from "~/server-types" +import {useTranslation} from "react-i18next" +import {useFormik} from "formik" +import {SimpleForm} from "~/components" +import {TextField} from "@mui/material" + +export interface SettingsFormProps { + settings: AdminSettings +} + +export default function SettingsForm({settings}: SettingsFormProps) { + const {t} = useTranslation() + + const validationSchema = yup.object().shape({ + randomEmailIdMinLength: yup + .number() + .min(1) + .max(1_023) + .label(t("routes.AdminRoute.forms.settings.randomEmailIdMinLength.label")), + randomEmailIdChars: yup + .string() + .label(t("routes.AdminRoute.forms.settings.randomEmailIdChars.label")), + randomEmailLengthIncreaseOnPercentage: yup + .number() + .label( + t("routes.AdminRoute.forms.settings.randomEmailLengthIncreaseOnPercentage.label"), + ), + imageProxyStorageLifeTimeInHours: yup + .number() + .label(t("routes.AdminRoute.forms.settings.imageProxyStorageLifeTimeInHours.label")), + customEmailSuffixLength: yup + .number() + .min(1) + .max(1_023) + .label(t("routes.AdminRoute.forms.settings.customEmailSuffixLength-label")), + customEmailSuffixChars: yup + .string() + .label(t("routes.AdminRoute.forms.settings.customEmailSuffixChars.label")), + userEmailEnableDisposableEmails: yup + .boolean() + .label(t("routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.label")), + userEmailEnableOtherRelays: yup + .boolean() + .label(t("routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.label")), + enableImageProxy: yup + .boolean() + .label(t("routes.AdminRoute.forms.settings.enableImageProxy.label")), + allowStatistics: yup + .boolean() + .label(t("routes.AdminRoute.forms.settings.allowStatistics.label")), + }) + + const formik = useFormik({ + validationSchema, + onSubmit: console.log, + initialValues: settings, + }) + + return ( + + + {[ + , + ]} + + + ) +} diff --git a/src/routes/AdminRoute.tsx b/src/routes/AdminRoute.tsx index c19b2aa..37ce762 100644 --- a/src/routes/AdminRoute.tsx +++ b/src/routes/AdminRoute.tsx @@ -1,6 +1,7 @@ import {ReactElement, useLayoutEffect} from "react" import {useTranslation} from "react-i18next" import {BsStarFill} from "react-icons/bs" +import {AiFillEdit} from "react-icons/ai" import {Link} from "react-router-dom" import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material" @@ -13,6 +14,8 @@ export default function AdminRoute(): ReactElement { const navigateToNext = useNavigateToNext() const user = useUser() + console.log(user) + useLayoutEffect(() => { if (!user.isAdmin) { navigateToNext() @@ -28,6 +31,12 @@ export default function AdminRoute(): ReactElement { + + + + + + ) diff --git a/src/routes/GlobalSettingsRoute.tsx b/src/routes/GlobalSettingsRoute.tsx new file mode 100644 index 0000000..84b5e57 --- /dev/null +++ b/src/routes/GlobalSettingsRoute.tsx @@ -0,0 +1,19 @@ +import {ReactElement} from "react" +import {useQuery} from "@tanstack/react-query" +import {AdminSettings} from "~/server-types" +import {AxiosError} from "axios" +import {getAdminSettings} from "~/apis" +import {QueryResult} from "~/components" +import {useTranslation} from "react-i18next" +import SettingsForm from "~/route-widgets/GlobalSettingsRoute/SettingForm" + +export default function GlobalSettingsRoute(): ReactElement { + const {t} = useTranslation() + const query = useQuery(["get_admin_settings"], getAdminSettings) + + return ( + query={query}> + {settings => } + + ) +} diff --git a/src/server-types.ts b/src/server-types.ts index 42e284e..a0d252d 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -198,3 +198,16 @@ export interface GetPageData { page?: number size?: number } + +export interface AdminSettings { + randomEmailIdMinLength: number + randomEmailIdChars: string + randomEmailLengthIncreaseOnPercentage: number + customEmailSuffixLength: number + customEmailSuffixChars: string + imageProxyStorageLifeTimeInHours: number + enableImageProxy: boolean + userEmailEnableDisposableEmails: boolean + userEmailEnableOtherRelays: boolean + allowStatistics: boolean +} From 1f46671dc43b63bf753b3f24c121af69f44bcd18 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Feb 2023 19:00:46 +0100 Subject: [PATCH 10/30] fix: remove console.log --- .../CreateReservedAliasRoute/UsersSelectField.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx b/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx index feff4b2..67fe769 100644 --- a/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx +++ b/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx @@ -18,7 +18,7 @@ import { SelectProps, } from "@mui/material" -import {GetAdminUsersResponse, getAdminUsers} from "~/apis" +import {getAdminUsers, GetAdminUsersResponse} from "~/apis" import {useUser} from "~/hooks" export interface UsersSelectFieldProps extends Omit { @@ -73,12 +73,10 @@ export default function UsersSelectField({ 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 From 6eacf14bae07e0bfeee15d853b5bc776efa2d3ce Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Feb 2023 20:07:34 +0100 Subject: [PATCH 11/30] fix: improvements --- .../CreateReservedAliasRoute/UsersSelectField.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx b/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx index 67fe769..029d50e 100644 --- a/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx +++ b/src/route-widgets/CreateReservedAliasRoute/UsersSelectField.tsx @@ -18,10 +18,11 @@ import { SelectProps, } from "@mui/material" -import {getAdminUsers, GetAdminUsersResponse} from "~/apis" +import {GetAdminUsersResponse, getAdminUsers} from "~/apis" import {useUser} from "~/hooks" -export interface UsersSelectFieldProps extends Omit { +export interface UsersSelectFieldProps + extends Omit { onChange: SelectProps["onChange"] value: GetAdminUsersResponse["users"] From 82a3641a6d4e85ad0b78b3fceec7ad55b0b845d9 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Feb 2023 20:16:44 +0100 Subject: [PATCH 12/30] feat: Add StringPoolField --- package.json | 2 + public/locales/en-US/translation.json | 20 +- src/apis/get-admin-settings.ts | 2 +- .../widgets/StringPoolField/AddNewDialog.tsx | 68 +++++++ .../StringPoolField/StringPoolField.tsx | 183 ++++++++++++++++++ .../widgets/StringPoolField/index.ts | 2 + src/components/widgets/index.ts | 1 + 7 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 src/components/widgets/StringPoolField/AddNewDialog.tsx create mode 100644 src/components/widgets/StringPoolField/StringPoolField.tsx create mode 100644 src/components/widgets/StringPoolField/index.ts diff --git a/package.json b/package.json index d458915..8d6e10d 100755 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "immutability-helper": "^3.1.1", "in-milliseconds": "^1.2.0", "in-seconds": "^1.2.0", + "lodash": "^4.17.21", "notistack": "^2.0.8", "openpgp": "^5.5.0", "react": "^18.2.0", @@ -56,6 +57,7 @@ "@types/deep-equal": "^1.0.1", "@types/group-array": "^1.0.1", "@types/jest": "^29.2.4", + "@types/lodash": "^4.14.191", "@types/node": "^18.11.18", "@types/openpgp": "^4.4.18", "@types/react-icons": "^3.0.0", diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index 6db885a..d1419ef 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -3,6 +3,7 @@ "cancelLabel": "Cancel", "emptyValue": "-", "emptyUnavailableValue": "Unavailable", + "saveLabel": "Save", "defaultValueSelection": "Default <{{value}}>", "defaultValueSelectionRaw": "<{{value}}>", @@ -345,7 +346,7 @@ "label": "Random alias character pool", "description": "Characters that are used to generate random emails." }, - "randomEmailIdLengthIncreaseOnPercentage": { + "randomEmailLengthIncreaseOnPercentage": { "label": "Percentage of used aliases", "description": "If the percentage of used random email IDs is higher than this value, the length of the random email ID will be increased. This is used to prevent spammers from guessing the email ID." }, @@ -378,6 +379,10 @@ "description": "If enabled, your instance will collect anonymous statistics and share them. They will only be stored locally on this instance but made public." } } + }, + "settings": { + "title": "Global Settings", + "description": "Configure global settings for your instance." } } }, @@ -458,6 +463,19 @@ "doNotShare": "Do not share", "decideLater": "Decide later", "doNotAskAgain": "Do not ask again" + }, + "StringPoolField": { + "addCustom": { + "label": "Add custom" + }, + "forms": { + "addNew": { + "title": "Add new value", + "description": "Enter your characters you would like to include", + "label": "Characters", + "submit": "Add" + } + } } }, diff --git a/src/apis/get-admin-settings.ts b/src/apis/get-admin-settings.ts index b262a91..afa9427 100644 --- a/src/apis/get-admin-settings.ts +++ b/src/apis/get-admin-settings.ts @@ -1,7 +1,7 @@ import {client} from "~/constants/axios-client" import {AdminSettings} from "~/server-types" -export default async function getAdminSettings(): Promise { +export default async function getAdminSettings(): Promise> { const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`, { withCredentials: true, }) diff --git a/src/components/widgets/StringPoolField/AddNewDialog.tsx b/src/components/widgets/StringPoolField/AddNewDialog.tsx new file mode 100644 index 0000000..9ef899f --- /dev/null +++ b/src/components/widgets/StringPoolField/AddNewDialog.tsx @@ -0,0 +1,68 @@ +import {ReactElement, useState} from "react" +import {useTranslation} from "react-i18next" +import {MdCheck} from "react-icons/md" +import {TiCancel} from "react-icons/ti" + +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + TextField, +} from "@mui/material" + +import {whenEnterPressed} from "~/utils" + +export interface StringPoolFieldProps { + onCreated: (value: string) => void + onClose: () => void + + open?: boolean +} + +export default function AddNewDialog({ + onCreated, + open = false, + onClose, +}: StringPoolFieldProps): ReactElement { + const {t} = useTranslation() + + const [value, setValue] = useState("") + + return ( + + {t("components.StringPoolField.forms.addNew.title")} + + + {t("components.StringPoolField.forms.addNew.description")} + + + setValue(e.target.value)} + label={t("components.StringPoolField.forms.addNew.label")} + name="addNew" + fullWidth + autoFocus + onKeyUp={whenEnterPressed(() => onCreated(value))} + /> + + + + + + + + ) +} diff --git a/src/components/widgets/StringPoolField/StringPoolField.tsx b/src/components/widgets/StringPoolField/StringPoolField.tsx new file mode 100644 index 0000000..ecc2e69 --- /dev/null +++ b/src/components/widgets/StringPoolField/StringPoolField.tsx @@ -0,0 +1,183 @@ +import {useTranslation} from "react-i18next" +import {MdAdd} from "react-icons/md" +import React, {ReactElement, useLayoutEffect, useMemo, useState} from "react" + +import { + Box, + Checkbox, + Chip, + FormControl, + FormHelperText, + InputLabel, + ListItemIcon, + ListItemText, + MenuItem, + Select, + SelectProps, +} from "@mui/material" + +import AddNewDialog from "./AddNewDialog" + +export interface StringPoolFieldProps + extends Omit, "onChange" | "value" | "multiple" | "labelId" | "label"> { + pools: Record + label: string + value: string + onChange: SelectProps["onChange"] + id: string + + allowCustom?: boolean + helperText?: string | string[] + error?: boolean +} + +export function createPool(pools: Record): Record { + return Object.fromEntries( + Object.entries(pools).map(([key, value]) => [key.split("").sort().join(""), value]), + ) +} + +export default function StringPoolField({ + pools, + value, + helperText, + id, + error, + label, + onChange, + onOpen, + allowCustom, + name, + fullWidth, + ...props +}: StringPoolFieldProps): ReactElement { + const {t} = useTranslation() + + const reversedPoolsMap = useMemo( + () => Object.fromEntries(Object.entries(pools).map(([key, value]) => [value, key])), + [pools], + ) + const [isInAddMode, setIsInAddMode] = useState(false) + const [uiRemainingValue, setUiRemainingValue] = useState("") + + const selectedValueMaps = Object.entries(pools) + .filter(([key]) => value.includes(key)) + .map(([, value]) => value) + const remainingValue = (() => { + // List of all characters inside the pools + const charactersInPools = Object.keys(pools).join("") + + return value + .split("") + .filter(char => !charactersInPools.includes(char)) + .join("") + })() + const selectValue = [...selectedValueMaps, remainingValue].filter(Boolean) + + useLayoutEffect(() => { + if (remainingValue) { + setUiRemainingValue(remainingValue) + } + }, [remainingValue]) + + return ( + <> + + + {label} + + + multiple + name={name} + labelId={id} + renderValue={(selected: string[]) => ( + + {selected.map(value => ( + + ))} + + )} + value={selectValue} + label={label} + onOpen={event => { + if (!remainingValue) { + setUiRemainingValue("") + } + + onOpen?.(event) + }} + onChange={(event, child) => { + if (!Array.isArray(event.target.value)) { + return + } + + const value = event.target.value.reduce((acc, value) => { + if (reversedPoolsMap[value]) { + return acc + reversedPoolsMap[value] + } + + return acc + value + }, "") + + onChange!( + // @ts-ignore + { + ...event, + target: { + ...event.target, + value: value as string, + }, + }, + child, + ) + }} + {...props} + > + {Object.entries(pools).map(([poolValue, label]) => ( + + + + + ))} + {uiRemainingValue && ( + + + + + )} + {allowCustom && ( + { + event.preventDefault() + setIsInAddMode(true) + }} + > + + + + + + )} + + {helperText ? {helperText} : null} + + { + setIsInAddMode(false) + + // @ts-ignore: This is enough for formik. + onChange({ + target: { + name, + value: value + newValue, + }, + }) + }} + onClose={() => setIsInAddMode(false)} + open={isInAddMode} + /> + + ) +} diff --git a/src/components/widgets/StringPoolField/index.ts b/src/components/widgets/StringPoolField/index.ts new file mode 100644 index 0000000..a9a2018 --- /dev/null +++ b/src/components/widgets/StringPoolField/index.ts @@ -0,0 +1,2 @@ +export * from "./StringPoolField" +export {default as StringPoolField} from "./StringPoolField" diff --git a/src/components/widgets/index.ts b/src/components/widgets/index.ts index b8b3f5e..d25c848 100644 --- a/src/components/widgets/index.ts +++ b/src/components/widgets/index.ts @@ -45,5 +45,6 @@ export {default as LoadingData} from "./LoadingData" export * from "./ExternalLinkIndication" export {default as ExternalLinkIndication} from "./ExternalLinkIndication" export {default as ExtensionSignalHandler} from "./ExtensionalSignalHandler" +export * from "./StringPoolField" export * as SimplePageBuilder from "./simple-page-builder" From 8f925072ccd1e3aed4bd3378b71720c2dd93d8e5 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Feb 2023 20:17:12 +0100 Subject: [PATCH 13/30] fix: Fix default settings loaded correctly for global settings form --- src/constants/admin-settings.ts | 14 ++++++++++++++ src/routes/GlobalSettingsRoute.tsx | 20 ++++++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 src/constants/admin-settings.ts diff --git a/src/constants/admin-settings.ts b/src/constants/admin-settings.ts new file mode 100644 index 0000000..5d340a0 --- /dev/null +++ b/src/constants/admin-settings.ts @@ -0,0 +1,14 @@ +import {AdminSettings} from "~/server-types" + +export const DEFAULT_ADMIN_SETTINGS: AdminSettings = { + randomEmailIdMinLength: 6, + randomEmailIdChars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + randomEmailLengthIncreaseOnPercentage: 0.0005, + customEmailSuffixLength: 4, + customEmailSuffixChars: "0123456789", + userEmailEnableOtherRelays: true, + userEmailEnableDisposableEmails: false, + imageProxyStorageLifeTimeInHours: 24, + enableImageProxy: true, + allowStatistics: true, +} diff --git a/src/routes/GlobalSettingsRoute.tsx b/src/routes/GlobalSettingsRoute.tsx index 84b5e57..5387e25 100644 --- a/src/routes/GlobalSettingsRoute.tsx +++ b/src/routes/GlobalSettingsRoute.tsx @@ -1,15 +1,23 @@ import {ReactElement} from "react" -import {useQuery} from "@tanstack/react-query" -import {AdminSettings} from "~/server-types" import {AxiosError} from "axios" +import _ from "lodash" + +import {useQuery} from "@tanstack/react-query" + +import {AdminSettings} from "~/server-types" import {getAdminSettings} from "~/apis" import {QueryResult} from "~/components" -import {useTranslation} from "react-i18next" -import SettingsForm from "~/route-widgets/GlobalSettingsRoute/SettingForm" +import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings" +import SettingsForm from "~/route-widgets/GlobalSettingsRoute/SettingsForm" export default function GlobalSettingsRoute(): ReactElement { - const {t} = useTranslation() - const query = useQuery(["get_admin_settings"], getAdminSettings) + const query = useQuery(["get_admin_settings"], async () => { + const settings = getAdminSettings() + + return _.mergeWith({}, DEFAULT_ADMIN_SETTINGS, settings, (o, s) => + _.isNull(s) ? o : s, + ) as AdminSettings + }) return ( query={query}> From 8583adefca66f60c94383e593aad103ee39b116b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Feb 2023 20:22:43 +0100 Subject: [PATCH 14/30] feat: Add title for StringPoolField values --- src/components/widgets/StringPoolField/StringPoolField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/widgets/StringPoolField/StringPoolField.tsx b/src/components/widgets/StringPoolField/StringPoolField.tsx index ecc2e69..b3f91c0 100644 --- a/src/components/widgets/StringPoolField/StringPoolField.tsx +++ b/src/components/widgets/StringPoolField/StringPoolField.tsx @@ -134,7 +134,7 @@ export default function StringPoolField({ {...props} > {Object.entries(pools).map(([poolValue, label]) => ( - + From 6eac16630416d6bca6bc2673d4a6d6d42f19dda0 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Feb 2023 20:22:51 +0100 Subject: [PATCH 15/30] feat: improved SettingsForm --- .../GlobalSettingsRoute/SettingForm.tsx | 92 ----- .../GlobalSettingsRoute/SettingsForm.tsx | 363 ++++++++++++++++++ 2 files changed, 363 insertions(+), 92 deletions(-) delete mode 100644 src/route-widgets/GlobalSettingsRoute/SettingForm.tsx create mode 100644 src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx diff --git a/src/route-widgets/GlobalSettingsRoute/SettingForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingForm.tsx deleted file mode 100644 index 4b643c1..0000000 --- a/src/route-widgets/GlobalSettingsRoute/SettingForm.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as yup from "yup" -import {AdminSettings} from "~/server-types" -import {useTranslation} from "react-i18next" -import {useFormik} from "formik" -import {SimpleForm} from "~/components" -import {TextField} from "@mui/material" - -export interface SettingsFormProps { - settings: AdminSettings -} - -export default function SettingsForm({settings}: SettingsFormProps) { - const {t} = useTranslation() - - const validationSchema = yup.object().shape({ - randomEmailIdMinLength: yup - .number() - .min(1) - .max(1_023) - .label(t("routes.AdminRoute.forms.settings.randomEmailIdMinLength.label")), - randomEmailIdChars: yup - .string() - .label(t("routes.AdminRoute.forms.settings.randomEmailIdChars.label")), - randomEmailLengthIncreaseOnPercentage: yup - .number() - .label( - t("routes.AdminRoute.forms.settings.randomEmailLengthIncreaseOnPercentage.label"), - ), - imageProxyStorageLifeTimeInHours: yup - .number() - .label(t("routes.AdminRoute.forms.settings.imageProxyStorageLifeTimeInHours.label")), - customEmailSuffixLength: yup - .number() - .min(1) - .max(1_023) - .label(t("routes.AdminRoute.forms.settings.customEmailSuffixLength-label")), - customEmailSuffixChars: yup - .string() - .label(t("routes.AdminRoute.forms.settings.customEmailSuffixChars.label")), - userEmailEnableDisposableEmails: yup - .boolean() - .label(t("routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.label")), - userEmailEnableOtherRelays: yup - .boolean() - .label(t("routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.label")), - enableImageProxy: yup - .boolean() - .label(t("routes.AdminRoute.forms.settings.enableImageProxy.label")), - allowStatistics: yup - .boolean() - .label(t("routes.AdminRoute.forms.settings.allowStatistics.label")), - }) - - const formik = useFormik({ - validationSchema, - onSubmit: console.log, - initialValues: settings, - }) - - return ( -
- - {[ - , - ]} - -
- ) -} diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx new file mode 100644 index 0000000..d89ab24 --- /dev/null +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -0,0 +1,363 @@ +import * as yup from "yup" +import {useFormik} from "formik" +import {TbCursorText} from "react-icons/tb" +import {useTranslation} from "react-i18next" + +import { + Checkbox, + FormControlLabel, + FormGroup, + FormHelperText, + InputAdornment, + TextField, +} from "@mui/material" + +import {AdminSettings} from "~/server-types" +import {SimpleForm, StringPoolField, createPool} from "~/components" +import {MdOutlineChangeCircle, MdTextFormat} from "react-icons/md" +import {BsImage} from "react-icons/bs" + +export interface SettingsFormProps { + settings: AdminSettings +} + +const DEFAULT_POOLS = createPool({ + abcdefghijklmnopqrstuvwxyz: "a-z", + ABCDEFGHIJKLMNOPQRSTUVWXYZ: "A-Z", + "0123456789": "0-9", +}) + +export default function SettingsForm({settings}: SettingsFormProps) { + const {t} = useTranslation() + + const validationSchema = yup.object().shape({ + randomEmailIdMinLength: yup + .number() + .min(1) + .max(1_023) + .label(t("routes.AdminRoute.forms.settings.randomEmailIdMinLength.label")), + randomEmailIdChars: yup + .string() + .label(t("routes.AdminRoute.forms.settings.randomEmailIdChars.label")), + randomEmailLengthIncreaseOnPercentage: yup + .number() + .min(0) + .max(1) + .label( + t("routes.AdminRoute.forms.settings.randomEmailLengthIncreaseOnPercentage.label"), + ), + imageProxyStorageLifeTimeInHours: yup + .number() + .label(t("routes.AdminRoute.forms.settings.imageProxyStorageLifeTimeInHours.label")), + customEmailSuffixLength: yup + .number() + .min(1) + .max(1_023) + .label(t("routes.AdminRoute.forms.settings.customEmailSuffixLength-label")), + customEmailSuffixChars: yup + .string() + .label(t("routes.AdminRoute.forms.settings.customEmailSuffixChars.label")), + userEmailEnableDisposableEmails: yup + .boolean() + .label(t("routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.label")), + userEmailEnableOtherRelays: yup + .boolean() + .label(t("routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.label")), + enableImageProxy: yup + .boolean() + .label(t("routes.AdminRoute.forms.settings.enableImageProxy.label")), + allowStatistics: yup + .boolean() + .label(t("routes.AdminRoute.forms.settings.allowStatistics.label")), + }) + + const formik = useFormik({ + validationSchema, + onSubmit: console.log, + initialValues: settings, + }) + + return ( +
+ + {[ + + + + ), + }} + />, + + + + } + />, + + + + ), + }} + />, + + + + ), + }} + />, + + + + ), + }} + />, + + + + ), + }} + />, + + + } + label={t("routes.AdminRoute.forms.settings.enableImageProxy.label")} + /> + + {(formik.touched.enableImageProxy && formik.errors.enableImageProxy) || + t("routes.AdminRoute.forms.settings.enableImageProxy.description")} + + , + + + } + label={t( + "routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.label", + )} + /> + + {(formik.touched.userEmailEnableDisposableEmails && + formik.errors.userEmailEnableDisposableEmails) || + t( + "routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.description", + )} + + , + + + } + label={t( + "routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.label", + )} + /> + + {(formik.touched.userEmailEnableOtherRelays && + formik.errors.userEmailEnableOtherRelays) || + t( + "routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.description", + )} + + , + + + } + label={t("routes.AdminRoute.forms.settings.allowStatistics.label")} + /> + + {(formik.touched.allowStatistics && formik.errors.allowStatistics) || + t("routes.AdminRoute.forms.settings.allowStatistics.description")} + + , + ]} + +
+ ) +} From c1edcd290dd70bfb0c83112539e6129a5982d8a5 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Feb 2023 20:35:53 +0100 Subject: [PATCH 16/30] fix: Improve SettingsForm UI --- .../GlobalSettingsRoute/SettingsForm.tsx | 630 ++++++++++-------- 1 file changed, 352 insertions(+), 278 deletions(-) diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx index d89ab24..db9faa9 100644 --- a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -2,20 +2,23 @@ import * as yup from "yup" import {useFormik} from "formik" import {TbCursorText} from "react-icons/tb" import {useTranslation} from "react-i18next" +import {MdCheck, MdOutlineChangeCircle, MdTextFormat} from "react-icons/md" +import {BsImage} from "react-icons/bs" import { Checkbox, FormControlLabel, FormGroup, FormHelperText, + Grid, InputAdornment, TextField, + Typography, } from "@mui/material" +import {LoadingButton} from "@mui/lab" import {AdminSettings} from "~/server-types" -import {SimpleForm, StringPoolField, createPool} from "~/components" -import {MdOutlineChangeCircle, MdTextFormat} from "react-icons/md" -import {BsImage} from "react-icons/bs" +import {StringPoolField, createPool} from "~/components" export interface SettingsFormProps { settings: AdminSettings @@ -79,285 +82,356 @@ export default function SettingsForm({settings}: SettingsFormProps) { return (
- - {[ - - - - ), - }} - />, - - - - } - />, - - - - ), - }} - />, - - - - ), - }} - />, - - - - ), - }} - />, - - - - ), - }} - />, - - - } - label={t("routes.AdminRoute.forms.settings.enableImageProxy.label")} - /> - - {(formik.touched.enableImageProxy && formik.errors.enableImageProxy) || - t("routes.AdminRoute.forms.settings.enableImageProxy.description")} - - , - - - } - label={t( - "routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.label", - )} - /> - - {(formik.touched.userEmailEnableDisposableEmails && - formik.errors.userEmailEnableDisposableEmails) || - t( - "routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.description", + + + + + {t("routes.AdminRoute.settings.title")} + + + + + {t("routes.AdminRoute.settings.description")} + + + + + + + + - , - - - } - label={t( - "routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.label", - )} - /> - - {(formik.touched.userEmailEnableOtherRelays && - formik.errors.userEmailEnableOtherRelays) || - t( - "routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.description", + name="randomEmailIdMinLength" + value={formik.values.randomEmailIdMinLength} + onChange={formik.handleChange} + error={ + formik.touched.randomEmailIdMinLength && + Boolean(formik.errors.randomEmailIdMinLength) + } + helperText={ + (formik.touched.randomEmailIdMinLength && + formik.errors.randomEmailIdMinLength) || + t( + "routes.AdminRoute.forms.settings.randomEmailIdMinLength.description", + ) + } + type="number" + disabled={formik.isSubmitting} + inputMode="numeric" + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + - , - - + + + } + /> + + + + + + ), + }} + /> + + + + + + ), + }} + /> + + + + + + } + /> + + + + + + ), + }} + /> + + + + + } + label={t( + "routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.label", + )} /> - } - label={t("routes.AdminRoute.forms.settings.allowStatistics.label")} - /> - - {(formik.touched.allowStatistics && formik.errors.allowStatistics) || - t("routes.AdminRoute.forms.settings.allowStatistics.description")} - - , - ]} - + + {(formik.touched.userEmailEnableDisposableEmails && + formik.errors.userEmailEnableDisposableEmails) || + t( + "routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.description", + )} + + + + + + + } + label={t( + "routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.label", + )} + /> + + {(formik.touched.userEmailEnableOtherRelays && + formik.errors.userEmailEnableOtherRelays) || + t( + "routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.description", + )} + + + + + + + } + label={t( + "routes.AdminRoute.forms.settings.enableImageProxy.label", + )} + /> + + {(formik.touched.enableImageProxy && + formik.errors.enableImageProxy) || + t( + "routes.AdminRoute.forms.settings.enableImageProxy.description", + )} + + + + + + + } + label={t( + "routes.AdminRoute.forms.settings.allowStatistics.label", + )} + /> + + {(formik.touched.allowStatistics && + formik.errors.allowStatistics) || + t( + "routes.AdminRoute.forms.settings.allowStatistics.description", + )} + + + + + + + + + } + > + {t("general.saveLabel")} + + + + +
) } From db072002fe7f2cc3e0566822aa1e044396811a82 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Feb 2023 20:51:22 +0100 Subject: [PATCH 17/30] feat: Add SettingsForm mutation handling --- public/locales/en-US/translation.json | 3 +- .../GlobalSettingsRoute/SettingsForm.tsx | 46 +++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index d1419ef..e069221 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -382,7 +382,8 @@ }, "settings": { "title": "Global Settings", - "description": "Configure global settings for your instance." + "description": "Configure global settings for your instance.", + "successMessage": "Settings have been saved successfully!" } } }, diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx index db9faa9..f32288a 100644 --- a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -4,6 +4,7 @@ import {TbCursorText} from "react-icons/tb" import {useTranslation} from "react-i18next" import {MdCheck, MdOutlineChangeCircle, MdTextFormat} from "react-icons/md" import {BsImage} from "react-icons/bs" +import {AxiosError} from "axios" import { Checkbox, @@ -16,12 +17,19 @@ import { Typography, } from "@mui/material" import {LoadingButton} from "@mui/lab" +import {useMutation} from "@tanstack/react-query" import {AdminSettings} from "~/server-types" import {StringPoolField, createPool} from "~/components" +import {updateAdminSettings} from "~/apis" +import {useErrorSuccessSnacks} from "~/hooks" +import {queryClient} from "~/constants/react-query" +import {parseFastAPIError} from "~/utils" +import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings" export interface SettingsFormProps { settings: AdminSettings + queryKey: readonly string[] } const DEFAULT_POOLS = createPool({ @@ -30,8 +38,9 @@ const DEFAULT_POOLS = createPool({ "0123456789": "0-9", }) -export default function SettingsForm({settings}: SettingsFormProps) { +export default function SettingsForm({settings, queryKey}: SettingsFormProps) { const {t} = useTranslation() + const {showSuccess, showError} = useErrorSuccessSnacks() const validationSchema = yup.object().shape({ randomEmailIdMinLength: yup @@ -74,9 +83,40 @@ export default function SettingsForm({settings}: SettingsFormProps) { .label(t("routes.AdminRoute.forms.settings.allowStatistics.label")), }) - const formik = useFormik({ + const {mutateAsync} = useMutation>( + async settings => { + // Set values to `null` that are their defaults + const strippedSettings = Object.fromEntries( + Object.entries(settings as AdminSettings).map(([key, value]) => { + if (value === DEFAULT_ADMIN_SETTINGS[key as keyof AdminSettings]) { + return [key, null] + } + + return [key, value] + }), + ) + + return updateAdminSettings(strippedSettings) + }, + { + onError: showError, + onSuccess: newSettings => { + showSuccess(t("routes.AdminRoute.settings.successMessage")) + + queryClient.setQueryData(queryKey, newSettings) + }, + }, + ) + + const formik = useFormik({ validationSchema, - onSubmit: console.log, + onSubmit: async (values, {setErrors}) => { + try { + await mutateAsync(values) + } catch (error) { + setErrors(parseFastAPIError(error as AxiosError)) + } + }, initialValues: settings, }) From ff18b33aaf3a61ba13a9e0f629d114d3d90c1cd9 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 11 Feb 2023 19:37:39 +0100 Subject: [PATCH 18/30] feat: Add Preview for admin SettingsForm.tsx --- public/locales/en-US/translation.json | 7 ++- src/App.tsx | 1 + .../AliasPercentageAmount.tsx | 32 ++++++++++ .../RandomAliasGenerator.tsx | 59 +++++++++++++++++++ .../GlobalSettingsRoute/SettingsForm.tsx | 15 +++++ 5 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 src/route-widgets/GlobalSettingsRoute/AliasPercentageAmount.tsx create mode 100644 src/route-widgets/GlobalSettingsRoute/RandomAliasGenerator.tsx diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index e069221..141d829 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -383,7 +383,12 @@ "settings": { "title": "Global Settings", "description": "Configure global settings for your instance.", - "successMessage": "Settings have been saved successfully!" + "successMessage": "Settings have been saved successfully!", + "randomAliasesPreview": { + "title": "Random aliases will look like this", + "helperText": "This is just a preview. Those are not real aliases." + }, + "randomAliasesIncreaseExplanation": "Random aliases' length will be increased from {{originalLength}} to {{increasedLength}} characters after {{amount}} aliases have been created." } } }, diff --git a/src/App.tsx b/src/App.tsx index 0e4e335..75f1d89 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -122,6 +122,7 @@ const router = createBrowserRouter([ }, { path: "/admin/settings", + loader: getServerSettings, element: , }, ], diff --git a/src/route-widgets/GlobalSettingsRoute/AliasPercentageAmount.tsx b/src/route-widgets/GlobalSettingsRoute/AliasPercentageAmount.tsx new file mode 100644 index 0000000..117a1da --- /dev/null +++ b/src/route-widgets/GlobalSettingsRoute/AliasPercentageAmount.tsx @@ -0,0 +1,32 @@ +import {ReactElement} from "react" +import {useTranslation} from "react-i18next" + +import {Alert, Typography} from "@mui/material" + +export interface AliasPercentageAmountProps { + characters: string + length: number + percentage: number +} + +export default function AliasesPercentageAmount({ + characters, + length, + percentage, +}: AliasPercentageAmountProps): ReactElement { + const {t} = useTranslation() + + const amount = Math.floor(Math.pow(characters.length, length) * percentage) + + return ( + + + {t("routes.AdminRoute.settings.randomAliasesIncreaseExplanation", { + originalLength: length, + increasedLength: length + 1, + amount, + })} + + + ) +} diff --git a/src/route-widgets/GlobalSettingsRoute/RandomAliasGenerator.tsx b/src/route-widgets/GlobalSettingsRoute/RandomAliasGenerator.tsx new file mode 100644 index 0000000..37cb4b6 --- /dev/null +++ b/src/route-widgets/GlobalSettingsRoute/RandomAliasGenerator.tsx @@ -0,0 +1,59 @@ +import {useLoaderData} from "react-router-dom" +import {ReactElement, useCallback, useState} from "react" +import {useUpdateEffect} from "react-use" +import {BiRefresh} from "react-icons/bi" +import {useTranslation} from "react-i18next" + +import {Alert, FormHelperText, Grid, IconButton, Typography, useTheme} from "@mui/material" + +import {ServerSettings} from "~/server-types" + +export interface RandomAliasGeneratorProps { + characters: string + length: number +} + +export default function RandomAliasGenerator({ + characters, + length, +}: RandomAliasGeneratorProps): ReactElement { + const serverSettings = useLoaderData() as ServerSettings + const {t} = useTranslation() + const theme = useTheme() + + const generateLocal = useCallback( + () => + Array.from({length}, () => + characters.charAt(Math.floor(Math.random() * characters.length)), + ).join(""), + [characters, length], + ) + const [local, setLocal] = useState(generateLocal) + + const email = `${local}@${serverSettings.mailDomain}` + + useUpdateEffect(() => { + setLocal(generateLocal()) + }, [generateLocal]) + + return ( + + + {t("routes.AdminRoute.settings.randomAliasesPreview.title")} + + + + {email} + + + setLocal(generateLocal())}> + + + + + + {t("routes.AdminRoute.settings.randomAliasesPreview.helperText")} + + + ) +} diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx index f32288a..af5c423 100644 --- a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -26,6 +26,8 @@ import {useErrorSuccessSnacks} from "~/hooks" import {queryClient} from "~/constants/react-query" import {parseFastAPIError} from "~/utils" import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings" +import AliasesPercentageAmount from "./AliasPercentageAmount" +import RandomAliasGenerator from "~/route-widgets/GlobalSettingsRoute/RandomAliasGenerator" export interface SettingsFormProps { settings: AdminSettings @@ -212,6 +214,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { } />
+ + + + + + Date: Sat, 11 Feb 2023 19:42:37 +0100 Subject: [PATCH 19/30] feat: Show hour unit on image proxy storage lifetime field --- public/locales/en-US/translation.json | 4 +++- .../GlobalSettingsRoute/SettingsForm.tsx | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index 141d829..3a98730 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -360,7 +360,9 @@ }, "imageProxyStorageLifeTimeInHours": { "label": "Image proxy storage lifetime", - "description": "The lifetime of images that are stored on the server. After this time, the image will be deleted." + "description": "The lifetime of images that are stored on the server in hours. After this time, the image will be deleted.", + "unit_one": "hour", + "unit_other": "hours" }, "enableImageProxy": { "label": "Enable image proxy", diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx index af5c423..44a9d70 100644 --- a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -355,6 +355,18 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { ), + endAdornment: ( + + {t( + "routes.AdminRoute.forms.settings.imageProxyStorageLifeTimeInHours.unit", + { + count: + formik.values + .imageProxyStorageLifeTimeInHours || 0, + }, + )} + + ), }} /> From a29fe864d3e0d4718bef9a33cf8e006927494050 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 11 Feb 2023 19:45:40 +0100 Subject: [PATCH 20/30] fix: Pass querykey --- src/routes/GlobalSettingsRoute.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/GlobalSettingsRoute.tsx b/src/routes/GlobalSettingsRoute.tsx index 5387e25..2b841db 100644 --- a/src/routes/GlobalSettingsRoute.tsx +++ b/src/routes/GlobalSettingsRoute.tsx @@ -11,7 +11,8 @@ import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings" import SettingsForm from "~/route-widgets/GlobalSettingsRoute/SettingsForm" export default function GlobalSettingsRoute(): ReactElement { - const query = useQuery(["get_admin_settings"], async () => { + const queryKey = ["get_admin_settings"] + const query = useQuery(queryKey, async () => { const settings = getAdminSettings() return _.mergeWith({}, DEFAULT_ADMIN_SETTINGS, settings, (o, s) => @@ -21,7 +22,7 @@ export default function GlobalSettingsRoute(): ReactElement { return ( query={query}> - {settings => } + {settings => } ) } From 6dd49d659e3ea4f682d6e29c23966b1382d6b179 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 11 Feb 2023 20:04:24 +0100 Subject: [PATCH 21/30] feat: Add reset functionality --- public/locales/en-US/translation.json | 3 +- .../GlobalSettingsRoute/SettingsForm.tsx | 41 +++++++++++++------ src/server-types.ts | 14 +++---- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index 3a98730..bd8b9d4 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -390,7 +390,8 @@ "title": "Random aliases will look like this", "helperText": "This is just a preview. Those are not real aliases." }, - "randomAliasesIncreaseExplanation": "Random aliases' length will be increased from {{originalLength}} to {{increasedLength}} characters after {{amount}} aliases have been created." + "randomAliasesIncreaseExplanation": "Random aliases' length will be increased from {{originalLength}} to {{increasedLength}} characters after {{amount}} aliases have been created.", + "resetLabel": "Reset to defaults" } } }, diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx index 44a9d70..d99b4d9 100644 --- a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -2,7 +2,7 @@ import * as yup from "yup" import {useFormik} from "formik" import {TbCursorText} from "react-icons/tb" import {useTranslation} from "react-i18next" -import {MdCheck, MdOutlineChangeCircle, MdTextFormat} from "react-icons/md" +import {MdCheck, MdClear, MdOutlineChangeCircle, MdTextFormat} from "react-icons/md" import {BsImage} from "react-icons/bs" import {AxiosError} from "axios" @@ -122,6 +122,8 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { initialValues: settings, }) + // Fields will either have a value or be filled from the default values. + // That means we will never have a `null` value. return (
@@ -255,9 +257,9 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { @@ -304,7 +306,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { "routes.AdminRoute.forms.settings.customEmailSuffixChars.label", )} name="customEmailSuffixChars" - value={formik.values.customEmailSuffixChars} + value={formik.values.customEmailSuffixChars!} onChange={formik.handleChange} error={ formik.touched.customEmailSuffixChars && @@ -403,7 +405,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { @@ -431,7 +433,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { @@ -459,7 +461,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { @@ -485,7 +487,22 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { - + + + } + color="warning" + onClick={() => { + formik.setValues(DEFAULT_ADMIN_SETTINGS) + formik.submitForm() + }} + > + {t("routes.AdminRoute.forms.settings.resetLabel")} + + Date: Sat, 11 Feb 2023 20:10:58 +0100 Subject: [PATCH 22/30] fix: field name --- src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx index d99b4d9..f9269c1 100644 --- a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -229,7 +229,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { label={t( "routes.AdminRoute.forms.settings.randomEmailLengthIncreaseOnPercentage.label", )} - name="randomEmailIdLengthIncreaseOnPercentage" + name="randomEmailLengthIncreaseOnPercentage" value={formik.values.randomEmailLengthIncreaseOnPercentage} onChange={formik.handleChange} error={ From 5e456205e961a2306defa521506edb71a22d9818 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 11 Feb 2023 20:30:34 +0100 Subject: [PATCH 23/30] fix: translations --- src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx index f9269c1..4cad614 100644 --- a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -500,7 +500,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { formik.submitForm() }} > - {t("routes.AdminRoute.forms.settings.resetLabel")} + {t("routes.AdminRoute.settings.resetLabel")} From f0ecace4756927e1cd93dfbb8ee1d77fa3e0b043 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 11 Feb 2023 20:31:30 +0100 Subject: [PATCH 24/30] fix: Checkbox will be disabled when form submitted --- src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx index 4cad614..5207c2f 100644 --- a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -382,6 +382,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { name="userEmailEnableDisposableEmails" /> } + disabled={formik.isSubmitting} label={t( "routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.label", )} @@ -410,6 +411,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { name="userEmailEnableOtherRelays" /> } + disabled={formik.isSubmitting} label={t( "routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.label", )} @@ -438,6 +440,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { name="enableImageProxy" /> } + disabled={formik.isSubmitting} label={t( "routes.AdminRoute.forms.settings.enableImageProxy.label", )} @@ -466,6 +469,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { name="allowStatistics" /> } + disabled={formik.isSubmitting} label={t( "routes.AdminRoute.forms.settings.allowStatistics.label", )} From b405a0817faba70adc8e2f5c99364b303fc49f3a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 12 Feb 2023 16:21:34 +0100 Subject: [PATCH 25/30] fix: Only show admin Page when user is admin; Remove Overview section --- src/routes/AuthenticatedRoute.tsx | 14 ++++++++------ src/routes/VerifyEmailRoute.tsx | 1 - 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/routes/AuthenticatedRoute.tsx b/src/routes/AuthenticatedRoute.tsx index c7ac98a..3298168 100644 --- a/src/routes/AuthenticatedRoute.tsx +++ b/src/routes/AuthenticatedRoute.tsx @@ -11,15 +11,17 @@ import NavigationButton, { NavigationSection, } from "~/route-widgets/AuthenticateRoute/NavigationButton" -const sections = (Object.keys(NavigationSection) as Array).filter( - value => isNaN(Number(value)), -) - export default function AuthenticatedRoute(): ReactElement { const {t} = useTranslation() const theme = useTheme() + const user = useUser() - useUser() + const sections = [ + NavigationSection.Aliases, + NavigationSection.Reports, + NavigationSection.Settings, + user.isAdmin && NavigationSection.Admin, + ].filter(value => value !== false) as NavigationSection[] return ( @@ -50,7 +52,7 @@ export default function AuthenticatedRoute(): ReactElement { {sections.map(key => ( - + ))} diff --git a/src/routes/VerifyEmailRoute.tsx b/src/routes/VerifyEmailRoute.tsx index e143295..b5a8786 100644 --- a/src/routes/VerifyEmailRoute.tsx +++ b/src/routes/VerifyEmailRoute.tsx @@ -45,7 +45,6 @@ export default function VerifyEmailRoute(): ReactElement { verifyEmail, { onSuccess: ({user}) => { - setEmail("") login(user) navigate("/auth/complete-account") }, From bf489d587b92aba743ad65182c803d3f0927ce1f Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 12 Feb 2023 16:21:41 +0100 Subject: [PATCH 26/30] fix: Navigate to /aliases by default --- src/hooks/use-navigate-to-next.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/use-navigate-to-next.ts b/src/hooks/use-navigate-to-next.ts index 5dc3cdf..0346133 100644 --- a/src/hooks/use-navigate-to-next.ts +++ b/src/hooks/use-navigate-to-next.ts @@ -3,7 +3,7 @@ import {useCallback} from "react" import {getNextUrl} from "~/utils" -export default function useNavigateToNext(defaultNextUrl = "/"): () => void { +export default function useNavigateToNext(defaultNextUrl = "/aliases"): () => void { const navigate = useNavigate() const location = useLocation() From 9a24e6b3e4eb24c648e5376d5582430c813b9d3d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 12 Feb 2023 16:22:05 +0100 Subject: [PATCH 27/30] fix: Navigate to /aliases after login --- src/routes/LoginRoute.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/LoginRoute.tsx b/src/routes/LoginRoute.tsx index 36a15cc..2016bb6 100644 --- a/src/routes/LoginRoute.tsx +++ b/src/routes/LoginRoute.tsx @@ -25,7 +25,7 @@ export default function LoginRoute(): ReactElement { if (user?.encryptedPassword) { navigate("/enter-password") } else { - navigate("/") + navigate("/aliases") } }, [user, navigate]) From 30ccdbce1323653a438004d8a000ad2c286886b6 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 12 Feb 2023 16:27:26 +0100 Subject: [PATCH 28/30] fix: Clean up user on sign up page when logged in --- src/components/AuthContext/AuthContextProvider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/AuthContext/AuthContextProvider.tsx b/src/components/AuthContext/AuthContextProvider.tsx index 777e493..9ca2205 100644 --- a/src/components/AuthContext/AuthContextProvider.tsx +++ b/src/components/AuthContext/AuthContextProvider.tsx @@ -35,6 +35,7 @@ export default function AuthContextProvider({children}: AuthContextProviderProps user as User, ) const logout = useCallback(() => { + localStorage.removeItem("signup-form-state-email") logoutMasterPassword() setUser(null) }, [logoutMasterPassword]) From 652837838b1c659d09db21baaa9f8d4207c9c426 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 12 Feb 2023 16:38:56 +0100 Subject: [PATCH 29/30] feat: Add EmptyStateScreen.tsx for reserved aliases --- public/locales/en-US/translation.json | 6 +- .../ReservedAliasesRoute/EmptyStateScreen.tsx | 37 ++++++++++ src/routes/ReservedAliasesRoute.tsx | 71 +++++++++++-------- 3 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 src/route-widgets/ReservedAliasesRoute/EmptyStateScreen.tsx diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index bd8b9d4..f67df95 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -295,7 +295,11 @@ } }, "userAmount_one": "Forwards to one user", - "userAmount_other": "Forwards to {{count}} users" + "userAmount_other": "Forwards to {{count}} users", + "emptyState": { + "title": "Create your first reserved alias", + "description": "Reserved aliases are aliases that will be forwarded to selected admin users. This is useful if you want to create aliases that are meant to be public, like contact@example.com or hello@example.com." + } }, "ReservedAliasDetailRoute": { "title": "Reserved Alias Details", diff --git a/src/route-widgets/ReservedAliasesRoute/EmptyStateScreen.tsx b/src/route-widgets/ReservedAliasesRoute/EmptyStateScreen.tsx new file mode 100644 index 0000000..ee300e9 --- /dev/null +++ b/src/route-widgets/ReservedAliasesRoute/EmptyStateScreen.tsx @@ -0,0 +1,37 @@ +import {ReactElement} from "react" +import {useTranslation} from "react-i18next" + +import {Container, Grid, Typography} from "@mui/material" +import {BsStarFill} from "react-icons/bs" + +export default function EmptyStateScreen(): ReactElement { + const {t} = useTranslation() + + return ( + + + + + {t("routes.ReservedAliasesRoute.emptyState.title")} + + + + + + + + {t("routes.ReservedAliasesRoute.emptyState.description")} + + + + + ) +} diff --git a/src/routes/ReservedAliasesRoute.tsx b/src/routes/ReservedAliasesRoute.tsx index 493f141..4b6ca4a 100644 --- a/src/routes/ReservedAliasesRoute.tsx +++ b/src/routes/ReservedAliasesRoute.tsx @@ -18,7 +18,8 @@ import {useQuery} from "@tanstack/react-query" import {PaginationResult, ReservedAlias} from "~/server-types" import {getReservedAliases} from "~/apis" -import {QueryResult, SimplePage} from "~/components" +import {NoSearchResults, QueryResult, SimplePage} from "~/components" +import EmptyStateScreen from "~/route-widgets/ReservedAliasesRoute/EmptyStateScreen" export default function ReservedAliasesRoute(): ReactElement { const {t} = useTranslation() @@ -33,8 +34,10 @@ export default function ReservedAliasesRoute(): ReactElement { query: queryValue, }), { - onSuccess: () => { - setShowSearch(true) + onSuccess: ({items}) => { + if (items.length) { + setShowSearch(true) + } }, }, ) @@ -81,32 +84,42 @@ export default function ReservedAliasesRoute(): ReactElement { } > , AxiosError> query={query}> - {({items: aliases}) => ( - - {aliases.map(alias => ( - - - {alias.local} - @{alias.domain} - - } - secondary={t("routes.ReservedAliasesRoute.userAmount", { - count: alias.users.length, - })} - /> - - - - - ))} - - )} + {({items: aliases}) => { + if (aliases.length === 0) { + if (searchValue === "") { + return + } else { + return + } + } + + return ( + + {aliases.map(alias => ( + + + {alias.local} + @{alias.domain} + + } + secondary={t("routes.ReservedAliasesRoute.userAmount", { + count: alias.users.length, + })} + /> + + + + + ))} + + ) + }} ) From 880f30ef879bef50bbbc80f8bb38b2cffe696949 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 12 Feb 2023 17:19:53 +0100 Subject: [PATCH 30/30] fix: User not being authenticated but on AuthenticatedRoute.tsx --- src/routes/AuthenticatedRoute.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/AuthenticatedRoute.tsx b/src/routes/AuthenticatedRoute.tsx index 3298168..f04ca7c 100644 --- a/src/routes/AuthenticatedRoute.tsx +++ b/src/routes/AuthenticatedRoute.tsx @@ -20,7 +20,7 @@ export default function AuthenticatedRoute(): ReactElement { NavigationSection.Aliases, NavigationSection.Reports, NavigationSection.Settings, - user.isAdmin && NavigationSection.Admin, + user?.isAdmin && NavigationSection.Admin, ].filter(value => value !== false) as NavigationSection[] return (