feat: install mui

This commit is contained in:
steven
2022-09-29 22:20:19 +08:00
parent a97fe9d81f
commit 4a88bace66
21 changed files with 1056 additions and 656 deletions

View File

@ -0,0 +1,94 @@
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
import { createRoot } from "react-dom/client";
import Icon from "./Icon";
type DialogStyle = "info" | "warning";
interface Props {
title: string;
content: string;
style?: DialogStyle;
closeBtnText?: string;
confirmBtnText?: string;
onClose?: () => void;
onConfirm?: () => void;
}
const defaultProps = {
title: "",
content: "",
style: "info",
closeBtnText: "Close",
confirmBtnText: "Confirm",
onClose: () => null,
onConfirm: () => null,
};
const Alert: React.FC<Props> = (props: Props) => {
const { title, content, closeBtnText, confirmBtnText, onClose, onConfirm, style } = {
...defaultProps,
...props,
};
const handleCloseBtnClick = () => {
onClose();
};
const handleConfirmBtnClick = async () => {
onConfirm();
};
return (
<Dialog open={true}>
<DialogTitle className="flex flex-row justify-between items-center w-80">
<p className="text-base">{title}</p>
<button className="rounded p-1 hover:bg-gray-100" onClick={handleCloseBtnClick}>
<Icon.X className="w-5 h-auto text-gray-600" />
</button>
</DialogTitle>
<DialogContent className="w-80">
<p className="content-text mb-4">{content}</p>
<div className="w-full flex flex-row justify-end items-center">
<button className="rounded px-3 py-2 mr-2 hover:opacity-80" onClick={handleCloseBtnClick}>
{closeBtnText}
</button>
<button
className={`rounded px-3 py-2 shadow bg-green-600 text-white hover:opacity-80 ${
style === "warning" ? "border-red-600 text-red-600 bg-red-100" : ""
}`}
onClick={handleConfirmBtnClick}
>
{confirmBtnText}
</button>
</div>
</DialogContent>
</Dialog>
);
};
export const showCommonDialog = (props: Props) => {
const tempDiv = document.createElement("div");
const dialog = createRoot(tempDiv);
document.body.append(tempDiv);
const destory = () => {
dialog.unmount();
tempDiv.remove();
};
const onClose = () => {
if (props.onClose) {
props.onClose();
}
destory();
};
const onConfirm = () => {
if (props.onConfirm) {
props.onConfirm();
}
destory();
};
dialog.render(<Alert {...props} onClose={onClose} onConfirm={onConfirm} />);
};

View File

@ -1,9 +1,9 @@
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
import { useState } from "react";
import { validate, ValidatorConfig } from "../helpers/validator";
import useLoading from "../hooks/useLoading";
import { userService } from "../services";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
const validateConfig: ValidatorConfig = {
@ -13,15 +13,18 @@ const validateConfig: ValidatorConfig = {
noChinese: true,
};
type Props = DialogProps;
interface Props {
onClose: () => void;
}
const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
const ChangePasswordDialog: React.FC<Props> = (props: Props) => {
const { onClose } = props;
const [newPassword, setNewPassword] = useState("");
const [newPasswordAgain, setNewPasswordAgain] = useState("");
const requestState = useLoading(false);
const handleCloseBtnClick = () => {
destroy();
onClose();
};
const handleNewPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -59,8 +62,8 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
id: user.id,
password: newPassword,
});
onClose();
toastHelper.info("Password changed");
handleCloseBtnClick();
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.message);
@ -69,14 +72,14 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
};
return (
<>
<div className="max-w-full w-80 flex flex-row justify-between items-center mb-4">
<Dialog open={true}>
<DialogTitle className="flex flex-row justify-between items-center w-80">
<p className="text-base">Change Password</p>
<button className="rounded p-1 hover:bg-gray-100" onClick={destroy}>
<button className="rounded p-1 hover:bg-gray-100" onClick={handleCloseBtnClick}>
<Icon.X className="w-5 h-auto text-gray-600" />
</button>
</div>
<div className="w-full flex flex-col justify-start items-start">
</DialogTitle>
<DialogContent>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">New Password</span>
<input
@ -98,8 +101,8 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
<div className="w-full flex flex-row justify-end items-center">
<button
disabled={requestState.isLoading}
className={`rounded px-3 py-2 ${requestState.isLoading ? "opacity-80" : ""}`}
onClick={destroy}
className={`rounded px-3 py-2 mr-2 hover:opacity-80 ${requestState.isLoading ? "opacity-80" : ""}`}
onClick={handleCloseBtnClick}
>
Cancel
</button>
@ -111,13 +114,9 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
Save
</button>
</div>
</div>
</>
</DialogContent>
</Dialog>
);
};
function showChangePasswordDialog() {
generateDialog({}, ChangePasswordDialog);
}
export default showChangePasswordDialog;
export default ChangePasswordDialog;

View File

@ -1,13 +1,15 @@
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
import { useEffect, useState } from "react";
import { shortcutService } from "../services";
import useLoading from "../hooks/useLoading";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
interface Props extends DialogProps {
interface Props {
workspaceId: WorkspaceId;
shortcutId?: ShortcutId;
onClose: () => void;
onConfirm?: () => void;
}
interface State {
@ -15,7 +17,7 @@ interface State {
}
const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
const { destroy, workspaceId, shortcutId } = props;
const { onClose, onConfirm, workspaceId, shortcutId } = props;
const [state, setState] = useState<State>({
shortcutCreate: {
workspaceId: workspaceId,
@ -90,7 +92,12 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
} else {
await shortcutService.createShortcut(state.shortcutCreate);
}
destroy();
if (onConfirm) {
onConfirm();
} else {
onClose();
}
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.error || error.response.data.message);
@ -98,14 +105,14 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
};
return (
<>
<div className="max-w-full w-80 sm:w-96 flex flex-row justify-between items-center mb-4">
<Dialog open={true}>
<DialogTitle className="flex flex-row justify-between items-center w-80 sm:w-96">
<p className="text-base">{shortcutId ? "Edit Shortcut" : "Create Shortcut"}</p>
<button className="rounded p-1 hover:bg-gray-100" onClick={destroy}>
<button className="rounded p-1 hover:bg-gray-100" onClick={onClose}>
<Icon.X className="w-5 h-auto text-gray-600" />
</button>
</div>
<div className="w-full flex flex-col justify-start items-start">
</DialogTitle>
<DialogContent>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Name</span>
<input
@ -184,20 +191,9 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
Save
</button>
</div>
</div>
</>
</DialogContent>
</Dialog>
);
};
export default function showCreateShortcutDialog(workspaceId: WorkspaceId, shortcutId?: ShortcutId, onDestory?: () => void): void {
generateDialog(
{
onDestory,
},
CreateShortcutDialog,
{
workspaceId,
shortcutId,
}
);
}
export default CreateShortcutDialog;

View File

@ -1,12 +1,14 @@
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
import { useEffect, useState } from "react";
import { workspaceService } from "../services";
import useLoading from "../hooks/useLoading";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
interface Props extends DialogProps {
interface Props {
workspaceId?: WorkspaceId;
onClose: () => void;
onConfirm?: () => void;
}
interface State {
@ -14,7 +16,7 @@ interface State {
}
const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => {
const { destroy, workspaceId } = props;
const { onClose, onConfirm, workspaceId } = props;
const [state, setState] = useState<State>({
workspaceCreate: {
name: "",
@ -75,7 +77,12 @@ const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => {
...state.workspaceCreate,
});
}
destroy();
if (onConfirm) {
onConfirm();
} else {
onClose();
}
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.error || error.response.data.message);
@ -84,14 +91,14 @@ const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => {
};
return (
<>
<div className="max-w-full w-80 flex flex-row justify-between items-center mb-4">
<Dialog open={true}>
<DialogTitle className="flex flex-row justify-between items-center w-80">
<p className="text-base">{workspaceId ? "Edit Workspace" : "Create Workspace"}</p>
<button className="rounded p-1 hover:bg-gray-100" onClick={destroy}>
<button className="rounded p-1 hover:bg-gray-100" onClick={onClose}>
<Icon.X className="w-5 h-auto text-gray-600" />
</button>
</div>
<div className="w-full flex flex-col justify-start items-start">
</DialogTitle>
<DialogContent>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Name</span>
<input
@ -121,13 +128,9 @@ const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => {
Save
</button>
</div>
</div>
</>
</DialogContent>
</Dialog>
);
};
export default function showCreateWorkspaceDialog(workspaceId?: WorkspaceId): void {
generateDialog({}, CreateWorkspaceDialog, {
workspaceId,
});
}
export default CreateWorkspaceDialog;

View File

@ -1,94 +0,0 @@
import { useEffect } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { ANIMATION_DURATION } from "../../helpers/consts";
import store from "../../store";
import "../../less/base-dialog.less";
interface DialogConfig {
className?: string;
clickSpaceDestroy?: boolean;
onDestory?: () => void;
}
interface Props extends DialogConfig, DialogProps {
children: React.ReactNode;
}
const BaseDialog: React.FC<Props> = (props: Props) => {
const { children, className, clickSpaceDestroy, destroy } = props;
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.code === "Escape") {
destroy();
}
};
document.body.addEventListener("keydown", handleKeyDown);
return () => {
document.body.removeEventListener("keydown", handleKeyDown);
};
}, []);
const handleSpaceClicked = () => {
if (clickSpaceDestroy) {
destroy();
}
};
return (
<div className={`dialog-wrapper px-2 sm:px-0 ${className}`} onClick={handleSpaceClicked}>
<div className="dialog-container" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
};
export function generateDialog<T extends DialogProps>(
config: DialogConfig,
DialogComponent: React.FC<T>,
props?: Omit<T, "destroy">
): DialogCallback {
const tempDiv = document.createElement("div");
const dialog = createRoot(tempDiv);
document.body.append(tempDiv);
setTimeout(() => {
tempDiv.firstElementChild?.classList.add("showup");
}, 0);
const cbs: DialogCallback = {
destroy: () => {
tempDiv.firstElementChild?.classList.remove("showup");
tempDiv.firstElementChild?.classList.add("showoff");
setTimeout(() => {
dialog.unmount();
tempDiv.remove();
}, ANIMATION_DURATION);
if (config.onDestory) {
config.onDestory();
}
},
};
const dialogProps = {
...props,
destroy: cbs.destroy,
} as T;
const Fragment = (
<Provider store={store}>
<BaseDialog destroy={cbs.destroy} clickSpaceDestroy={true} {...config}>
<DialogComponent {...dialogProps} />
</BaseDialog>
</Provider>
);
dialog.render(Fragment);
return cbs;
}

View File

@ -1,85 +0,0 @@
import Icon from "../Icon";
import { generateDialog } from "./BaseDialog";
import "../../less/common-dialog.less";
type DialogStyle = "info" | "warning";
interface Props extends DialogProps {
title: string;
content: string;
style?: DialogStyle;
closeBtnText?: string;
confirmBtnText?: string;
onClose?: () => void;
onConfirm?: () => void;
}
const defaultProps = {
title: "",
content: "",
style: "info",
closeBtnText: "Close",
confirmBtnText: "Confirm",
onClose: () => null,
onConfirm: () => null,
};
const CommonDialog: React.FC<Props> = (props: Props) => {
const { title, content, destroy, closeBtnText, confirmBtnText, onClose, onConfirm, style } = {
...defaultProps,
...props,
};
const handleCloseBtnClick = () => {
onClose();
destroy();
};
const handleConfirmBtnClick = async () => {
onConfirm();
destroy();
};
return (
<>
<div className="dialog-header-container">
<p className="title-text">{title}</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X />
</button>
</div>
<div className="dialog-content-container">
<p className="content-text">{content}</p>
<div className="btns-container">
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
{closeBtnText}
</span>
<span className={`btn confirm-btn ${style}`} onClick={handleConfirmBtnClick}>
{confirmBtnText}
</span>
</div>
</div>
</>
);
};
interface CommonDialogProps {
title: string;
content: string;
className?: string;
style?: DialogStyle;
closeBtnText?: string;
confirmBtnText?: string;
onClose?: () => void;
onConfirm?: () => void;
}
export const showCommonDialog = (props: CommonDialogProps) => {
generateDialog(
{
className: `common-dialog ${props?.className ?? ""}`,
},
CommonDialog,
props
);
};

View File

@ -1 +0,0 @@
export { generateDialog } from "./BaseDialog";

View File

@ -1,102 +1,130 @@
import { useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useAppSelector } from "../store";
import { userService } from "../services";
import Icon from "./Icon";
import Dropdown from "./common/Dropdown";
import showCreateWorkspaceDialog from "./CreateWorkspaceDialog";
import CreateWorkspaceDialog from "./CreateWorkspaceDialog";
interface State {
showCreateWorkspaceDialog: boolean;
}
const Header: React.FC = () => {
const params = useParams();
const navigate = useNavigate();
const { user } = useAppSelector((state) => state.user);
const { workspaceList } = useAppSelector((state) => state.workspace);
const [state, setState] = useState<State>({
showCreateWorkspaceDialog: false,
});
const activedWorkspace = workspaceList.find((workspace) => workspace.name === params.workspaceName ?? "");
const handleCreateWorkspaceButtonClick = () => {
setState({
...state,
showCreateWorkspaceDialog: true,
});
};
const handleSignOutButtonClick = async () => {
await userService.doSignOut();
navigate("/user/auth");
};
return (
<div className="w-full bg-amber-50">
<div className="w-full max-w-4xl mx-auto px-3 py-5 flex flex-row justify-between items-center">
<div className="flex flex-row justify-start items-center">
<Link to={"/"} className="text-base font-mono font-medium cursor-pointer">
Corgi
</Link>
{workspaceList.length > 0 && activedWorkspace !== undefined && (
<>
<span className="font-mono mx-2 text-gray-200">/</span>
<>
<div className="w-full bg-amber-50">
<div className="w-full max-w-4xl mx-auto px-3 py-5 flex flex-row justify-between items-center">
<div className="flex flex-row justify-start items-center">
<Link to={"/"} className="text-base font-mono font-medium cursor-pointer">
Corgi
</Link>
{workspaceList.length > 0 && activedWorkspace !== undefined && (
<>
<span className="font-mono mx-2 text-gray-200">/</span>
<Dropdown
trigger={
<button className="flex flex-row justify-end items-center cursor-pointer">
<span className="font-mono">{activedWorkspace?.name}</span>
<Icon.ChevronDown className="ml-1 w-5 h-auto text-gray-600" />
</button>
}
actions={
<>
{workspaceList.map((workspace) => {
return (
<Link
key={workspace.id}
to={`/${workspace.name}`}
className="w-full px-3 leading-10 flex flex-row justify-between items-center text-left cursor-pointer rounded whitespace-nowrap hover:bg-gray-100"
>
<span className="truncate">{workspace.name}</span>
{workspace.name === activedWorkspace?.name && <Icon.Check className="w-4 h-auto ml-1 shrink-0" />}
</Link>
);
})}
<hr className="w-full border-t my-1 border-t-gray-100" />
<button
className="w-full flex flex-row justify-start items-center px-3 leading-10 rounded cursor-pointer hover:bg-gray-100"
onClick={handleCreateWorkspaceButtonClick}
>
<Icon.Plus className="w-4 h-auto mr-2" /> Create Workspace
</button>
</>
}
actionsClassName="!w-48 !-left-4"
></Dropdown>
</>
)}
</div>
<div className="relative">
{user ? (
<Dropdown
trigger={
<button className="flex flex-row justify-end items-center cursor-pointer">
<span className="font-mono">{activedWorkspace?.name}</span>
<span>{user?.name}</span>
<Icon.ChevronDown className="ml-1 w-5 h-auto text-gray-600" />
</button>
}
actions={
<>
{workspaceList.map((workspace) => {
return (
<Link
key={workspace.id}
to={`/${workspace.name}`}
className="w-full px-3 leading-10 flex flex-row justify-between items-center text-left cursor-pointer rounded whitespace-nowrap hover:bg-gray-100"
>
<span className="truncate">{workspace.name}</span>
{workspace.name === activedWorkspace?.name && <Icon.Check className="w-4 h-auto ml-1 shrink-0" />}
</Link>
);
})}
<hr className="w-full border-t my-1 border-t-gray-100" />
<button
className="w-full flex flex-row justify-start items-center px-3 leading-10 rounded cursor-pointer hover:bg-gray-100"
onClick={() => showCreateWorkspaceDialog()}
<Link
to="/account"
className="w-full flex flex-row justify-start items-center px-3 leading-10 text-left cursor-pointer rounded whitespace-nowrap hover:bg-gray-100"
>
<Icon.Plus className="w-4 h-auto mr-1" /> Create Workspace
<Icon.User className="w-4 h-auto mr-2" /> My Account
</Link>
<button
className="w-full flex flex-row justify-start items-center px-3 leading-10 text-left cursor-pointer rounded whitespace-nowrap hover:bg-gray-100"
onClick={() => handleSignOutButtonClick()}
>
<Icon.LogOut className="w-4 h-auto mr-2" /> Sign out
</button>
</>
}
actionsClassName="!w-48 !-left-4"
actionsClassName="!w-40"
></Dropdown>
</>
)}
</div>
<div className="relative">
{user ? (
<Dropdown
trigger={
<button className="flex flex-row justify-end items-center cursor-pointer">
<span>{user?.name}</span>
<Icon.ChevronDown className="ml-1 w-5 h-auto text-gray-600" />
</button>
}
actions={
<>
<Link
to="/account"
className="w-full flex flex-row justify-start items-center px-3 leading-10 text-left cursor-pointer rounded whitespace-nowrap hover:bg-gray-100"
>
<Icon.User className="w-4 h-auto mr-1" /> My Account
</Link>
<button
className="w-full flex flex-row justify-start items-center px-3 leading-10 text-left cursor-pointer rounded whitespace-nowrap hover:bg-gray-100"
onClick={() => handleSignOutButtonClick()}
>
<Icon.LogOut className="w-4 h-auto mr-1" /> Sign out
</button>
</>
}
actionsClassName="!w-40"
></Dropdown>
) : (
<span className="cursor-pointer" onClick={() => navigate("/user/auth")}>
Sign in
</span>
)}
) : (
<span className="cursor-pointer" onClick={() => navigate("/user/auth")}>
Sign in
</span>
)}
</div>
</div>
</div>
</div>
{state.showCreateWorkspaceDialog && (
<CreateWorkspaceDialog
onClose={() => {
setState({
...state,
showCreateWorkspaceDialog: false,
});
}}
/>
)}
</>
);
};

View File

@ -4,7 +4,7 @@ import useLoading from "../hooks/useLoading";
import { workspaceService } from "../services";
import toastHelper from "./Toast";
import Dropdown from "./common/Dropdown";
import { showCommonDialog } from "./Dialog/CommonDialog";
import { showCommonDialog } from "./Alert";
import Icon from "./Icon";
const userRoles = ["Admin", "User"];
@ -26,17 +26,16 @@ const MemberListView: React.FC<Props> = (props: Props) => {
});
const loadingState = useLoading();
const fetchWorkspaceUserList = () => {
const fetchWorkspaceUserList = async () => {
loadingState.setLoading();
return Promise.all([workspaceService.getWorkspaceUserList(workspaceId)])
.then(([workspaceUserList]) => {
setState({
workspaceUserList: workspaceUserList,
});
})
.finally(() => {
loadingState.setFinish();
try {
const [workspaceUserList] = await Promise.all([workspaceService.getWorkspaceUserList(workspaceId)]);
setState({
workspaceUserList: workspaceUserList,
});
} finally {
loadingState.setFinish();
}
};
useEffect(() => {

View File

@ -1,25 +1,41 @@
import copy from "copy-to-clipboard";
import { useState } from "react";
import { shortcutService, workspaceService } from "../services";
import { useAppSelector } from "../store";
import { showCommonDialog } from "./Dialog/CommonDialog";
import Dropdown from "./common/Dropdown";
import { UNKNOWN_ID } from "../helpers/consts";
import { showCommonDialog } from "./Alert";
import Icon from "./Icon";
import showCreateShortcutDialog from "./CreateShortcutDialog";
import Dropdown from "./common/Dropdown";
import CreateShortcutDialog from "./CreateShortcutDialog";
interface Props {
workspaceId: WorkspaceId;
shortcutList: Shortcut[];
}
interface State {
currentEditingShortcutId: ShortcutId;
}
const ShortcutListView: React.FC<Props> = (props: Props) => {
const { workspaceId, shortcutList } = props;
const { user } = useAppSelector((state) => state.user);
const [state, setState] = useState<State>({
currentEditingShortcutId: UNKNOWN_ID,
});
const handleCopyButtonClick = (shortcut: Shortcut) => {
const workspace = workspaceService.getWorkspaceById(workspaceId);
copy(`${location.host}/${workspace?.name}/go/${shortcut.name}`);
};
const handleEditShortcutButtonClick = (shortcut: Shortcut) => {
setState({
...state,
currentEditingShortcutId: shortcut.id,
});
};
const handleDeleteShortcutButtonClick = (shortcut: Shortcut) => {
showCommonDialog({
title: "Delete Shortcut",
@ -32,55 +48,76 @@ const ShortcutListView: React.FC<Props> = (props: Props) => {
};
return (
<div className="w-full flex flex-col justify-start items-start">
{shortcutList.map((shortcut) => {
return (
<div key={shortcut.id} className="w-full flex flex-row justify-between items-start border px-6 py-4 mb-3 rounded-lg">
<div className="flex flex-row justify-start items-center mr-4">
<span>{shortcut.name}</span>
<span className="text-gray-400 text-sm ml-2">({shortcut.description})</span>
<>
<div className="w-full flex flex-col justify-start items-start">
{shortcutList.map((shortcut) => {
return (
<div key={shortcut.id} className="w-full flex flex-row justify-between items-start border px-6 py-4 mb-3 rounded-lg">
<div className="flex flex-row justify-start items-center mr-4">
<span>{shortcut.name}</span>
<span className="text-gray-400 text-sm ml-2">({shortcut.description})</span>
</div>
<div className="flex flex-row justify-end items-center">
<span className=" w-12 mr-2 text-gray-600">{shortcut.creator.name}</span>
<button
className="cursor-pointer mr-4 hover:opacity-80"
onClick={() => {
handleCopyButtonClick(shortcut);
}}
>
<Icon.Copy className="w-5 h-auto" />
</button>
<a className="cursor-pointer mr-4 hover:opacity-80" target="blank" href={shortcut.link}>
<Icon.ExternalLink className="w-5 h-auto" />
</a>
<Dropdown
actions={
<>
<button
disabled={shortcut.creatorId !== user?.id}
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
onClick={() => handleEditShortcutButtonClick(shortcut)}
>
Edit
</button>
<button
disabled={shortcut.creatorId !== user?.id}
className="w-full px-3 text-left leading-10 cursor-pointer rounded text-red-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
onClick={() => {
handleDeleteShortcutButtonClick(shortcut);
}}
>
Delete
</button>
</>
}
actionsClassName="!w-24"
></Dropdown>
</div>
</div>
<div className="flex flex-row justify-end items-center">
<span className=" w-12 mr-2 text-gray-600">{shortcut.creator.name}</span>
<button
className="cursor-pointer mr-4 hover:opacity-80"
onClick={() => {
handleCopyButtonClick(shortcut);
}}
>
<Icon.Copy className="w-5 h-auto" />
</button>
<a className="cursor-pointer mr-4 hover:opacity-80" target="blank" href={shortcut.link}>
<Icon.ExternalLink className="w-5 h-auto" />
</a>
<Dropdown
actions={
<>
<button
disabled={shortcut.creatorId !== user?.id}
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
onClick={() => showCreateShortcutDialog(workspaceId, shortcut.id)}
>
Edit
</button>
<button
disabled={shortcut.creatorId !== user?.id}
className="w-full px-3 text-left leading-10 cursor-pointer rounded text-red-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
onClick={() => {
handleDeleteShortcutButtonClick(shortcut);
}}
>
Delete
</button>
</>
}
actionsClassName="!w-24"
></Dropdown>
</div>
</div>
);
})}
</div>
);
})}
</div>
{state.currentEditingShortcutId !== UNKNOWN_ID && (
<CreateShortcutDialog
workspaceId={workspaceId}
shortcutId={state.currentEditingShortcutId}
onClose={() => {
setState({
...state,
currentEditingShortcutId: UNKNOWN_ID,
});
}}
onConfirm={() => {
setState({
...state,
currentEditingShortcutId: UNKNOWN_ID,
});
}}
/>
)}
</>
);
};

View File

@ -1,13 +1,15 @@
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
import { useState } from "react";
import { UNKNOWN_ID } from "../helpers/consts";
import { upsertWorkspaceUser } from "../helpers/api";
import useLoading from "../hooks/useLoading";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
interface Props extends DialogProps {
interface Props {
workspaceId: WorkspaceId;
onClose: () => void;
onConfirm?: () => void;
}
interface State {
@ -15,7 +17,7 @@ interface State {
}
const UpsertWorkspaceUserDialog: React.FC<Props> = (props: Props) => {
const { destroy, workspaceId } = props;
const { onClose, onConfirm, workspaceId } = props;
const [state, setState] = useState<State>({
workspaceUserUpsert: {
workspaceId: workspaceId,
@ -56,7 +58,12 @@ const UpsertWorkspaceUserDialog: React.FC<Props> = (props: Props) => {
await upsertWorkspaceUser({
...state.workspaceUserUpsert,
});
destroy();
if (onConfirm) {
onConfirm();
} else {
onClose();
}
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.error || error.response.data.message);
@ -65,14 +72,14 @@ const UpsertWorkspaceUserDialog: React.FC<Props> = (props: Props) => {
};
return (
<>
<div className="max-w-full w-80 flex flex-row justify-between items-center mb-4">
<Dialog open={true}>
<DialogTitle className="flex flex-row justify-between items-center w-80">
<p className="text-base">Create Workspace Member</p>
<button className="rounded p-1 hover:bg-gray-100" onClick={destroy}>
<button className="rounded p-1 hover:bg-gray-100" onClick={onClose}>
<Icon.X className="w-5 h-auto text-gray-600" />
</button>
</div>
<div className="w-full flex flex-col justify-start items-start">
</DialogTitle>
<DialogContent>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">User ID</span>
<input
@ -120,19 +127,9 @@ const UpsertWorkspaceUserDialog: React.FC<Props> = (props: Props) => {
Save
</button>
</div>
</div>
</>
</DialogContent>
</Dialog>
);
};
export default function showUpsertWorkspaceUserDialog(workspaceId: WorkspaceId, onDestory?: () => void) {
return generateDialog(
{
onDestory,
},
UpsertWorkspaceUserDialog,
{
workspaceId,
}
);
}
export default UpsertWorkspaceUserDialog;

View File

@ -1,15 +1,31 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { UNKNOWN_ID } from "../helpers/consts";
import { workspaceService } from "../services";
import { showCommonDialog } from "./Dialog/CommonDialog";
import { showCommonDialog } from "./Alert";
import Dropdown from "./common/Dropdown";
import showCreateWorkspaceDialog from "./CreateWorkspaceDialog";
import CreateWorkspaceDialog from "./CreateWorkspaceDialog";
interface Props {
workspaceList: Workspace[];
}
interface State {
currentEditingWorkspaceId: WorkspaceId;
}
const WorkspaceListView: React.FC<Props> = (props: Props) => {
const { workspaceList } = props;
const [state, setState] = useState<State>({
currentEditingWorkspaceId: UNKNOWN_ID,
});
const handleEditWorkspaceButtonClick = (workspaceId: WorkspaceId) => {
setState({
...state,
currentEditingWorkspaceId: workspaceId,
});
};
const handleDeleteWorkspaceButtonClick = (workspace: Workspace) => {
showCommonDialog({
@ -23,41 +39,55 @@ const WorkspaceListView: React.FC<Props> = (props: Props) => {
};
return (
<div className="w-full flex flex-col justify-start items-start">
{workspaceList.map((workspace) => {
return (
<div key={workspace.id} className="w-full flex flex-row justify-between items-start border px-6 py-4 mb-3 rounded-lg">
<div className="flex flex-col justify-start items-start">
<Link to={`/${workspace.name}`} className="text-lg cursor-pointer hover:underline">
{workspace.name}
</Link>
<span className="text-sm mt-1 text-gray-600">{workspace.description}</span>
<>
<div className="w-full flex flex-col justify-start items-start">
{workspaceList.map((workspace) => {
return (
<div key={workspace.id} className="w-full flex flex-row justify-between items-start border px-6 py-4 mb-3 rounded-lg">
<div className="flex flex-col justify-start items-start">
<Link to={`/${workspace.name}`} className="text-lg cursor-pointer hover:underline">
{workspace.name}
</Link>
<span className="text-sm mt-1 text-gray-600">{workspace.description}</span>
</div>
<Dropdown
actions={
<>
<button
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
onClick={() => handleEditWorkspaceButtonClick(workspace.id)}
>
Edit
</button>
<button
className="w-full px-3 text-left leading-10 cursor-pointer rounded text-red-600 hover:bg-gray-100"
onClick={() => {
handleDeleteWorkspaceButtonClick(workspace);
}}
>
Delete
</button>
</>
}
actionsClassName="!w-24"
></Dropdown>
</div>
<Dropdown
actions={
<>
<button
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
onClick={() => showCreateWorkspaceDialog(workspace.id)}
>
Edit
</button>
<button
className="w-full px-3 text-left leading-10 cursor-pointer rounded text-red-600 hover:bg-gray-100"
onClick={() => {
handleDeleteWorkspaceButtonClick(workspace);
}}
>
Delete
</button>
</>
}
actionsClassName="!w-24"
></Dropdown>
</div>
);
})}
</div>
);
})}
</div>
{state.currentEditingWorkspaceId !== UNKNOWN_ID && (
<CreateWorkspaceDialog
workspaceId={state.currentEditingWorkspaceId}
onClose={() => {
setState({
...state,
currentEditingWorkspaceId: UNKNOWN_ID,
});
}}
/>
)}
</>
);
};

View File

@ -5,9 +5,9 @@ import useLoading from "../hooks/useLoading";
import { workspaceService } from "../services";
import { useAppSelector } from "../store";
import { unknownWorkspace, unknownWorkspaceUser } from "../store/modules/workspace";
import showCreateWorkspaceDialog from "./CreateWorkspaceDialog";
import { showCommonDialog } from "./Dialog/CommonDialog";
import { showCommonDialog } from "./Alert";
import toastHelper from "./Toast";
import CreateWorkspaceDialog from "./CreateWorkspaceDialog";
interface Props {
workspaceId: WorkspaceId;
@ -16,6 +16,7 @@ interface Props {
interface State {
workspace: Workspace;
workspaceUser: WorkspaceUser;
showEditWorkspaceDialog: boolean;
}
const WorkspaceSetting: React.FC<Props> = (props: Props) => {
@ -25,6 +26,7 @@ const WorkspaceSetting: React.FC<Props> = (props: Props) => {
const [state, setState] = useState<State>({
workspace: unknownWorkspace,
workspaceUser: unknownWorkspaceUser,
showEditWorkspaceDialog: false,
});
const loadingState = useLoading();
@ -39,6 +41,7 @@ const WorkspaceSetting: React.FC<Props> = (props: Props) => {
Promise.all([workspaceService.getWorkspaceUser(workspace.id, user.id)])
.then(([workspaceUser]) => {
setState({
...state,
workspace,
workspaceUser,
});
@ -49,7 +52,29 @@ const WorkspaceSetting: React.FC<Props> = (props: Props) => {
}, []);
const handleEditWorkspaceButtonClick = () => {
showCreateWorkspaceDialog(state.workspace.id);
setState({
...state,
showEditWorkspaceDialog: true,
});
};
const handleEditWorkspaceDialogConfirm = () => {
const prevWorkspace = state.workspace;
const workspace = workspaceService.getWorkspaceById(workspaceId);
if (!workspace) {
toastHelper.error("workspace not found");
return;
}
setState({
...state,
workspace: workspace,
showEditWorkspaceDialog: false,
});
if (prevWorkspace.name !== workspace.name) {
navigate(`/${workspace.name}#setting`);
}
};
const handleDeleteWorkspaceButtonClick = () => {
@ -80,38 +105,53 @@ const WorkspaceSetting: React.FC<Props> = (props: Props) => {
};
return (
<div className="w-full flex flex-col justify-start items-start">
<p className="text-3xl mt-2 mb-4">{state.workspace.name}</p>
<p>{state.workspace.description}</p>
<>
<div className="w-full flex flex-col justify-start items-start">
<p className="text-3xl mt-2 mb-4">{state.workspace.name}</p>
<p>{state.workspace.description}</p>
<div className="border-t pt-4 mt-2 flex flex-row justify-start items-center">
<span className="text-gray-400 mr-2">Actions:</span>
<div className="flex flex-row justify-start items-center space-x-2">
{state.workspaceUser.role === "ADMIN" ? (
<>
<button className="border rounded-md px-3 leading-8 hover:shadow" onClick={handleEditWorkspaceButtonClick}>
Edit
</button>
<button
className="border rounded-md px-3 leading-8 border-red-600 text-red-600 bg-red-50 hover:shadow"
onClick={handleDeleteWorkspaceButtonClick}
>
Delete
</button>
</>
) : (
<>
<button
className="border rounded-md px-3 leading-8 border-red-600 text-red-600 bg-red-50 hover:shadow"
onClick={handleExitWorkspaceButtonClick}
>
Exit
</button>
</>
)}
<div className="border-t pt-4 mt-2 flex flex-row justify-start items-center">
<span className="text-gray-400 mr-2">Actions:</span>
<div className="flex flex-row justify-start items-center space-x-2">
{state.workspaceUser.role === "ADMIN" ? (
<>
<button className="border rounded-md px-3 leading-8 hover:shadow" onClick={handleEditWorkspaceButtonClick}>
Edit
</button>
<button
className="border rounded-md px-3 leading-8 border-red-600 text-red-600 bg-red-50 hover:shadow"
onClick={handleDeleteWorkspaceButtonClick}
>
Delete
</button>
</>
) : (
<>
<button
className="border rounded-md px-3 leading-8 border-red-600 text-red-600 bg-red-50 hover:shadow"
onClick={handleExitWorkspaceButtonClick}
>
Exit
</button>
</>
)}
</div>
</div>
</div>
</div>
{state.showEditWorkspaceDialog && (
<CreateWorkspaceDialog
workspaceId={state.workspace.id}
onClose={() => {
setState({
...state,
showEditWorkspaceDialog: false,
});
}}
onConfirm={handleEditWorkspaceDialogConfirm}
/>
)}
</>
);
};

View File

@ -1,37 +0,0 @@
.dialog-wrapper {
@apply fixed top-0 left-0 flex flex-col justify-start items-center w-full h-full pt-16 z-100 overflow-x-hidden overflow-y-scroll bg-transparent transition-all;
&.showup {
background-color: rgba(0, 0, 0, 0.6);
}
&.showoff {
display: none;
}
> .dialog-container {
@apply flex flex-col justify-start items-start bg-white p-4 sm:px-6 rounded-lg;
> .dialog-header-container {
@apply flex flex-row justify-between items-center w-full mb-4;
> .title-text {
> .icon-text {
@apply mr-2 text-base;
}
}
.btn {
@apply flex flex-col justify-center items-center w-6 h-6 rounded hover:bg-gray-100 hover:shadow;
}
}
> .dialog-content-container {
@apply flex flex-col justify-start items-start w-full;
}
> .dialog-footer-container {
@apply flex flex-row justify-end items-center w-full mt-4;
}
}
}

View File

@ -1,25 +0,0 @@
.common-dialog {
> .dialog-container {
@apply w-80;
> .dialog-content-container {
@apply flex flex-col justify-start items-start;
> .btns-container {
@apply flex flex-row justify-end items-center w-full mt-4;
> .btn {
@apply text-sm py-1 px-3 mr-2 rounded-md cursor-pointer hover:opacity-80;
&.confirm-btn {
@apply bg-red-100 border border-solid border-blue-600 text-blue-600;
&.warning {
@apply border-red-600 text-red-600;
}
}
}
}
}
}
}

View File

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { userService, workspaceService } from "../services";
import { useAppSelector } from "../store";
@ -6,11 +6,18 @@ import useLoading from "../hooks/useLoading";
import Icon from "../components/Icon";
import Header from "../components/Header";
import WorkspaceListView from "../components/WorkspaceListView";
import showCreateWorkspaceDialog from "../components/CreateWorkspaceDialog";
import CreateWorkspaceDialog from "../components/CreateWorkspaceDialog";
interface State {
showCreateWorkspaceDialog: boolean;
}
const Home: React.FC = () => {
const navigate = useNavigate();
const { workspaceList } = useAppSelector((state) => state.workspace);
const [state, setState] = useState<State>({
showCreateWorkspaceDialog: false,
});
const loadingState = useLoading();
useEffect(() => {
@ -24,29 +31,49 @@ const Home: React.FC = () => {
});
}, []);
const handleCreateWorkspaceButtonClick = () => {
setState({
...state,
showCreateWorkspaceDialog: true,
});
};
return (
<div className="w-full h-full flex flex-col justify-start items-start">
<Header />
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start">
<div className="mb-4 w-full flex flex-row justify-between items-center">
<span className="font-mono text-gray-400">Workspace List</span>
<button
className="text-sm flex flex-row justify-start items-center border px-3 leading-10 rounded-lg cursor-pointer hover:shadow"
onClick={() => showCreateWorkspaceDialog()}
>
<Icon.Plus className="w-5 h-auto mr-1" /> Create Workspace
</button>
</div>
{loadingState.isLoading ? (
<div className="py-4 w-full flex flex-row justify-center items-center">
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
loading
<>
<div className="w-full h-full flex flex-col justify-start items-start">
<Header />
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start">
<div className="mb-4 w-full flex flex-row justify-between items-center">
<span className="font-mono text-gray-400">Workspace List</span>
<button
className="text-sm flex flex-row justify-start items-center border px-3 leading-10 rounded-lg cursor-pointer hover:shadow"
onClick={handleCreateWorkspaceButtonClick}
>
<Icon.Plus className="w-5 h-auto mr-1" /> Create Workspace
</button>
</div>
) : (
<WorkspaceListView workspaceList={workspaceList} />
)}
{loadingState.isLoading ? (
<div className="py-4 w-full flex flex-row justify-center items-center">
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
loading
</div>
) : (
<WorkspaceListView workspaceList={workspaceList} />
)}
</div>
</div>
</div>
{state.showCreateWorkspaceDialog && (
<CreateWorkspaceDialog
onClose={() => {
setState({
...state,
showCreateWorkspaceDialog: false,
});
}}
/>
)}
</>
);
};

View File

@ -1,17 +1,24 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAppSelector } from "../store";
import Header from "../components/Header";
import { showCommonDialog } from "../components/Dialog/CommonDialog";
import { showCommonDialog } from "../components/Alert";
import { userService } from "../services";
import Icon from "../components/Icon";
import copy from "copy-to-clipboard";
import toastHelper from "../components/Toast";
import showChangePasswordDialog from "../components/ChangePasswordDialog";
import ChangePasswordDialog from "../components/ChangePasswordDialog";
interface State {
showChangePasswordDialog: boolean;
}
const UserDetail: React.FC = () => {
const navigate = useNavigate();
const { user } = useAppSelector((state) => state.user);
const [state, setState] = useState<State>({
showChangePasswordDialog: false,
});
useEffect(() => {
if (!userService.getState().user) {
@ -21,7 +28,10 @@ const UserDetail: React.FC = () => {
}, []);
const handleChangePasswordBtnClick = async () => {
showChangePasswordDialog();
setState({
...state,
showChangePasswordDialog: true,
});
};
const handleCopyOpenIdBtnClick = async () => {
@ -49,35 +59,47 @@ const UserDetail: React.FC = () => {
};
return (
<div className="w-full h-full flex flex-col justify-start items-start">
<Header />
<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-3xl mt-2 mb-4">{user?.name}</p>
<p className="leading-8 flex flex-row justify-start items-center">
<span className="mr-3 text-gray-500 font-mono">Email: </span>
{user?.email}
</p>
<p className="leading-8 flex flex-row justify-start items-center">
<span className="mr-3 text-gray-500 font-mono">Password: </span>
<button className="border rounded-md px-2 leading-8 hover:shadow" onClick={handleChangePasswordBtnClick}>
Change
</button>
</p>
<p className="leading-8 flex flex-row justify-start items-center">
<span className="mr-3 text-gray-500 font-mono">OpenID:</span>
<input type="text" value={user?.openId} readOnly className="border shrink rounded-md px-3 pr-5 shadow-inner truncate" />
<button className="-ml-6 bg-white text-gray-600 hover:text-black" onClick={handleCopyOpenIdBtnClick}>
<Icon.Clipboard className="w-4 h-auto" />
</button>
<button
className="border ml-4 rounded-md px-2 leading-8 border-red-600 text-red-600 bg-red-50 hover:shadow"
onClick={handleResetOpenIdBtnClick}
>
Reset
</button>
</p>
<>
<div className="w-full h-full flex flex-col justify-start items-start">
<Header />
<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-3xl mt-2 mb-4">{user?.name}</p>
<p className="leading-8 flex flex-row justify-start items-center">
<span className="mr-3 text-gray-500 font-mono">Email: </span>
{user?.email}
</p>
<p className="leading-8 flex flex-row justify-start items-center">
<span className="mr-3 text-gray-500 font-mono">Password: </span>
<button className="border rounded-md px-2 leading-8 hover:shadow" onClick={handleChangePasswordBtnClick}>
Change
</button>
</p>
<p className="leading-8 flex flex-row justify-start items-center">
<span className="mr-3 text-gray-500 font-mono">OpenID:</span>
<input type="text" value={user?.openId} readOnly className="border shrink rounded-md px-3 pr-5 shadow-inner truncate" />
<button className="-ml-6 bg-white text-gray-600 hover:text-black" onClick={handleCopyOpenIdBtnClick}>
<Icon.Clipboard className="w-4 h-auto" />
</button>
<button
className="border ml-4 rounded-md px-2 leading-8 border-red-600 text-red-600 bg-red-50 hover:shadow"
onClick={handleResetOpenIdBtnClick}
>
Reset
</button>
</p>
</div>
</div>
</div>
{state.showChangePasswordDialog && (
<ChangePasswordDialog
onClose={() => {
setState({
...state,
showChangePasswordDialog: false,
});
}}
/>
)}
</>
);
};

View File

@ -7,17 +7,19 @@ import useLoading from "../hooks/useLoading";
import Icon from "../components/Icon";
import toastHelper from "../components/Toast";
import Dropdown from "../components/common/Dropdown";
import showCreateShortcutDialog from "../components/CreateShortcutDialog";
import showUpsertWorkspaceUserDialog from "../components/UpsertWorkspaceUserDialog";
import Header from "../components/Header";
import ShortcutListView from "../components/ShortcutListView";
import MemberListView from "../components/MemberListView";
import WorkspaceSetting from "../components/WorkspaceSetting";
import CreateShortcutDialog from "../components/CreateShortcutDialog";
import UpsertWorkspaceUserDialog from "../components/UpsertWorkspaceUserDialog";
interface State {
workspace: Workspace;
workspaceUser: WorkspaceUser;
userList: WorkspaceUser[];
showCreateShortcutDialog: boolean;
showUpsertWorkspaceUserDialog: boolean;
}
const WorkspaceDetail: React.FC = () => {
@ -30,6 +32,8 @@ const WorkspaceDetail: React.FC = () => {
workspace: unknownWorkspace,
workspaceUser: unknownWorkspaceUser,
userList: [],
showCreateShortcutDialog: false,
showUpsertWorkspaceUserDialog: false,
});
const loadingState = useLoading();
@ -53,6 +57,7 @@ const WorkspaceDetail: React.FC = () => {
])
.then(([, workspaceUser, workspaceUserList]) => {
setState({
...state,
workspace,
workspaceUser,
userList: workspaceUserList,
@ -70,86 +75,130 @@ const WorkspaceDetail: React.FC = () => {
}, [location.hash]);
const handleCreateShortcutButtonClick = () => {
showCreateShortcutDialog(state.workspace.id, undefined, async () => {
if (location.hash !== "#shortcuts") {
navigate("#shortcuts");
}
setState({
...state,
showCreateShortcutDialog: true,
});
};
const handleUpsertWorkspaceMemberButtonClick = () => {
showUpsertWorkspaceUserDialog(state.workspace.id, async () => {
const workspaceUserList = await workspaceService.getWorkspaceUserList(state.workspace.id);
setState({
...state,
userList: workspaceUserList,
});
if (location.hash !== "#members") {
navigate("#members");
}
setState({
...state,
showUpsertWorkspaceUserDialog: true,
});
};
return (
<div className="w-full h-full flex flex-col justify-start items-start">
<Header />
<div className="mx-auto max-w-4xl w-full px-3 pb-6 flex flex-col justify-start items-start">
<div className="w-full flex flex-row justify-between items-center mt-4 mb-4">
<div className="flex flex-row justify-start items-center space-x-4">
<NavLink to="#shortcuts" className={`${location.hash === "#shortcuts" && "underline"}`}>
Shortcuts
</NavLink>
<NavLink to="#members" className={`${location.hash === "#members" && "underline"}`}>
Members
</NavLink>
<NavLink to="#setting" className={`${location.hash === "#setting" && "underline"}`}>
Setting
</NavLink>
</div>
<div>
<Dropdown
trigger={
<button className="w-32 flex flex-row justify-start items-center border px-3 leading-10 rounded-lg cursor-pointer hover:shadow">
<Icon.Plus className="w-4 h-auto mr-1" /> Add new...
</button>
}
actions={
<>
<button
className="w-full flex flex-row justify-start items-center px-3 leading-10 rounded cursor-pointer hover:bg-gray-100"
onClick={handleCreateShortcutButtonClick}
>
Shortcut
<>
<div className="w-full h-full flex flex-col justify-start items-start">
<Header />
<div className="mx-auto max-w-4xl w-full px-3 pb-6 flex flex-col justify-start items-start">
<div className="w-full flex flex-row justify-between items-center mt-4 mb-4">
<div className="flex flex-row justify-start items-center space-x-4">
<NavLink to="#shortcuts" className={`${location.hash === "#shortcuts" && "underline"}`}>
Shortcuts
</NavLink>
<NavLink to="#members" className={`${location.hash === "#members" && "underline"}`}>
Members
</NavLink>
<NavLink to="#setting" className={`${location.hash === "#setting" && "underline"}`}>
Setting
</NavLink>
</div>
<div>
<Dropdown
trigger={
<button className="w-32 flex flex-row justify-start items-center border px-3 leading-10 rounded-lg cursor-pointer hover:shadow">
<Icon.Plus className="w-4 h-auto mr-1" /> Add new...
</button>
<button
className="w-full flex flex-row justify-start items-center px-3 leading-10 rounded cursor-pointer hover:bg-gray-100"
onClick={handleUpsertWorkspaceMemberButtonClick}
>
Member
</button>
</>
}
actionsClassName="!w-32"
/>
}
actions={
<>
<button
className="w-full flex flex-row justify-start items-center px-3 leading-10 rounded cursor-pointer hover:bg-gray-100"
onClick={handleCreateShortcutButtonClick}
>
Shortcut
</button>
<button
className="w-full flex flex-row justify-start items-center px-3 leading-10 rounded cursor-pointer hover:bg-gray-100"
onClick={handleUpsertWorkspaceMemberButtonClick}
>
Member
</button>
</>
}
actionsClassName="!w-32"
/>
</div>
</div>
{loadingState.isLoading ? (
<div className="py-4 w-full flex flex-row justify-center items-center">
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
loading
</div>
) : (
<>
{location.hash === "#shortcuts" && <ShortcutListView workspaceId={state.workspace.id} shortcutList={shortcutList} />}
{location.hash === "#members" && (
<MemberListView
key={Date.now()}
workspaceId={state.workspace.id}
workspaceUser={state.workspaceUser}
userList={state.userList}
/>
)}
{location.hash === "#setting" && <WorkspaceSetting workspaceId={state.workspace.id} />}
</>
)}
</div>
{loadingState.isLoading ? (
<div className="py-4 w-full flex flex-row justify-center items-center">
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
loading
</div>
) : (
<>
{location.hash === "#shortcuts" && <ShortcutListView workspaceId={state.workspace.id} shortcutList={shortcutList} />}
{location.hash === "#members" && (
<MemberListView workspaceId={state.workspace.id} workspaceUser={state.workspaceUser} userList={state.userList} />
)}
{location.hash === "#setting" && <WorkspaceSetting workspaceId={state.workspace.id} />}
</>
)}
</div>
</div>
{state.showCreateShortcutDialog && (
<CreateShortcutDialog
workspaceId={state.workspace.id}
onClose={() => {
setState({
...state,
showCreateShortcutDialog: false,
});
}}
onConfirm={() => {
setState({
...state,
showCreateShortcutDialog: false,
});
if (location.hash !== "#shortcuts") {
navigate("#shortcuts");
}
}}
/>
)}
{state.showUpsertWorkspaceUserDialog && (
<UpsertWorkspaceUserDialog
workspaceId={state.workspace.id}
onClose={() => {
setState({
...state,
showUpsertWorkspaceUserDialog: false,
});
}}
onConfirm={async () => {
const workspaceUserList = await workspaceService.getWorkspaceUserList(state.workspace.id);
setState({
...state,
userList: workspaceUserList,
showUpsertWorkspaceUserDialog: false,
});
if (location.hash !== "#members") {
navigate("#members");
}
}}
/>
)}
</>
);
};

View File

@ -1,7 +0,0 @@
interface DialogProps {
destroy: FunctionType;
}
interface DialogCallback {
destroy: FunctionType;
}