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}
/>
)}
</>
);
};