chore: update frontend modules

This commit is contained in:
Steven 2023-06-22 18:07:28 +08:00
parent bd627fb250
commit 98fb1264c3
27 changed files with 22 additions and 3884 deletions

4
.gitignore vendored
View File

@ -7,9 +7,7 @@ tmp
# Frontend asset # Frontend asset
web/dist web/dist
web-r
# build folder # build folder
build build
.DS_Store .DS_Store

2
web/.gitignore vendored
View File

@ -3,5 +3,3 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
.yarn/*

View File

@ -6,7 +6,6 @@ import useLoading from "../hooks/useLoading";
import Icon from "./Icon"; import Icon from "./Icon";
interface Props { interface Props {
workspaceId: WorkspaceId;
shortcutId?: ShortcutId; shortcutId?: ShortcutId;
onClose: () => void; onClose: () => void;
onConfirm?: () => void; onConfirm?: () => void;
@ -17,10 +16,9 @@ interface State {
} }
const CreateShortcutDialog: React.FC<Props> = (props: Props) => { const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
const { onClose, onConfirm, workspaceId, shortcutId } = props; const { onClose, onConfirm, shortcutId } = props;
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
shortcutCreate: { shortcutCreate: {
workspaceId: workspaceId,
name: "", name: "",
link: "", link: "",
description: "", description: "",
@ -36,7 +34,6 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
setState({ setState({
...state, ...state,
shortcutCreate: Object.assign(state.shortcutCreate, { shortcutCreate: Object.assign(state.shortcutCreate, {
workspaceId: shortcutTemp.workspaceId,
name: shortcutTemp.name, name: shortcutTemp.name,
link: shortcutTemp.link, link: shortcutTemp.link,
description: shortcutTemp.description, description: shortcutTemp.description,

View File

@ -1,143 +0,0 @@
import { Button, Input, Modal, ModalDialog } from "@mui/joy";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { workspaceService } from "../services";
import useLoading from "../hooks/useLoading";
import Icon from "./Icon";
interface Props {
workspaceId?: WorkspaceId;
onClose: () => void;
onConfirm?: (workspace: Workspace) => void;
}
interface State {
workspaceCreate: WorkspaceCreate;
}
const CreateWorkspaceDialog: React.FC<Props> = (props: Props) => {
const { onClose, onConfirm, workspaceId } = props;
const [state, setState] = useState<State>({
workspaceCreate: {
name: "",
title: "",
description: "",
},
});
const requestState = useLoading(false);
useEffect(() => {
if (workspaceId) {
const workspaceTemp = workspaceService.getWorkspaceById(workspaceId);
if (workspaceTemp) {
setState({
...state,
workspaceCreate: Object.assign(state.workspaceCreate, {
name: workspaceTemp.name,
title: workspaceTemp.title,
description: workspaceTemp.description,
}),
});
}
}
}, [workspaceId]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>, key: string) => {
const text = e.target.value as string;
const tempObject = {} as any;
tempObject[key] = text;
setState({
...state,
workspaceCreate: Object.assign(state.workspaceCreate, tempObject),
});
};
const handleSaveBtnClick = async () => {
if (!state.workspaceCreate.name) {
toast.error("ID is required");
return;
}
if (!state.workspaceCreate.title) {
toast.error("Title is required");
return;
}
requestState.setLoading();
try {
let workspace;
if (workspaceId) {
workspace = await workspaceService.patchWorkspace({
id: workspaceId,
...state.workspaceCreate,
});
} else {
workspace = await workspaceService.createWorkspace({
...state.workspaceCreate,
});
}
if (onConfirm) {
onConfirm(workspace);
} else {
onClose();
}
} catch (error: any) {
console.error(error);
toast.error(JSON.stringify(error.response.data));
}
requestState.setFinish();
};
return (
<Modal open={true}>
<ModalDialog>
<div className="flex flex-row justify-between items-center w-80">
<span className="text-lg font-medium">{workspaceId ? "Edit Workspace" : "Create Workspace"}</span>
<Button variant="plain" onClick={onClose}>
<Icon.X className="w-5 h-auto text-gray-600" />
</Button>
</div>
<div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
ID <span className="text-red-600">*</span>
</span>
<Input
className="w-full"
type="text"
placeholder="Workspace ID is an unique identifier for your workspace."
value={state.workspaceCreate.name}
onChange={(e) => handleInputChange(e, "name")}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">
Name <span className="text-red-600">*</span>
</span>
<Input className="w-full" type="text" value={state.workspaceCreate.title} onChange={(e) => handleInputChange(e, "title")} />
</div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">Description</span>
<Input
className="w-full"
type="text"
value={state.workspaceCreate.description}
onChange={(e) => handleInputChange(e, "description")}
/>
</div>
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
<Button color="neutral" variant="plain" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onClose}>
Cancel
</Button>
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
Save
</Button>
</div>
</div>
</ModalDialog>
</Modal>
);
};
export default CreateWorkspaceDialog;

View File

@ -1,31 +1,12 @@
import { useState } from "react"; import { Link, useNavigate } from "react-router-dom";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useAppSelector } from "../store"; import { useAppSelector } from "../store";
import { userService } from "../services"; import { userService } from "../services";
import Icon from "./Icon"; import Icon from "./Icon";
import Dropdown from "./common/Dropdown"; import Dropdown from "./common/Dropdown";
import CreateWorkspaceDialog from "./CreateWorkspaceDialog";
interface State {
showCreateWorkspaceDialog: boolean;
}
const Header: React.FC = () => { const Header: React.FC = () => {
const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAppSelector((state) => state.user); 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 () => { const handleSignOutButtonClick = async () => {
await userService.doSignOut(); await userService.doSignOut();
@ -41,43 +22,6 @@ const Header: React.FC = () => {
<img src="/logo.png" className="w-8 h-auto mr-2" alt="" /> <img src="/logo.png" className="w-8 h-auto mr-2" alt="" />
Shortify Shortify
</Link> </Link>
{workspaceList.length > 0 && activedWorkspace !== undefined && (
<>
<span className="font-mono mx-1 text-gray-200">/</span>
<Dropdown
trigger={
<button className="flex flex-row justify-end items-center cursor-pointer">
<span className="font-mono">{activedWorkspace?.title}</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.title}</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>
<div className="relative flex-shrink-0"> <div className="relative flex-shrink-0">
{user ? ( {user ? (
@ -114,24 +58,6 @@ const Header: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
{state.showCreateWorkspaceDialog && (
<CreateWorkspaceDialog
onClose={() => {
setState({
...state,
showCreateWorkspaceDialog: false,
});
}}
onConfirm={(workspace: Workspace) => {
setState({
...state,
showCreateWorkspaceDialog: false,
});
navigate(`/${workspace.name}`);
}}
/>
)}
</> </>
); );
}; };

View File

@ -1,136 +0,0 @@
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import { deleteWorkspaceUser, upsertWorkspaceUser } from "../helpers/api";
import { useAppSelector } from "../store";
import { unknownWorkspace, unknownWorkspaceUser } from "../store/modules/workspace";
import useLoading from "../hooks/useLoading";
import { workspaceService } from "../services";
import Dropdown from "./common/Dropdown";
import { showCommonDialog } from "./Alert";
import Icon from "./Icon";
const userRoles = ["Admin", "User"];
interface Props {
workspaceId: WorkspaceId;
}
const MemberListView: React.FC<Props> = (props: Props) => {
const { workspaceId } = props;
const user = useAppSelector((state) => state.user.user) as User;
const { workspaceList } = useAppSelector((state) => state.workspace);
const workspace = workspaceList.find((workspace) => workspace.id === workspaceId) ?? unknownWorkspace;
const currentUser = workspace.workspaceUserList.find((workspaceUser) => workspaceUser.userId === user.id) ?? unknownWorkspaceUser;
const loadingState = useLoading();
useEffect(() => {
const workspace = workspaceService.getWorkspaceById(workspaceId);
if (!workspace) {
toast.error("workspace not found");
return;
}
loadingState.setFinish();
}, []);
const handleWorkspaceUserRoleChange = async (workspaceUser: WorkspaceUser, role: Role) => {
if (workspaceUser.userId === currentUser.userId) {
toast.error("Cannot change yourself.");
return;
}
await upsertWorkspaceUser({
workspaceId: workspaceId,
userId: workspaceUser.userId,
role,
});
await workspaceService.fetchWorkspaceById(workspaceId);
};
const handleDeleteWorkspaceUserButtonClick = (workspaceUser: WorkspaceUser) => {
showCommonDialog({
title: "Delete Workspace Member",
content: `Are you sure to delete member \`${workspaceUser.displayName}\` in this workspace?`,
style: "danger",
onConfirm: async () => {
await deleteWorkspaceUser({
workspaceId: workspaceId,
userId: workspaceUser.userId,
});
await workspaceService.fetchWorkspaceById(workspaceId);
},
});
};
return (
<div className="w-full flex flex-col justify-start items-start">
{loadingState.isLoading ? (
<></>
) : (
workspace.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.displayName}</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;

View File

@ -3,9 +3,8 @@ import copy from "copy-to-clipboard";
import { useState } from "react"; import { useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { UNKNOWN_ID } from "../helpers/consts"; import { UNKNOWN_ID } from "../helpers/consts";
import { shortcutService, workspaceService } from "../services"; import { shortcutService } from "../services";
import { useAppSelector } from "../store"; import { useAppSelector } from "../store";
import { unknownWorkspace, unknownWorkspaceUser } from "../store/modules/workspace";
import { absolutifyLink } from "../helpers/utils"; import { absolutifyLink } from "../helpers/utils";
import { showCommonDialog } from "./Alert"; import { showCommonDialog } from "./Alert";
import Icon from "./Icon"; import Icon from "./Icon";
@ -13,7 +12,6 @@ import Dropdown from "./common/Dropdown";
import CreateShortcutDialog from "./CreateShortcutDialog"; import CreateShortcutDialog from "./CreateShortcutDialog";
interface Props { interface Props {
workspaceId: WorkspaceId;
shortcutList: Shortcut[]; shortcutList: Shortcut[];
} }
@ -22,22 +20,18 @@ interface State {
} }
const ShortcutListView: React.FC<Props> = (props: Props) => { const ShortcutListView: React.FC<Props> = (props: Props) => {
const { workspaceId, shortcutList } = props; const { shortcutList } = props;
const user = useAppSelector((state) => state.user.user as User); const user = useAppSelector((state) => state.user.user as User);
const { workspaceList } = useAppSelector((state) => state.workspace);
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
currentEditingShortcutId: UNKNOWN_ID, currentEditingShortcutId: UNKNOWN_ID,
}); });
const workspace = workspaceList.find((workspace) => workspace.id === workspaceId) ?? unknownWorkspace;
const workspaceUser = workspace.workspaceUserList.find((workspaceUser) => workspaceUser.userId === user.id) ?? unknownWorkspaceUser;
const havePermission = (shortcut: Shortcut) => { const havePermission = (shortcut: Shortcut) => {
return workspaceUser.role === "ADMIN" || shortcut.creatorId === user.id; return user.role === "ADMIN" || shortcut.creatorId === user.id;
}; };
const handleCopyButtonClick = (shortcut: Shortcut) => { const handleCopyButtonClick = (shortcut: Shortcut) => {
const workspace = workspaceService.getWorkspaceById(workspaceId); copy(absolutifyLink(`/${shortcut.name}`));
copy(absolutifyLink(`/${workspace?.name}/${shortcut.name}`));
toast.success("Shortcut link copied to clipboard."); toast.success("Shortcut link copied to clipboard.");
}; };
@ -117,7 +111,6 @@ const ShortcutListView: React.FC<Props> = (props: Props) => {
{state.currentEditingShortcutId !== UNKNOWN_ID && ( {state.currentEditingShortcutId !== UNKNOWN_ID && (
<CreateShortcutDialog <CreateShortcutDialog
workspaceId={workspaceId}
shortcutId={state.currentEditingShortcutId} shortcutId={state.currentEditingShortcutId}
onClose={() => { onClose={() => {
setState({ setState({

View File

@ -1,116 +0,0 @@
import { Button, Input, Modal, ModalDialog, Radio, RadioGroup } from "@mui/joy";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { workspaceService } from "../services";
import { UNKNOWN_ID } from "../helpers/consts";
import { upsertWorkspaceUser } from "../helpers/api";
import useLoading from "../hooks/useLoading";
import Icon from "./Icon";
interface Props {
workspaceId: WorkspaceId;
onClose: () => void;
onConfirm?: () => void;
}
interface State {
workspaceUserUpsert: WorkspaceUserUpsert;
}
const UpsertWorkspaceUserDialog: React.FC<Props> = (props: Props) => {
const { onClose, onConfirm, 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) {
toast.error("User ID is required");
return;
}
requestState.setLoading();
try {
await upsertWorkspaceUser({
...state.workspaceUserUpsert,
});
await workspaceService.fetchWorkspaceById(workspaceId);
if (onConfirm) {
onConfirm();
} else {
onClose();
}
} catch (error: any) {
console.error(error);
toast.error(JSON.stringify(error.response.data));
}
requestState.setFinish();
};
return (
<Modal open={true}>
<ModalDialog>
<div className="flex flex-row justify-between items-center w-80">
<span className="text-lg font-medium">Create Workspace Member</span>
<Button variant="plain" onClick={onClose}>
<Icon.X className="w-5 h-auto text-gray-600" />
</Button>
</div>
<div>
<div className="w-full flex flex-col justify-start items-start mb-3">
<span className="mb-2">User ID</span>
<Input
className="w-full"
type="number"
value={state.workspaceUserUpsert.userId <= 0 ? "" : 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>
<RadioGroup orientation="horizontal" value={state.workspaceUserUpsert.role} onChange={handleUserRoleInputChange}>
<Radio value="USER" label="User" />
<Radio value="ADMIN" label="Admin" />
</RadioGroup>
</div>
</div>
<div className="w-full flex flex-row justify-end items-center">
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
Save
</Button>
</div>
</div>
</ModalDialog>
</Modal>
);
};
export default UpsertWorkspaceUserDialog;

View File

@ -1,94 +0,0 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { UNKNOWN_ID } from "../helpers/consts";
import { workspaceService } from "../services";
import { showCommonDialog } from "./Alert";
import Dropdown from "./common/Dropdown";
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({
title: "Delete Workspace",
content: `Are you sure to delete workspace \`${workspace.name}\`?`,
style: "danger",
onConfirm: async () => {
await workspaceService.deleteWorkspaceById(workspace.id);
},
});
};
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>
<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>
);
})}
</div>
{state.currentEditingWorkspaceId !== UNKNOWN_ID && (
<CreateWorkspaceDialog
workspaceId={state.currentEditingWorkspaceId}
onClose={() => {
setState({
...state,
currentEditingWorkspaceId: UNKNOWN_ID,
});
}}
/>
)}
</>
);
};
export default WorkspaceListView;

View File

@ -1,189 +0,0 @@
import { Button } from "@mui/joy";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-hot-toast";
import { deleteWorkspaceUser } from "../helpers/api";
import useLoading from "../hooks/useLoading";
import { workspaceService } from "../services";
import { useAppSelector } from "../store";
import { unknownWorkspace, unknownWorkspaceUser } from "../store/modules/workspace";
import { showCommonDialog } from "./Alert";
import Icon from "./Icon";
import CreateWorkspaceDialog from "./CreateWorkspaceDialog";
import UpsertWorkspaceUserDialog from "./UpsertWorkspaceUserDialog";
import MemberListView from "./MemberListView";
interface Props {
workspaceId: WorkspaceId;
}
interface State {
showEditWorkspaceDialog: boolean;
showUpsertWorkspaceUserDialog: boolean;
}
const WorkspaceSetting: React.FC<Props> = (props: Props) => {
const { workspaceId } = props;
const navigate = useNavigate();
const user = useAppSelector((state) => state.user.user) as User;
const [state, setState] = useState<State>({
showEditWorkspaceDialog: false,
showUpsertWorkspaceUserDialog: false,
});
const { workspaceList } = useAppSelector((state) => state.workspace);
const loadingState = useLoading();
const workspace = workspaceList.find((workspace) => workspace.id === workspaceId) ?? unknownWorkspace;
const workspaceUser = workspace.workspaceUserList.find((workspaceUser) => workspaceUser.userId === user.id) ?? unknownWorkspaceUser;
useEffect(() => {
const workspace = workspaceService.getWorkspaceById(workspaceId);
if (!workspace) {
toast.error("workspace not found");
return;
}
loadingState.setFinish();
}, []);
const handleEditWorkspaceButtonClick = () => {
setState({
...state,
showEditWorkspaceDialog: true,
});
};
const handleUpsertWorkspaceMemberButtonClick = () => {
setState({
...state,
showUpsertWorkspaceUserDialog: true,
});
};
const handleEditWorkspaceDialogConfirm = async () => {
setState({
...state,
showEditWorkspaceDialog: false,
});
const workspace = await workspaceService.fetchWorkspaceById(workspaceId);
navigate(`/${workspace.name}#setting`);
};
const handleDeleteWorkspaceButtonClick = () => {
showCommonDialog({
title: "Delete Workspace",
content: `Are you sure to delete workspace \`${workspace.name}\`?`,
style: "danger",
onConfirm: async () => {
await workspaceService.deleteWorkspaceById(workspace.id);
navigate("/");
},
});
};
const handleExitWorkspaceButtonClick = () => {
showCommonDialog({
title: "Exit Workspace",
content: `Are you sure to exit workspace \`${workspace.name}\`?`,
style: "danger",
onConfirm: async () => {
await deleteWorkspaceUser({
workspaceId: workspace.id,
userId: workspaceUser.userId,
});
navigate("/");
},
});
};
return (
<>
<div className="w-full flex flex-col justify-start items-start">
<span className="w-full text-2xl font-medium border-b pb-2 mb-4">General</span>
<p className="mb-4">ID: {workspace.name}</p>
<p className="mb-4">Name: {workspace.title}</p>
<p className="mb-4">Description: {workspace.description || "No description."}</p>
{workspaceUser.role === "ADMIN" && (
<div className="flex flex-row justify-start items-center">
<div className="flex flex-row justify-start items-center space-x-2">
<Button variant="soft" onClick={handleEditWorkspaceButtonClick}>
<Icon.Edit className="w-4 h-auto mr-1" />
Edit
</Button>
</div>
</div>
)}
</div>
<div className="w-full mt-8 flex flex-col justify-start items-start">
<div className="w-full border-b pb-2 mb-4 flex flex-row justify-between items-center">
<span className="text-2xl font-medium">Members</span>
{workspaceUser.role === "ADMIN" && (
<Button variant="soft" onClick={handleUpsertWorkspaceMemberButtonClick}>
<Icon.Plus className="w-4 h-auto mr-1" />
New member
</Button>
)}
</div>
<MemberListView workspaceId={workspaceId} />
</div>
<div className="w-full mt-8 flex flex-col justify-start items-start">
<span className="w-full text-2xl font-medium border-b pb-2 mb-4">Danger Zone</span>
<div className="flex flex-row justify-start items-center">
<div className="flex flex-row justify-start items-center space-x-2">
{workspaceUser.role === "ADMIN" ? (
<>
<Button variant="soft" color="danger" onClick={handleDeleteWorkspaceButtonClick}>
<Icon.Trash className="w-4 h-auto mr-1" />
Delete
</Button>
</>
) : (
<>
<Button variant="soft" color="danger" onClick={handleExitWorkspaceButtonClick}>
Exit
</Button>
</>
)}
</div>
</div>
</div>
{state.showEditWorkspaceDialog && (
<CreateWorkspaceDialog
workspaceId={workspace.id}
onClose={() => {
setState({
...state,
showEditWorkspaceDialog: false,
});
}}
onConfirm={() => handleEditWorkspaceDialogConfirm()}
/>
)}
{state.showUpsertWorkspaceUserDialog && (
<UpsertWorkspaceUserDialog
workspaceId={workspace.id}
onClose={() => {
setState({
...state,
showUpsertWorkspaceUserDialog: false,
});
}}
onConfirm={async () => {
setState({
...state,
showUpsertWorkspaceUserDialog: false,
});
if (location.hash !== "#members") {
navigate("#members");
}
}}
/>
)}
</>
);
};
export default WorkspaceSetting;

View File

@ -53,59 +53,11 @@ export function deleteUser(userDelete: UserDelete) {
return axios.delete(`/api/user/${userDelete.id}`); return axios.delete(`/api/user/${userDelete.id}`);
} }
export function getWorkspaceList(find?: WorkspaceFind) {
const queryList = [];
if (find?.creatorId) {
queryList.push(`creatorId=${find.creatorId}`);
}
if (find?.memberId) {
queryList.push(`memberId=${find.memberId}`);
}
return axios.get<ResponseObject<Workspace[]>>(`/api/workspace?${queryList.join("&")}`);
}
export function getWorkspaceById(workspaceId: WorkspaceId) {
return axios.get<ResponseObject<Workspace>>(`/api/workspace/${workspaceId}`);
}
export function createWorkspace(create: WorkspaceCreate) {
return axios.post<ResponseObject<Workspace>>("/api/workspace", create);
}
export function patchWorkspace(patch: WorkspacePatch) {
return axios.patch<ResponseObject<Workspace>>(`/api/workspace/${patch.id}`, patch);
}
export function deleteWorkspaceById(workspaceId: WorkspaceId) {
return axios.delete(`/api/workspace/${workspaceId}`);
}
export function upsertWorkspaceUser(upsert: WorkspaceUserUpsert) {
return axios.post<ResponseObject<WorkspaceUser>>(`/api/workspace/${upsert.workspaceId}/user`, upsert);
}
export function getWorkspaceUserList(workspaceUserFind?: WorkspaceUserFind) {
return axios.get<ResponseObject<WorkspaceUser[]>>(`/api/workspace/${workspaceUserFind?.workspaceId}/user`);
}
export function getWorkspaceUser(workspaceUserFind?: WorkspaceUserFind) {
return axios.get<ResponseObject<WorkspaceUser>>(
`/api/workspace/${workspaceUserFind?.workspaceId}/user/${workspaceUserFind?.userId ?? ""}`
);
}
export function deleteWorkspaceUser(workspaceUserDelete: WorkspaceUserDelete) {
return axios.delete(`/api/workspace/${workspaceUserDelete.workspaceId}/user/${workspaceUserDelete.userId}`);
}
export function getShortcutList(shortcutFind?: ShortcutFind) { export function getShortcutList(shortcutFind?: ShortcutFind) {
const queryList = []; const queryList = [];
if (shortcutFind?.creatorId) { if (shortcutFind?.creatorId) {
queryList.push(`creatorId=${shortcutFind.creatorId}`); queryList.push(`creatorId=${shortcutFind.creatorId}`);
} }
if (shortcutFind?.workspaceId) {
queryList.push(`workspaceId=${shortcutFind.workspaceId}`);
}
return axios.get<ResponseObject<Shortcut[]>>(`/api/shortcut?${queryList.join("&")}`); return axios.get<ResponseObject<Shortcut[]>>(`/api/shortcut?${queryList.join("&")}`);
} }

View File

@ -1,15 +0,0 @@
(() => {
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (str: any, newStr: any) {
// If a regex pattern
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
return this.replace(str, newStr);
}
// If a string
return this.replace(new RegExp(str, "g"), newStr);
};
}
})();
export default null;

View File

@ -2,7 +2,6 @@ import { createRoot } from "react-dom/client";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import store from "./store"; import store from "./store";
import App from "./App"; import App from "./App";
import "./helpers/polyfill";
import "./css/index.css"; import "./css/index.css";
const container = document.getElementById("root"); const container = document.getElementById("root");

View File

@ -9,7 +9,7 @@ import useLoading from "../hooks/useLoading";
import Icon from "../components/Icon"; import Icon from "../components/Icon";
const validateConfig: ValidatorConfig = { const validateConfig: ValidatorConfig = {
minLength: 4, minLength: 3,
maxLength: 24, maxLength: 24,
noSpace: true, noSpace: true,
noChinese: true, noChinese: true,

View File

@ -1,23 +1,15 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { userService, workspaceService } from "../services"; import { userService, shortcutService } from "../services";
import { useAppSelector } from "../store"; import { useAppSelector } from "../store";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import Icon from "../components/Icon"; import Icon from "../components/Icon";
import WorkspaceListView from "../components/WorkspaceListView"; import ShortcutListView from "../components/ShortcutListView";
import CreateWorkspaceDialog from "../components/CreateWorkspaceDialog";
interface State {
showCreateWorkspaceDialog: boolean;
}
const Home: React.FC = () => { const Home: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { workspaceList } = useAppSelector((state) => state.workspace);
const [state, setState] = useState<State>({
showCreateWorkspaceDialog: false,
});
const loadingState = useLoading(); const loadingState = useLoading();
const { shortcutList } = useAppSelector((state) => state.shortcut);
useEffect(() => { useEffect(() => {
if (!userService.getState().user) { if (!userService.getState().user) {
@ -25,23 +17,11 @@ const Home: React.FC = () => {
return; return;
} }
Promise.all([workspaceService.fetchWorkspaceList()]).finally(() => { Promise.all([shortcutService.getMyAllShortcuts()]).finally(() => {
const workspaceList = workspaceService.getState().workspaceList;
if (workspaceList.length > 0) {
navigate(`/${workspaceList[0].name}`);
return;
}
loadingState.setFinish(); loadingState.setFinish();
}); });
}, []); }, []);
const handleCreateWorkspaceButtonClick = () => {
setState({
...state,
showCreateWorkspaceDialog: true,
});
};
return ( return (
<> <>
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start"> <div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start">
@ -53,39 +33,10 @@ const Home: React.FC = () => {
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" /> <Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
loading loading
</div> </div>
) : workspaceList.length === 0 ? (
<div className="w-full flex flex-col justify-center items-center">
<Icon.Frown className="mt-8 w-16 h-auto text-gray-400" />
<p className="mt-4 text-xl text-gray-600">Oops, no workspace.</p>
<button
className="mt-4 text-lg flex flex-row justify-start items-center border px-3 py-2 rounded-lg cursor-pointer hover:shadow"
onClick={handleCreateWorkspaceButtonClick}
>
<Icon.Plus className="w-5 h-auto mr-1" /> Create Workspace
</button>
</div>
) : ( ) : (
<WorkspaceListView workspaceList={workspaceList} /> <ShortcutListView shortcutList={shortcutList} />
)} )}
</div> </div>
{state.showCreateWorkspaceDialog && (
<CreateWorkspaceDialog
onClose={() => {
setState({
...state,
showCreateWorkspaceDialog: false,
});
}}
onConfirm={(workspace: Workspace) => {
setState({
...state,
showCreateWorkspaceDialog: false,
});
navigate(`/${workspace.name}`);
}}
/>
)}
</> </>
); );
}; };

View File

@ -1,153 +0,0 @@
import { useEffect, useState } from "react";
import { NavLink, useLocation, useNavigate, useParams } from "react-router-dom";
import toast from "react-hot-toast";
import { shortcutService, userService } from "../services";
import { useAppSelector } from "../store";
import { unknownWorkspace } from "../store/modules/workspace";
import useLoading from "../hooks/useLoading";
import Icon from "../components/Icon";
import Dropdown from "../components/common/Dropdown";
import ShortcutListView from "../components/ShortcutListView";
import WorkspaceSetting from "../components/WorkspaceSetting";
import CreateShortcutDialog from "../components/CreateShortcutDialog";
interface State {
showCreateShortcutDialog: boolean;
}
const WorkspaceDetail: React.FC = () => {
const navigate = useNavigate();
const params = useParams();
const location = useLocation();
const { workspaceList } = useAppSelector((state) => state.workspace);
const { shortcutList } = useAppSelector((state) => state.shortcut);
const [state, setState] = useState<State>({
showCreateShortcutDialog: false,
});
const loadingState = useLoading();
const workspace = workspaceList.find((workspace) => workspace.name === params.workspaceName) ?? unknownWorkspace;
useEffect(() => {
if (!userService.getState().user) {
navigate("/user/auth");
return;
}
if (!workspace) {
toast.error("workspace not found");
return;
}
Promise.all([shortcutService.fetchWorkspaceShortcuts(workspace.id)]).finally(() => {
loadingState.setFinish();
});
}, [params.workspaceName]);
useEffect(() => {
if (location.hash !== "#shortcuts" && location.hash !== "#setting") {
navigate("#shortcuts");
}
}, [location.hash]);
const handleCreateShortcutButtonClick = () => {
setState({
...state,
showCreateShortcutDialog: true,
});
};
return (
<>
<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-3 sm:space-x-4">
<NavLink
to="#shortcuts"
className={`py-1 text-gray-400 border-b-2 border-b-transparent ${
location.hash === "#shortcuts" && "!border-b-black text-black"
}`}
>
Shortcuts
</NavLink>
<NavLink
to="#setting"
className={`py-1 text-gray-400 border-b-2 border-b-transparent ${
location.hash === "#setting" && "!border-b-black text-black"
}`}
>
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
</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" &&
(shortcutList.length === 0 ? (
<div className="w-full flex flex-col justify-center items-center">
<Icon.Frown className="mt-8 w-16 h-auto text-gray-400" />
<p className="mt-4 text-xl text-gray-600">Oops, no shortcut.</p>
<button
className="mt-4 text-lg flex flex-row justify-start items-center border px-3 py-2 rounded-lg cursor-pointer hover:shadow"
onClick={handleCreateShortcutButtonClick}
>
<Icon.Plus className="w-5 h-auto mr-1" /> Create Shortcut
</button>
</div>
) : (
<ShortcutListView workspaceId={workspace.id} shortcutList={shortcutList} />
))}
{location.hash === "#setting" && <WorkspaceSetting workspaceId={workspace.id} />}
</>
)}
</div>
{state.showCreateShortcutDialog && (
<CreateShortcutDialog
workspaceId={workspace.id}
onClose={() => {
setState({
...state,
showCreateShortcutDialog: false,
});
}}
onConfirm={() => {
setState({
...state,
showCreateShortcutDialog: false,
});
if (location.hash !== "#shortcuts") {
navigate("#shortcuts");
}
}}
/>
)}
</>
);
};
export default WorkspaceDetail;

View File

@ -1,11 +1,10 @@
import { createBrowserRouter, redirect } from "react-router-dom"; import { createBrowserRouter, redirect } from "react-router-dom";
import { isNullorUndefined } from "../helpers/utils"; import { isNullorUndefined } from "../helpers/utils";
import { userService, workspaceService } from "../services"; import { userService } from "../services";
import Root from "../layout/Root"; import Root from "../layout/Root";
import Auth from "../pages/Auth"; import Auth from "../pages/Auth";
import Home from "../pages/Home"; import Home from "../pages/Home";
import UserDetail from "../pages/UserDetail"; import UserDetail from "../pages/UserDetail";
import WorkspaceDetail from "../pages/WorkspaceDetail";
import ShortcutRedirector from "../pages/ShortcutRedirector"; import ShortcutRedirector from "../pages/ShortcutRedirector";
const router = createBrowserRouter([ const router = createBrowserRouter([
@ -43,23 +42,6 @@ const router = createBrowserRouter([
// do nth // do nth
} }
const { user } = userService.getState();
if (isNullorUndefined(user)) {
return redirect("/user/auth");
}
},
},
{
path: "/:workspaceName",
element: <WorkspaceDetail />,
loader: async () => {
try {
await userService.initialState();
await workspaceService.fetchWorkspaceList();
} catch (error) {
// do nth
}
const { user } = userService.getState(); const { user } = userService.getState();
if (isNullorUndefined(user)) { if (isNullorUndefined(user)) {
return redirect("/user/auth"); return redirect("/user/auth");
@ -69,7 +51,7 @@ const router = createBrowserRouter([
], ],
}, },
{ {
path: "/:workspaceName/:shortcutName", path: "/:shortcutName",
element: <ShortcutRedirector />, element: <ShortcutRedirector />,
}, },
]); ]);

View File

@ -1,6 +1,5 @@
import globalService from "./globalService"; import globalService from "./globalService";
import shortcutService from "./shortcutService"; import shortcutService from "./shortcutService";
import userService from "./userService"; import userService from "./userService";
import workspaceService from "./workspaceService";
export { globalService, shortcutService, userService, workspaceService }; export { globalService, shortcutService, userService };

View File

@ -15,12 +15,8 @@ const shortcutService = {
return store.getState().shortcut; return store.getState().shortcut;
}, },
fetchWorkspaceShortcuts: async (workspaceId: WorkspaceId) => { fetchWorkspaceShortcuts: async () => {
const { data } = ( const { data } = (await api.getShortcutList({})).data;
await api.getShortcutList({
workspaceId,
})
).data;
const shortcuts = data.map((s) => convertResponseModelShortcut(s)); const shortcuts = data.map((s) => convertResponseModelShortcut(s));
store.dispatch(setShortcuts(shortcuts)); store.dispatch(setShortcuts(shortcuts));
return shortcuts; return shortcuts;

View File

@ -1,91 +0,0 @@
import * as api from "../helpers/api";
import store from "../store";
import { createWorkspace, deleteWorkspace, patchWorkspace, setWorkspaceById, setWorkspaceList } from "../store/modules/workspace";
const convertResponseModelWorkspace = (workspace: Workspace): Workspace => {
return {
...workspace,
createdTs: workspace.createdTs * 1000,
updatedTs: workspace.updatedTs * 1000,
};
};
const workspaceService = {
getState: () => {
return store.getState().workspace;
},
fetchWorkspaceList: async () => {
const { data } = (await api.getWorkspaceList()).data;
const workspaces = data.map((w) => convertResponseModelWorkspace(w));
store.dispatch(setWorkspaceList(workspaces));
return workspaces;
},
fetchWorkspaceById: async (workspaceId: WorkspaceId) => {
const { data } = (await api.getWorkspaceById(workspaceId)).data;
const workspace = convertResponseModelWorkspace(data);
store.dispatch(setWorkspaceById(workspace));
return workspace;
},
getWorkspaceByName: (workspaceName: string) => {
const workspaceList = workspaceService.getState().workspaceList;
for (const workspace of workspaceList) {
if (workspace.name === workspaceName) {
return workspace;
}
}
return undefined;
},
getWorkspaceById: (id: WorkspaceId) => {
const workspaceList = workspaceService.getState().workspaceList;
for (const workspace of workspaceList) {
if (workspace.id === id) {
return workspace;
}
}
return undefined;
},
createWorkspace: async (create: WorkspaceCreate) => {
const { data } = (await api.createWorkspace(create)).data;
const workspace = convertResponseModelWorkspace(data);
store.dispatch(createWorkspace(workspace));
return workspace;
},
patchWorkspace: async (patch: WorkspacePatch) => {
const { data } = (await api.patchWorkspace(patch)).data;
const workspace = convertResponseModelWorkspace(data);
store.dispatch(patchWorkspace(workspace));
return workspace;
},
deleteWorkspaceById: async (id: WorkspaceId) => {
await api.deleteWorkspaceById(id);
store.dispatch(deleteWorkspace(id));
},
getWorkspaceUserList: async (id: WorkspaceId) => {
const { data } = (
await api.getWorkspaceUserList({
workspaceId: id,
})
).data;
return data;
},
getWorkspaceUser: async (workspaceId: WorkspaceId, userId: UserId) => {
const { data } = (
await api.getWorkspaceUser({
workspaceId: workspaceId,
userId: userId,
})
).data;
return data;
},
};
export default workspaceService;

View File

@ -2,14 +2,12 @@ import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useSelector } from "react-redux"; import { TypedUseSelectorHook, useSelector } from "react-redux";
import globalReducer from "./modules/global"; import globalReducer from "./modules/global";
import userReducer from "./modules/user"; import userReducer from "./modules/user";
import workspaceReducer from "./modules/workspace";
import shortcutReducer from "./modules/shortcut"; import shortcutReducer from "./modules/shortcut";
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
global: globalReducer, global: globalReducer,
user: userReducer, user: userReducer,
workspace: workspaceReducer,
shortcut: shortcutReducer, shortcut: shortcutReducer,
}, },
}); });

View File

@ -1,75 +0,0 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { UNKNOWN_ID } from "../../helpers/consts";
export const unknownWorkspace = {
id: UNKNOWN_ID,
workspaceUserList: [],
} as unknown as Workspace;
export const unknownWorkspaceUser = {
workspaceId: UNKNOWN_ID,
userId: UNKNOWN_ID,
role: "USER",
} as unknown as WorkspaceUser;
interface State {
workspaceList: Workspace[];
}
const workspaceSlice = createSlice({
name: "workspace",
initialState: {
workspaceList: [],
} as State,
reducers: {
setWorkspaceList: (state, action: PayloadAction<Workspace[]>) => {
return {
...state,
workspaceList: action.payload,
};
},
setWorkspaceById: (state, action: PayloadAction<Workspace>) => {
return {
...state,
workspaceList: state.workspaceList.map((s) => {
if (s.id === action.payload.id) {
return action.payload;
} else {
return s;
}
}),
};
},
createWorkspace: (state, action: PayloadAction<Workspace>) => {
return {
...state,
workspaceList: state.workspaceList.concat(action.payload).sort((a, b) => b.createdTs - a.createdTs),
};
},
patchWorkspace: (state, action: PayloadAction<Partial<Workspace>>) => {
return {
...state,
workspaceList: state.workspaceList.map((s) => {
if (s.id === action.payload.id) {
return {
...s,
...action.payload,
};
} else {
return s;
}
}),
};
},
deleteWorkspace: (state, action: PayloadAction<WorkspaceId>) => {
return {
...state,
workspaceList: [...state.workspaceList].filter((workspace) => workspace.id !== action.payload),
};
},
},
});
export const { setWorkspaceList, setWorkspaceById, createWorkspace, patchWorkspace, deleteWorkspace } = workspaceSlice.actions;
export default workspaceSlice.reducer;

View File

@ -1,28 +0,0 @@
type Role = "ADMIN" | "USER";
interface WorkspaceUser {
workspaceId: WorkspaceId;
userId: UserId;
role: Role;
createdTs: TimeStamp;
updatedTs: TimeStamp;
email: string;
displayName: string;
}
interface WorkspaceUserUpsert {
workspaceId: WorkspaceId;
userId: UserId;
role: Role;
updatedTs?: TimeStamp;
}
interface WorkspaceUserFind {
workspaceId: WorkspaceId;
userId?: UserId;
}
interface WorkspaceUserDelete {
workspaceId: WorkspaceId;
userId: UserId;
}

View File

@ -9,7 +9,6 @@ interface Shortcut {
creator: User; creator: User;
createdTs: TimeStamp; createdTs: TimeStamp;
updatedTs: TimeStamp; updatedTs: TimeStamp;
workspaceId: WorkspaceId;
rowStatus: RowStatus; rowStatus: RowStatus;
name: string; name: string;
@ -19,8 +18,6 @@ interface Shortcut {
} }
interface ShortcutCreate { interface ShortcutCreate {
workspaceId: WorkspaceId;
name: string; name: string;
link: string; link: string;
description: string; description: string;
@ -38,5 +35,4 @@ interface ShortcutPatch {
interface ShortcutFind { interface ShortcutFind {
creatorId?: UserId; creatorId?: UserId;
workspaceId?: WorkspaceId;
} }

View File

@ -1,5 +1,7 @@
type UserId = number; type UserId = number;
type Role = "ADMIN" | "USER";
interface User { interface User {
id: UserId; id: UserId;
@ -10,6 +12,7 @@ interface User {
email: string; email: string;
displayName: string; displayName: string;
openId: string; openId: string;
role: Role;
} }
interface UserCreate { interface UserCreate {
@ -22,7 +25,6 @@ interface UserPatch {
id: UserId; id: UserId;
rowStatus?: RowStatus; rowStatus?: RowStatus;
displayName?: string; displayName?: string;
password?: string; password?: string;
resetOpenId?: boolean; resetOpenId?: boolean;

View File

@ -1,35 +0,0 @@
type WorkspaceId = number;
interface Workspace {
id: WorkspaceId;
creatorId: UserId;
createdTs: TimeStamp;
updatedTs: TimeStamp;
rowStatus: RowStatus;
name: string;
title: string;
description: string;
workspaceUserList: WorkspaceUser[];
}
interface WorkspaceCreate {
name: string;
title: string;
description: string;
}
interface WorkspacePatch {
id: WorkspaceId;
rowStatus?: RowStatus;
name?: string;
title?: string;
description?: string;
}
interface WorkspaceFind {
creatorId?: UserId;
memberId?: UserId;
}

File diff suppressed because it is too large Load Diff