feat: add member list in setting

This commit is contained in:
Steven
2023-07-09 00:45:26 +08:00
parent c00f7d0852
commit 5db3506cba
26 changed files with 614 additions and 238 deletions

View File

@@ -2,7 +2,7 @@ import { Button, Input, Modal, ModalDialog } from "@mui/joy";
import { useState } from "react";
import { toast } from "react-hot-toast";
import useLoading from "../hooks/useLoading";
import { userService } from "../services";
import useUserStore from "../stores/v1/user";
import Icon from "./Icon";
interface Props {
@@ -11,6 +11,7 @@ interface Props {
const ChangePasswordDialog: React.FC<Props> = (props: Props) => {
const { onClose } = props;
const userStore = useUserStore();
const [newPassword, setNewPassword] = useState("");
const [newPasswordAgain, setNewPasswordAgain] = useState("");
const requestState = useLoading(false);
@@ -43,9 +44,8 @@ const ChangePasswordDialog: React.FC<Props> = (props: Props) => {
requestState.setLoading();
try {
const user = userService.getState().user as User;
await userService.patchUser({
id: user.id,
userStore.patchUser({
id: userStore.getCurrentUser().id,
password: newPassword,
});
onClose();

View File

@@ -0,0 +1,201 @@
import { Button, Input, Modal, ModalDialog, Radio, RadioGroup } from "@mui/joy";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import useLoading from "../hooks/useLoading";
import useUserStore from "../stores/v1/user";
import Icon from "./Icon";
interface Props {
user?: User;
onClose: () => void;
onConfirm?: () => void;
}
interface State {
userCreate: UserCreate;
}
const roles: Role[] = ["USER", "ADMIN"];
const CreateUserDialog: React.FC<Props> = (props: Props) => {
const { onClose, onConfirm, user } = props;
const userStore = useUserStore();
const [state, setState] = useState<State>({
userCreate: {
email: "",
nickname: "",
password: "",
role: "USER",
},
});
const requestState = useLoading(false);
const isEditing = !!user;
useEffect(() => {
if (user) {
setState({
...state,
userCreate: Object.assign(state.userCreate, {
email: user.email,
nickname: user.nickname,
password: "",
role: user.role,
}),
});
}
}, [user]);
const setPartialState = (partialState: Partial<State>) => {
setState({
...state,
...partialState,
});
};
const handleEmailInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPartialState({
userCreate: Object.assign(state.userCreate, {
email: e.target.value.toLowerCase(),
}),
});
};
const handleNicknameInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPartialState({
userCreate: Object.assign(state.userCreate, {
nickname: e.target.value,
}),
});
};
const handlePasswordInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPartialState({
userCreate: Object.assign(state.userCreate, {
password: e.target.value,
}),
});
};
const handleRoleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPartialState({
userCreate: Object.assign(state.userCreate, {
visibility: e.target.value,
}),
});
};
const handleSaveBtnClick = async () => {
if (!state.userCreate.email) {
toast.error("Email is required");
return;
}
try {
if (user) {
const userPatch: UserPatch = {
id: user.id,
};
if (user.email !== state.userCreate.email) {
userPatch.email = state.userCreate.email;
}
if (user.nickname !== state.userCreate.nickname) {
userPatch.nickname = state.userCreate.nickname;
}
if (state.userCreate.password) {
userPatch.password = state.userCreate.password;
}
if (user.role !== state.userCreate.role) {
userPatch.role = state.userCreate.role;
}
await userStore.patchUser(userPatch);
} else {
await userStore.createUser(state.userCreate);
}
if (onConfirm) {
onConfirm();
} else {
onClose();
}
} catch (error: any) {
console.error(error);
toast.error(error.response.data.message);
}
};
return (
<Modal open={true}>
<ModalDialog>
<div className="flex flex-row justify-between items-center w-80 sm:w-96 mb-4">
<span className="text-lg font-medium">{isEditing ? "Edit User" : "Create User"}</span>
<Button variant="plain" onClick={onClose}>
<Icon.X className="w-5 h-auto text-gray-600" />
</Button>
</div>
<div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Email <span className="text-red-600">*</span>
</span>
<div className="relative w-full">
<Input
className="w-full"
type="email"
placeholder="Unique user email"
value={state.userCreate.email}
onChange={handleEmailInputChange}
/>
</div>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Nickname <span className="text-red-600">*</span>
</span>
<Input
className="w-full"
type="text"
placeholder="Nickname"
value={state.userCreate.nickname}
onChange={handleNicknameInputChange}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Password
{!isEditing && <span className="text-red-600"> *</span>}
</span>
<Input
className="w-full"
type="password"
placeholder=""
value={state.userCreate.password}
onChange={handlePasswordInputChange}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Role <span className="text-red-600">*</span>
</span>
<div className="w-full flex flex-row justify-start items-center text-base">
<RadioGroup orientation="horizontal" value={state.userCreate.role} onChange={handleRoleInputChange}>
{roles.map((role) => (
<Radio key={role} value={role} label={role} />
))}
</RadioGroup>
</div>
</div>
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
<Button color="neutral" variant="plain" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onClose}>
Cancel
</Button>
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
Save
</Button>
</div>
</div>
</ModalDialog>
</Modal>
);
};
export default CreateUserDialog;

View File

@@ -2,8 +2,7 @@ import { Button, Input, Modal, ModalDialog } from "@mui/joy";
import { useState } from "react";
import { toast } from "react-hot-toast";
import useLoading from "../hooks/useLoading";
import { userService } from "../services";
import { useAppSelector } from "../stores";
import useUserStore from "../stores/v1/user";
import Icon from "./Icon";
interface Props {
@@ -12,9 +11,10 @@ interface Props {
const EditUserinfoDialog: React.FC<Props> = (props: Props) => {
const { onClose } = props;
const user = useAppSelector((state) => state.user.user as User);
const [email, setEmail] = useState(user.email);
const [nickname, setNickname] = useState(user.nickname);
const userStore = useUserStore();
const currentUser = userStore.getCurrentUser();
const [email, setEmail] = useState(currentUser.email);
const [nickname, setNickname] = useState(currentUser.nickname);
const requestState = useLoading(false);
const handleCloseBtnClick = () => {
@@ -39,14 +39,13 @@ const EditUserinfoDialog: React.FC<Props> = (props: Props) => {
requestState.setLoading();
try {
const user = userService.getState().user as User;
await userService.patchUser({
id: user.id,
await userStore.patchUser({
id: currentUser.id,
email,
nickname,
});
onClose();
toast("Password changed");
toast("User information updated");
} catch (error: any) {
console.error(error);
toast.error(error.response.data.message);

View File

@@ -1,14 +1,14 @@
import { Avatar } from "@mui/joy";
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAppSelector } from "../stores";
import useUserStore from "../stores/v1/user";
import Icon from "./Icon";
import Dropdown from "./common/Dropdown";
import AboutDialog from "./AboutDialog";
const Header: React.FC = () => {
const navigate = useNavigate();
const user = useAppSelector((state) => state.user).user as User;
const currentUser = useUserStore().getCurrentUser();
const [showAboutDialog, setShowAboutDialog] = useState<boolean>(false);
const handleSignOutButtonClick = async () => {
@@ -30,7 +30,7 @@ const Header: React.FC = () => {
trigger={
<button className="flex flex-row justify-end items-center cursor-pointer">
<Avatar size="sm" variant="plain" />
<span>{user.nickname}</span>
<span>{currentUser.nickname}</span>
<Icon.ChevronDown className="ml-2 w-5 h-auto text-gray-600" />
</button>
}

View File

@@ -4,8 +4,8 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import toast from "react-hot-toast";
import { shortcutService } from "../services";
import { useAppSelector } from "../stores";
import useFaviconStore from "../stores/v1/favicon";
import useUserStore from "../stores/v1/user";
import { absolutifyLink } from "../helpers/utils";
import { showCommonDialog } from "./Alert";
import Icon from "./Icon";
@@ -21,11 +21,11 @@ interface Props {
const ShortcutView = (props: Props) => {
const { shortcut, handleEdit } = props;
const { t } = useTranslation();
const user = useAppSelector((state) => state.user.user as User);
const currentUser = useUserStore().getCurrentUser();
const faviconStore = useFaviconStore();
const [favicon, setFavicon] = useState<string | undefined>(undefined);
const [showQRCodeDialog, setShowQRCodeDialog] = useState<boolean>(false);
const havePermission = user.role === "ADMIN" || shortcut.creatorId === user.id;
const havePermission = currentUser.role === "ADMIN" || shortcut.creatorId === currentUser.id;
const shortifyLink = absolutifyLink(`/s/${shortcut.name}`);
useEffect(() => {

View File

@@ -1,26 +1,26 @@
import { Button } from "@mui/joy";
import { useState } from "react";
import { useAppSelector } from "../../stores";
import useUserStore from "../../stores/v1/user";
import ChangePasswordDialog from "../ChangePasswordDialog";
import EditUserinfoDialog from "../EditUserinfoDialog";
const AccountSection: React.FC = () => {
const user = useAppSelector((state) => state.user).user as User;
const currentUser = useUserStore().getCurrentUser();
const [showEditUserinfoDialog, setShowEditUserinfoDialog] = useState<boolean>(false);
const [showChangePasswordDialog, setShowChangePasswordDialog] = useState<boolean>(false);
const isAdmin = user.role === "ADMIN";
const isAdmin = currentUser.role === "ADMIN";
return (
<>
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start gap-y-2">
<p className="text-gray-400">Account</p>
<p className="text-base font-semibold leading-6 text-gray-900">Account</p>
<p className="flex flex-row justify-start items-center mt-2">
<span className="text-xl font-medium">{user.nickname}</span>
<span className="text-xl">{currentUser.nickname}</span>
{isAdmin && <span className="ml-2 bg-blue-600 text-white px-2 leading-6 text-sm rounded-full">Admin</span>}
</p>
<p className="flex flex-row justify-start items-center">
<span className="mr-3 text-gray-500 font-mono">Email: </span>
{user.email}
{currentUser.email}
</p>
<div className="flex flex-row justify-start items-center gap-2 mt-2">
<Button variant="outlined" color="neutral" onClick={() => setShowEditUserinfoDialog(true)}>

View File

@@ -0,0 +1,95 @@
import { useEffect, useState } from "react";
import { Button } from "@mui/joy";
import CreateUserDialog from "../CreateUserDialog";
import useUserStore from "../../stores/v1/user";
const MemberSection = () => {
const userStore = useUserStore();
const [showCreateUserDialog, setShowCreateUserDialog] = useState<boolean>(false);
const [currentEditingUser, setCurrentEditingUser] = useState<User | undefined>(undefined);
const userList = Object.values(userStore.userMap);
useEffect(() => {
userStore.fetchUserList();
}, []);
const handleCreateUserDialogClose = () => {
setShowCreateUserDialog(false);
setCurrentEditingUser(undefined);
};
return (
<>
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start space-y-4">
<div className="w-full">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<p className="text-base font-semibold leading-6 text-gray-900">Users</p>
<p className="mt-2 text-sm text-gray-700">
A list of all the users in your workspace including their nickname, email and role.
</p>
</div>
<div className="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<Button
onClick={() => {
setShowCreateUserDialog(true);
setCurrentEditingUser(undefined);
}}
>
Add user
</Button>
</div>
</div>
<div className="mt-2 flow-root">
<div className="overflow-x-auto">
<div className="inline-block min-w-full py-2 align-middle">
<table className="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900">
Nickname
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Email
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Role
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{userList.map((user) => (
<tr key={user.email}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm text-gray-900">{user.nickname}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.email}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.role}</td>
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium">
<button
className="text-indigo-600 hover:text-indigo-900"
onClick={() => {
setCurrentEditingUser(user);
setShowCreateUserDialog(true);
}}
>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{showCreateUserDialog && <CreateUserDialog user={currentEditingUser} onClose={handleCreateUserDialogClose} />}
</>
);
};
export default MemberSection;

View File

@@ -18,10 +18,9 @@ const WorkspaceSection: React.FC = () => {
return (
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start space-y-4">
<p className="text-gray-400">Workspace settings</p>
<p className="text-base font-semibold leading-6 text-gray-900">Workspace settings</p>
<div className="w-full flex flex-col justify-start items-start">
<Checkbox
className="font-medium"
label="Disable user signup"
checked={disallowSignUp}
onChange={(event) => handleDisallowSignUpChange(event.target.checked)}