mirror of
https://github.com/aykhans/slash-e.git
synced 2025-09-06 09:14:18 +00:00
chore: update workspace user pages
This commit is contained in:
@@ -189,10 +189,10 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default function showCreateShortcutDialog(workspaceId: WorkspaceId, shortcutId?: ShortcutId): void {
|
||||
export default function showCreateShortcutDialog(workspaceId: WorkspaceId, shortcutId?: ShortcutId, onDestory?: () => void): void {
|
||||
generateDialog(
|
||||
{
|
||||
className: "px-2 sm:px-0",
|
||||
onDestory,
|
||||
},
|
||||
CreateShortcutDialog,
|
||||
{
|
||||
|
@@ -127,13 +127,7 @@ const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
export default function showCreateWorkspaceDialog(workspaceId?: WorkspaceId): void {
|
||||
generateDialog(
|
||||
{
|
||||
className: "px-2 sm:px-0",
|
||||
},
|
||||
CreateWorkspaceDialog,
|
||||
{
|
||||
workspaceId,
|
||||
}
|
||||
);
|
||||
generateDialog({}, CreateWorkspaceDialog, {
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import "../../less/base-dialog.less";
|
||||
interface DialogConfig {
|
||||
className?: string;
|
||||
clickSpaceDestroy?: boolean;
|
||||
onDestory?: () => void;
|
||||
}
|
||||
|
||||
interface Props extends DialogConfig, DialogProps {
|
||||
@@ -38,7 +39,7 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`dialog-wrapper ${className}`} onClick={handleSpaceClicked}>
|
||||
<div className={`dialog-wrapper px-2 sm:px-0 ${className}`} onClick={handleSpaceClicked}>
|
||||
<div className="dialog-container" onClick={(e) => e.stopPropagation()}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -67,6 +68,10 @@ export function generateDialog<T extends DialogProps>(
|
||||
dialog.unmount();
|
||||
tempDiv.remove();
|
||||
}, ANIMATION_DURATION);
|
||||
|
||||
if (config.onDestory) {
|
||||
config.onDestory();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
146
web/src/components/MemberListView.tsx
Normal file
146
web/src/components/MemberListView.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { deleteWorkspaceUser, upsertWorkspaceUser } from "../helpers/api";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { workspaceService } from "../services";
|
||||
import toastHelper from "./Toast";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||
import Icon from "./Icon";
|
||||
|
||||
const userRoles = ["Admin", "User"];
|
||||
|
||||
interface Props {
|
||||
workspaceId: WorkspaceId;
|
||||
workspaceUser: WorkspaceUser;
|
||||
userList: WorkspaceUser[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
workspaceUserList: WorkspaceUser[];
|
||||
}
|
||||
|
||||
const MemberListView: React.FC<Props> = (props: Props) => {
|
||||
const { workspaceId, workspaceUser: currentUser } = props;
|
||||
const [state, setState] = useState<State>({
|
||||
workspaceUserList: [],
|
||||
});
|
||||
const loadingState = useLoading();
|
||||
|
||||
const fetchWorkspaceUserList = () => {
|
||||
loadingState.setLoading();
|
||||
return Promise.all([workspaceService.getWorkspaceUserList(workspaceId)])
|
||||
.then(([workspaceUserList]) => {
|
||||
setState({
|
||||
workspaceUserList: workspaceUserList,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
loadingState.setFinish();
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorkspaceUserList();
|
||||
}, [props]);
|
||||
|
||||
const handleWorkspaceUserRoleChange = async (workspaceUser: WorkspaceUser, role: Role) => {
|
||||
if (workspaceUser.userId === currentUser.userId) {
|
||||
toastHelper.error("Cannot change yourself.");
|
||||
return;
|
||||
}
|
||||
|
||||
await upsertWorkspaceUser({
|
||||
workspaceId: workspaceId,
|
||||
userId: workspaceUser.userId,
|
||||
role,
|
||||
});
|
||||
await fetchWorkspaceUserList();
|
||||
};
|
||||
|
||||
const handleDeleteWorkspaceUserButtonClick = (workspaceUser: WorkspaceUser) => {
|
||||
showCommonDialog({
|
||||
title: "Delete Workspace Member",
|
||||
content: `Are you sure to delete member \`${workspaceUser.user.name}\` in this workspace?`,
|
||||
style: "warning",
|
||||
onConfirm: async () => {
|
||||
await deleteWorkspaceUser({
|
||||
workspaceId: workspaceId,
|
||||
userId: workspaceUser.userId,
|
||||
});
|
||||
await fetchWorkspaceUserList();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
{loadingState.isLoading ? (
|
||||
<></>
|
||||
) : (
|
||||
state.workspaceUserList.map((workspaceUser) => {
|
||||
return (
|
||||
<div key={workspaceUser.userId} 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>{workspaceUser.user.name}</span>
|
||||
{currentUser.userId === workspaceUser.userId && <span className="ml-2 text-gray-400">(yourself)</span>}
|
||||
</div>
|
||||
<div className="flex flex-row justify-end items-center">
|
||||
{currentUser.role === "ADMIN" ? (
|
||||
<>
|
||||
<Dropdown
|
||||
className="mr-4"
|
||||
trigger={
|
||||
<button className="flex flex-row justify-end items-center cursor-pointer">
|
||||
<span className="font-mono">{workspaceUser.role}</span>
|
||||
<Icon.ChevronDown className="ml-1 w-5 h-auto text-gray-600" />
|
||||
</button>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{userRoles.map((role) => {
|
||||
return (
|
||||
<button
|
||||
key={role}
|
||||
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"
|
||||
onClick={() => handleWorkspaceUserRoleChange(workspaceUser, role.toUpperCase() as Role)}
|
||||
>
|
||||
<span className="truncate">{role}</span>
|
||||
{workspaceUser.role === role.toLocaleUpperCase() && <Icon.Check className="w-4 h-auto ml-1 shrink-0" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
}
|
||||
actionsClassName="!w-28 !-left-4"
|
||||
></Dropdown>
|
||||
<Dropdown
|
||||
actions={
|
||||
<>
|
||||
<button
|
||||
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={() => {
|
||||
handleDeleteWorkspaceUserButtonClick(workspaceUser);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
actionsClassName="!w-24"
|
||||
></Dropdown>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-mono">{workspaceUser.role}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemberListView;
|
@@ -1,9 +1,10 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import { shortcutService, workspaceService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
import showCreateShortcutDialog from "./CreateShortcutDialog";
|
||||
import Icon from "./Icon";
|
||||
import showCreateShortcutDialog from "./CreateShortcutDialog";
|
||||
|
||||
interface Props {
|
||||
workspaceId: WorkspaceId;
|
||||
@@ -20,7 +21,14 @@ const ShortcutListView: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
const handleDeleteShortcutButtonClick = (shortcut: Shortcut) => {
|
||||
shortcutService.deleteShortcutById(shortcut.id);
|
||||
showCommonDialog({
|
||||
title: "Delete Shortcut",
|
||||
content: `Are you sure to delete shortcut \`${shortcut.name}\` in this workspace?`,
|
||||
style: "warning",
|
||||
onConfirm: async () => {
|
||||
await shortcutService.deleteShortcutById(shortcut.id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
138
web/src/components/UpsertWorkspaceUserDialog.tsx
Normal file
138
web/src/components/UpsertWorkspaceUserDialog.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
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 {
|
||||
workspaceId: WorkspaceId;
|
||||
}
|
||||
|
||||
interface State {
|
||||
workspaceUserUpsert: WorkspaceUserUpsert;
|
||||
}
|
||||
|
||||
const UpsertWorkspaceUserDialog: React.FC<Props> = (props: Props) => {
|
||||
const { destroy, workspaceId } = props;
|
||||
const [state, setState] = useState<State>({
|
||||
workspaceUserUpsert: {
|
||||
workspaceId: workspaceId,
|
||||
userId: UNKNOWN_ID,
|
||||
role: "USER",
|
||||
},
|
||||
});
|
||||
const requestState = useLoading(false);
|
||||
|
||||
const handleUserIdInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value as string;
|
||||
|
||||
setState({
|
||||
workspaceUserUpsert: Object.assign(state.workspaceUserUpsert, {
|
||||
userId: Number(text),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleUserRoleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value as string;
|
||||
|
||||
setState({
|
||||
workspaceUserUpsert: Object.assign(state.workspaceUserUpsert, {
|
||||
role: text,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (!state.workspaceUserUpsert.userId) {
|
||||
toastHelper.error("User ID is required");
|
||||
return;
|
||||
}
|
||||
|
||||
requestState.setLoading();
|
||||
try {
|
||||
await upsertWorkspaceUser({
|
||||
...state.workspaceUserUpsert,
|
||||
});
|
||||
destroy();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.error || error.response.data.message);
|
||||
}
|
||||
requestState.setFinish();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-full w-80 flex flex-row justify-between items-center mb-4">
|
||||
<p className="text-base">Create Workspace Member</p>
|
||||
<button className="rounded p-1 hover:bg-gray-100" onClick={destroy}>
|
||||
<Icon.X className="w-5 h-auto text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">User ID</span>
|
||||
<input
|
||||
className="w-full rounded border text-sm shadow-inner px-2 py-2"
|
||||
type="number"
|
||||
value={state.workspaceUserUpsert.userId}
|
||||
onChange={handleUserIdInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">Role</span>
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
name="role"
|
||||
id="role-user"
|
||||
value="USER"
|
||||
onChange={handleUserRoleInputChange}
|
||||
checked={state.workspaceUserUpsert.role === "USER"}
|
||||
/>
|
||||
<label htmlFor="role-user" className="ml-1 mr-4">
|
||||
User
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="role"
|
||||
id="role-admin"
|
||||
value="ADMIN"
|
||||
onChange={handleUserRoleInputChange}
|
||||
checked={state.workspaceUserUpsert.role === "ADMIN"}
|
||||
/>
|
||||
<label htmlFor="role-admin" className="ml-1">
|
||||
Admin
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-end items-center">
|
||||
<button
|
||||
disabled={requestState.isLoading}
|
||||
className={`rounded px-3 leading-9 shadow bg-green-600 text-white hover:bg-green-700 ${
|
||||
requestState.isLoading ? "opacity-80" : ""
|
||||
}`}
|
||||
onClick={handleSaveBtnClick}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function showUpsertWorkspaceUserDialog(workspaceId: WorkspaceId, onDestory?: () => void) {
|
||||
return generateDialog(
|
||||
{
|
||||
onDestory,
|
||||
},
|
||||
UpsertWorkspaceUserDialog,
|
||||
{
|
||||
workspaceId,
|
||||
}
|
||||
);
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { workspaceService } from "../services";
|
||||
import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
import showCreateWorkspaceDialog from "./CreateWorkspaceDialog";
|
||||
|
||||
@@ -11,7 +12,14 @@ const WorkspaceListView: React.FC<Props> = (props: Props) => {
|
||||
const { workspaceList } = props;
|
||||
|
||||
const handleDeleteWorkspaceButtonClick = (workspace: Workspace) => {
|
||||
workspaceService.deleteWorkspaceById(workspace.id);
|
||||
showCommonDialog({
|
||||
title: "Delete Workspace",
|
||||
content: `Are you sure to delete workspace \`${workspace.name}\`?`,
|
||||
style: "warning",
|
||||
onConfirm: async () => {
|
||||
await workspaceService.deleteWorkspaceById(workspace.id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
Reference in New Issue
Block a user