diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 77b9d425..748fbb3b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,6 +33,7 @@ "randomcolor": "^0.6.2", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", + "react-infinite-scroller": "^1.2.6", "react-redux": "^9.0.4", "react-resizable-layout": "^0.7.2", "react-router-dom": "^6.21.1", @@ -45,6 +46,7 @@ "@types/randomcolor": "^0.5.9", "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", + "@types/react-infinite-scroller": "^1.2.5", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", @@ -2101,6 +2103,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-infinite-scroller": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.5.tgz", + "integrity": "sha512-fJU1jhMgoL6NJFrqTM0Ob7tnd2sQWGxe2ESwiU6FZWbJK/VO/Er5+AOhc+e2zbT0dk5pLygqctsulOLJ8xnSzw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -5562,6 +5573,17 @@ "react": "^18.2.0" } }, + "node_modules/react-infinite-scroller": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz", + "integrity": "sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==", + "dependencies": { + "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": "^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 020c2925..870fe960 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "randomcolor": "^0.6.2", "react": "^17.0.0 || ^18.0.0", "react-dom": "^17.0.0 || ^18.0.0", + "react-infinite-scroller": "^1.2.6", "react-redux": "^9.0.4", "react-resizable-layout": "^0.7.2", "react-router-dom": "^6.21.1", @@ -49,6 +50,7 @@ "@types/randomcolor": "^0.5.9", "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", + "@types/react-infinite-scroller": "^1.2.5", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", diff --git a/frontend/src/components/drawers/WorkspaceDrawer.tsx b/frontend/src/components/drawers/WorkspaceDrawer.tsx index 8d295062..1ded594b 100644 --- a/frontend/src/components/drawers/WorkspaceDrawer.tsx +++ b/frontend/src/components/drawers/WorkspaceDrawer.tsx @@ -14,10 +14,12 @@ import MoreVertIcon from "@mui/icons-material/MoreVert"; import { useSelector } from "react-redux"; import { selectUser } from "../../store/userSlice"; import { MouseEventHandler, useState } from "react"; -import ProfilePopover from "../common/ProfilePopover"; +import ProfilePopover from "../popovers/ProfilePopover"; import { useParams } from "react-router-dom"; import { useGetWorkspaceQuery } from "../../hooks/api/workspace"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import WorkspaceListPopover from "../popovers/WorkspaceListPopover"; const DRAWER_WIDTH = 240; @@ -26,6 +28,9 @@ function WorkspaceDrawer() { const userStore = useSelector(selectUser); const { data: workspace } = useGetWorkspaceQuery(params.workspaceId); const [profileAnchorEl, setProfileAnchorEl] = useState<(EventTarget & Element) | null>(null); + const [workspaceListAnchorEl, setWorkspaceListAnchorEl] = useState< + (EventTarget & Element) | null + >(null); const handleOpenProfilePopover: MouseEventHandler = (event) => { setProfileAnchorEl(event.currentTarget); @@ -35,6 +40,14 @@ function WorkspaceDrawer() { setProfileAnchorEl(null); }; + const handleOpenWorkspacePopover: MouseEventHandler = (event) => { + setWorkspaceListAnchorEl(event.currentTarget); + }; + + const handleCloseWorkspacePopover = () => { + setWorkspaceListAnchorEl(null); + }; + return ( - + - + {workspaceListAnchorEl ? ( + + ) : ( + + )} + + diff --git a/frontend/src/components/common/ProfilePopover.tsx b/frontend/src/components/popovers/ProfilePopover.tsx similarity index 100% rename from frontend/src/components/common/ProfilePopover.tsx rename to frontend/src/components/popovers/ProfilePopover.tsx diff --git a/frontend/src/components/popovers/WorkspaceListPopover.tsx b/frontend/src/components/popovers/WorkspaceListPopover.tsx new file mode 100644 index 00000000..4f3f71a9 --- /dev/null +++ b/frontend/src/components/popovers/WorkspaceListPopover.tsx @@ -0,0 +1,96 @@ +import { + Box, + CircularProgress, + ListItemSecondaryAction, + ListItemText, + MenuItem, + MenuList, + Popover, + PopoverProps, +} from "@mui/material"; +import { useGetWorkspaceListQuery } from "../../hooks/api/workspace"; +import InfiniteScroll from "react-infinite-scroller"; +import { useMemo } from "react"; +import { Workspace } from "../../hooks/api/types/workspace"; +import { useNavigate, useParams } from "react-router-dom"; +import CheckIcon from "@mui/icons-material/Check"; + +interface WorkspaceListPopoverProps extends PopoverProps { + width?: number; +} + +function WorkspaceListPopover(props: WorkspaceListPopoverProps) { + const { width, ...popoverProps } = props; + const navigate = useNavigate(); + const params = useParams(); + const { data: workspacePageList, hasNextPage, fetchNextPage } = useGetWorkspaceListQuery(); + const workspaceList = useMemo(() => { + return workspacePageList?.pages.reduce((prev: Array, page) => { + return prev.concat(page.workspaces); + }, [] as Array); + }, [workspacePageList?.pages]); + + const handleMoveToSelectedWorkspace = (workspaceId: string) => { + if (params.workspaceId === workspaceId) return; + + navigate(`/workspace/${workspaceId}`); + }; + + return ( + + + fetchNextPage()} + hasMore={hasNextPage} + loader={ + + + + } + useWindow={false} + > + + {workspaceList?.map((workspace) => ( + handleMoveToSelectedWorkspace(workspace.id)} + > + + {workspace.title} + + {params.workspaceId === workspace.id && ( + + + + )} + + ))} + + + + + ); +} + +export default WorkspaceListPopover; diff --git a/frontend/src/hooks/api/types/workspace.d.ts b/frontend/src/hooks/api/types/workspace.d.ts index 50e2f9ca..d879a029 100644 --- a/frontend/src/hooks/api/types/workspace.d.ts +++ b/frontend/src/hooks/api/types/workspace.d.ts @@ -6,3 +6,8 @@ export interface Workspace { } export class GetWorkspaceResponse extends Workspace {} + +export class GetWorkspaceListResponse { + cursor: string | null; + workspaces: Array; +} diff --git a/frontend/src/hooks/api/workspace.ts b/frontend/src/hooks/api/workspace.ts index 880d8888..cb98612e 100644 --- a/frontend/src/hooks/api/workspace.ts +++ b/frontend/src/hooks/api/workspace.ts @@ -1,11 +1,15 @@ -import { useQuery } from "@tanstack/react-query"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import axios from "axios"; -import { GetWorkspaceResponse } from "./types/workspace"; +import { GetWorkspaceListResponse, GetWorkspaceResponse } from "./types/workspace"; export const generateGetWorkspaceQueryKey = (workspaceId: string) => { return ["workspaces", workspaceId]; }; +export const generateGetWorkspaceListQueryKey = () => { + return ["workspaces"]; +}; + export const useGetWorkspaceQuery = (workspaceId?: string) => { const query = useQuery({ queryKey: generateGetWorkspaceQueryKey(workspaceId || ""), @@ -21,3 +25,22 @@ export const useGetWorkspaceQuery = (workspaceId?: string) => { return query; }; + +export const useGetWorkspaceListQuery = () => { + const query = useInfiniteQuery({ + queryKey: generateGetWorkspaceListQueryKey(), + queryFn: async ({ pageParam }) => { + const res = await axios.get("/workspaces", { + params: { + cursor: pageParam, + }, + }); + return res.data; + }, + initialPageParam: undefined, + getPreviousPageParam: (firstPage) => firstPage.cursor ?? undefined, + getNextPageParam: (lastPage) => lastPage.cursor ?? undefined, + }); + + return query; +};