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] 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 => (
+
+ ))}
+
+ )
+}
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