diff options
author | crupest <crupest@outlook.com> | 2020-11-17 17:31:42 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-11-17 17:31:42 +0800 |
commit | e51e3b7c3c987f52823798b749e6c6deb2bfbe38 (patch) | |
tree | 3ff6f99f7b87a79e3279b3bd7de1431894f87066 | |
parent | 711a148fbbf4361f9c0632eff973c2f240a67c5d (diff) | |
download | timeline-e51e3b7c3c987f52823798b749e6c6deb2bfbe38.tar.gz timeline-e51e3b7c3c987f52823798b749e6c6deb2bfbe38.tar.bz2 timeline-e51e3b7c3c987f52823798b749e6c6deb2bfbe38.zip |
...
-rw-r--r-- | FrontEnd/src/app/http/user.ts | 25 | ||||
-rw-r--r-- | FrontEnd/src/app/views/admin/AdminSubPage.tsx | 18 | ||||
-rw-r--r-- | FrontEnd/src/app/views/admin/UserAdmin.tsx | 352 | ||||
-rw-r--r-- | FrontEnd/src/app/views/common/OperationDialog.tsx | 31 |
4 files changed, 163 insertions, 263 deletions
diff --git a/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts index 92a6433e..243846d1 100644 --- a/FrontEnd/src/app/http/user.ts +++ b/FrontEnd/src/app/http/user.ts @@ -28,6 +28,8 @@ export interface HttpUser { } export interface HttpUserPatchRequest { + username?: string; + password?: string; nickname?: string; } @@ -36,6 +38,11 @@ export interface HttpChangePasswordRequest { newPassword: string; } +export interface HttpCreateUserRequest { + username: string; + password: string; +} + export class HttpUserNotExistError extends Error { constructor(public innerError?: AxiosError) { super(); @@ -56,6 +63,7 @@ export interface IHttpUserClient { req: HttpUserPatchRequest, token: string ): Promise<HttpUser>; + delete(username: string, token: string): Promise<void>; getAvatar(username: string): Promise<BlobWithEtag>; getAvatar( username: string, @@ -73,6 +81,8 @@ export interface IHttpUserClient { permission: UserPermission, token: string ): Promise<void>; + + createUser(req: HttpCreateUserRequest, token: string): Promise<HttpUser>; } export class HttpUserClient implements IHttpUserClient { @@ -102,6 +112,13 @@ export class HttpUserClient implements IHttpUserClient { .catch(convertToNetworkError); } + delete(username: string, token: string): Promise<void> { + return axios + .delete(`${apiBaseUrl}/users/${username}?token=${token}`) + .catch(convertToNetworkError) + .then(); + } + getAvatar(username: string): Promise<BlobWithEtag>; getAvatar( username: string, @@ -171,6 +188,14 @@ export class HttpUserClient implements IHttpUserClient { .catch(convertToNetworkError) .then(); } + + createUser(req: HttpCreateUserRequest, token: string): Promise<HttpUser> { + return axios + .post<HttpUser>(`${apiBaseUrl}/userop/createuser?token=${token}`, req) + .then(extractResponseData) + .catch(convertToNetworkError) + .then(); + } } let client: IHttpUserClient = new HttpUserClient(); diff --git a/FrontEnd/src/app/views/admin/AdminSubPage.tsx b/FrontEnd/src/app/views/admin/AdminSubPage.tsx index 5d2df13c..5efe1ee3 100644 --- a/FrontEnd/src/app/views/admin/AdminSubPage.tsx +++ b/FrontEnd/src/app/views/admin/AdminSubPage.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Nav } from "react-bootstrap"; +import { Container, Nav } from "react-bootstrap"; import { useHistory, useRouteMatch } from "react-router"; const AdminSubPage: React.FC = ({ children }) => { @@ -13,8 +13,8 @@ const AdminSubPage: React.FC = ({ children }) => { } return ( - <> - <Nav variant="tabs"> + <Container> + <Nav variant="tabs" className="my-2"> <Nav.Item> <Nav.Link active={name === "users"} @@ -25,19 +25,9 @@ const AdminSubPage: React.FC = ({ children }) => { Users </Nav.Link> </Nav.Item> - <Nav.Item> - <Nav.Link - active={name === "more"} - onClick={() => { - toggle("more"); - }} - > - More - </Nav.Link> - </Nav.Item> </Nav> {children} - </> + </Container> ); }; diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx index 856e6136..3432cddf 100644 --- a/FrontEnd/src/app/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from "react"; -import axios from "axios"; import { ListGroup, Row, @@ -13,77 +12,22 @@ import OperationDialog from "../common/OperationDialog"; import AdminSubPage from "./AdminSubPage"; import { User, AuthUser } from "@/services/user"; -import { getHttpUserClient } from "@/http/user"; +import { getHttpUserClient, HttpUser } from "@/http/user"; -const apiBaseUrl = "/api"; - -interface CreateUserInfo { - username: string; - password: string; -} - -async function createUser(user: CreateUserInfo, token: string): Promise<User> { - const res = await axios.post<User>( - `${apiBaseUrl}/userop/createuser?token=${token}`, - user - ); - return res.data; -} - -function deleteUser(username: string, token: string): Promise<void> { - return axios.delete(`${apiBaseUrl}/users/${username}?token=${token}`); -} - -function changeUsername( - oldUsername: string, - newUsername: string, - token: string -): Promise<void> { - return axios.patch(`${apiBaseUrl}/users/${oldUsername}?token=${token}`, { - username: newUsername, - }); -} - -function changePassword( - username: string, - newPassword: string, - token: string -): Promise<void> { - return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, { - password: newPassword, - }); -} - -const kChangeUsername = "changeusername"; -const kChangePassword = "changepassword"; -const kChangePermission = "changepermission"; +const kModify = "modify"; const kDelete = "delete"; -type TChangeUsername = typeof kChangeUsername; -type TChangePassword = typeof kChangePassword; -type TChangePermission = typeof kChangePermission; +type TModify = typeof kModify; type TDelete = typeof kDelete; -type ContextMenuItem = - | TChangeUsername - | TChangePassword - | TChangePermission - | TDelete; +type ContextMenuItem = TModify | TDelete; interface UserCardProps { - onContextMenu: (item: ContextMenuItem) => void; + on: { [key in ContextMenuItem]: () => void }; user: User; } -const UserItem: React.FC<UserCardProps> = (props) => { - const user = props.user; - - const createClickCallback = (item: ContextMenuItem): (() => void) => { - return () => { - props.onContextMenu(item); - }; - }; - +const UserItem: React.FC<UserCardProps> = ({ user, on }) => { return ( <ListGroup.Item className="container"> <Row className="align-items-center"> @@ -101,19 +45,8 @@ const UserItem: React.FC<UserCardProps> = (props) => { Manage </Dropdown.Toggle> <Dropdown.Menu> - <Dropdown.Item onClick={createClickCallback(kChangeUsername)}> - Change Username - </Dropdown.Item> - <Dropdown.Item onClick={createClickCallback(kChangePassword)}> - Change Password - </Dropdown.Item> - <Dropdown.Item onClick={createClickCallback(kChangePermission)}> - Change Permission - </Dropdown.Item> - <Dropdown.Item - className="text-danger" - onClick={createClickCallback(kDelete)} - > + <Dropdown.Item onClick={on["modify"]}>Modify</Dropdown.Item> + <Dropdown.Item className="text-danger" onClick={on["delete"]}> Delete </Dropdown.Item> </Dropdown.Menu> @@ -124,16 +57,20 @@ const UserItem: React.FC<UserCardProps> = (props) => { ); }; -interface DialogProps { +interface DialogProps<TData = undefined, TReturn = undefined> { open: boolean; close: () => void; + token: string; + data: TData; + onSuccess: (data: TReturn) => void; } -interface CreateUserDialogProps extends DialogProps { - process: (user: CreateUserInfo) => Promise<void>; -} - -const CreateUserDialog: React.FC<CreateUserDialogProps> = (props) => { +const CreateUserDialog: React.FC<DialogProps<undefined, HttpUser>> = ({ + open, + close, + token, + onSuccess, +}) => { return ( <OperationDialog title="Create" @@ -146,13 +83,17 @@ const CreateUserDialog: React.FC<CreateUserDialogProps> = (props) => { ] as const } onProcess={([username, password]) => - props.process({ - username: username, - password: password, - }) + getHttpUserClient().createUser( + { + username, + password, + }, + token + ) } - close={props.close} - open={props.open} + close={close} + open={open} + onSuccessAndClose={onSuccess} /> ); }; @@ -161,110 +102,64 @@ const UsernameLabel: React.FC = (props) => { return <span style={{ color: "blue" }}>{props.children}</span>; }; -interface UserDeleteDialogProps extends DialogProps { - username: string; - process: () => Promise<void>; -} - -const UserDeleteDialog: React.FC<UserDeleteDialogProps> = (props) => { +const UserDeleteDialog: React.FC<DialogProps< + { username: string }, + unknown +>> = ({ open, close, token, data: { username }, onSuccess }) => { return ( <OperationDialog - open={props.open} - close={props.close} + open={open} + close={close} title="Dangerous" titleColor="dangerous" inputPrompt={() => ( <> - {"You are deleting user "} - <UsernameLabel>{props.username}</UsernameLabel> - {" !"} + You are deleting user <UsernameLabel>{username}</UsernameLabel> ! </> )} - onProcess={props.process} + onProcess={() => getHttpUserClient().delete(username, token)} + onSuccessAndClose={onSuccess} /> ); }; -interface UserModifyDialogProps<T> extends DialogProps { - username: string; - process: (value: T) => Promise<void>; -} - -const UserChangeUsernameDialog: React.FC<UserModifyDialogProps<string>> = ( - props -) => { +const UserModifyDialog: React.FC<DialogProps< + { + oldUser: HttpUser; + }, + HttpUser +>> = ({ open, close, token, data: { oldUser }, onSuccess }) => { return ( <OperationDialog - open={props.open} - close={props.close} + open={open} + close={close} title="Caution" titleColor="dangerous" inputPrompt={() => ( <> - {"You are change the username of user "} - <UsernameLabel>{props.username}</UsernameLabel> - {" !"} + You are change the password of user + <UsernameLabel>{oldUser.username}</UsernameLabel> ! </> )} - inputScheme={[{ type: "text", label: "New Username" }]} - onProcess={([newUsername]) => { - return props.process(newUsername); - }} - /> - ); -}; - -const UserChangePasswordDialog: React.FC<UserModifyDialogProps<string>> = ( - props -) => { - return ( - <OperationDialog - open={props.open} - close={props.close} - title="Caution" - titleColor="dangerous" - inputPrompt={() => ( - <> - {"You are change the password of user "} - <UsernameLabel>{props.username}</UsernameLabel> - {" !"} - </> - )} - inputScheme={[{ type: "text", label: "New Password" }]} - onProcess={([newPassword]) => { - return props.process(newPassword); - }} - /> - ); -}; - -interface UserChangePermissionDialogProps extends DialogProps { - username: string; - newPermission: boolean; - process: () => Promise<void>; -} - -const UserChangePermissionDialog: React.FC<UserChangePermissionDialogProps> = ( - props -) => { - return ( - <OperationDialog - open={props.open} - close={props.close} - title="Caution" - titleColor="dangerous" - inputPrompt={() => ( - <> - {"You are change user "} - <UsernameLabel>{props.username}</UsernameLabel> - {" to "} - <span style={{ color: "orange" }}> - {props.newPermission ? "administrator" : "normal user"} - </span> - {" !"} - </> - )} - onProcess={props.process} + inputScheme={ + [ + { type: "text", label: "New Username", initValue: oldUser.username }, + { type: "text", label: "New Password" }, + { type: "text", label: "New Nickname", initValue: oldUser.nickname }, + ] as const + } + onProcess={([username, password, nickname]) => + getHttpUserClient().patch( + oldUser.username, + { + username: username !== oldUser.username ? username : undefined, + password: password !== "" ? password : undefined, + nickname: nickname !== oldUser.nickname ? nickname : undefined, + }, + token + ) + } + onSuccessAndClose={onSuccess} /> ); }; @@ -279,23 +174,18 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => { | { type: "create"; } - | { type: TDelete; username: string } | { - type: TChangeUsername; - username: string; + type: TModify; + user: HttpUser; } - | { - type: TChangePassword; - username: string; - } - | { - type: TChangePermission; - username: string; - newPermission: boolean; - }; + | { type: TDelete; username: string }; const [users, setUsers] = useState<User[] | null>(null); const [dialog, setDialog] = useState<DialogInfo>(null); + const [usersVersion, setUsersVersion] = useState<number>(0); + const updateUsers = (): void => { + setUsersVersion(usersVersion + 1); + }; const token = props.user.token; @@ -311,20 +201,19 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => { return () => { subscribe = false; }; - }, []); + }, [usersVersion]); let dialogNode: React.ReactNode; - if (dialog) + if (dialog) { switch (dialog.type) { case "create": dialogNode = ( <CreateUserDialog open close={() => setDialog(null)} - process={async (user) => { - const u = await createUser(user, token); - setUsers((oldUsers) => [...(oldUsers ?? []), u]); - }} + token={token} + data={undefined} + onSuccess={updateUsers} /> ); break; @@ -333,52 +222,25 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => { <UserDeleteDialog open close={() => setDialog(null)} - username={dialog.username} - process={async () => { - await deleteUser(dialog.username, token); - setUsers((oldUsers) => - (oldUsers ?? []).filter((u) => u.username !== dialog.username) - ); - }} - /> - ); - break; - case kChangeUsername: - dialogNode = ( - <UserChangeUsernameDialog - open - close={() => setDialog(null)} - username={dialog.username} - process={async (newUsername) => { - await changeUsername(dialog.username, newUsername, token); - setUsers((oldUsers) => { - const users = (oldUsers ?? []).slice(); - const findedUser = users.find( - (u) => u.username === dialog.username - ); - if (findedUser) findedUser.username = newUsername; - return users; - }); - }} + token={token} + data={{ username: dialog.username }} + onSuccess={updateUsers} /> ); break; - case kChangePassword: + case kModify: dialogNode = ( - <UserChangePasswordDialog + <UserModifyDialog open close={() => setDialog(null)} - username={dialog.username} - process={async (newPassword) => { - await changePassword(dialog.username, newPassword, token); - }} + token={token} + data={{ oldUser: dialog.user }} + onSuccess={updateUsers} /> ); break; - case kChangePermission: { - break; - } } + } if (users) { const userComponents = users.map((user) => { @@ -386,24 +248,40 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => { <UserItem key={user.username} user={user} - onContextMenu={(item) => {}} + on={{ + modify: () => { + setDialog({ + type: "modify", + user, + }); + }, + delete: () => { + setDialog({ + type: "delete", + username: user.username, + }); + }, + }} /> ); }); return ( <AdminSubPage> - <Button - variant="success" - onClick={() => - setDialog({ - type: "create", - }) - } - className="align-self-end" - > - Create User - </Button> + <Row className="justify-content-end my-2"> + <Col xs="auto"> + <Button + variant="success" + onClick={() => + setDialog({ + type: "create", + }) + } + > + Create User + </Button> + </Col> + </Row> {userComponents} {dialogNode} </AdminSubPage> diff --git a/FrontEnd/src/app/views/common/OperationDialog.tsx b/FrontEnd/src/app/views/common/OperationDialog.tsx index e32e9277..08baf93a 100644 --- a/FrontEnd/src/app/views/common/OperationDialog.tsx +++ b/FrontEnd/src/app/views/common/OperationDialog.tsx @@ -77,11 +77,6 @@ type MapOperationInputInfoValueTypeList< [Index in keyof Tuple]: MapOperationInputInfoValueType<Tuple[Index]>; } & { length: Tuple["length"] }; -interface OperationResult { - type: "success" | "failure"; - data: unknown; -} - export type OperationInputError = | { [index: number]: I18nText | null | undefined; @@ -98,6 +93,7 @@ const isNoError = (error: OperationInputError): boolean => { }; export interface OperationDialogProps< + TData, OperationInputInfoList extends readonly OperationInputInfo[] > { open: boolean; @@ -106,28 +102,39 @@ export interface OperationDialogProps< titleColor?: "default" | "dangerous" | "create" | string; onProcess: ( inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList> - ) => Promise<unknown>; + ) => Promise<TData>; inputScheme?: OperationInputInfoList; inputValidator?: ( inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList> ) => OperationInputError; inputPrompt?: I18nText | (() => React.ReactNode); processPrompt?: () => React.ReactNode; - successPrompt?: (data: unknown) => React.ReactNode; + successPrompt?: (data: TData) => React.ReactNode; failurePrompt?: (error: unknown) => React.ReactNode; - onSuccessAndClose?: () => void; + onSuccessAndClose?: (data: TData) => void; } const OperationDialog = < + TData, OperationInputInfoList extends readonly OperationInputInfo[] >( - props: OperationDialogProps<OperationInputInfoList> + props: OperationDialogProps<TData, OperationInputInfoList> ): React.ReactElement => { const inputScheme = props.inputScheme as readonly OperationInputInfo[]; const { t } = useTranslation(); - type Step = "input" | "process" | OperationResult; + type Step = + | "input" + | "process" + | { + type: "success"; + data: TData; + } + | { + type: "failure"; + data: unknown; + }; const [step, setStep] = useState<Step>("input"); const [values, setValues] = useState<(boolean | string)[]>( inputScheme.map((i) => { @@ -153,7 +160,7 @@ const OperationDialog = < step.type === "success" && props.onSuccessAndClose ) { - props.onSuccessAndClose(); + props.onSuccessAndClose(step.data); } } else { console.log("Attempt to close modal when processing."); @@ -169,7 +176,7 @@ const OperationDialog = < > ) .then( - (d: unknown) => { + (d) => { setStep({ type: "success", data: d, |