[WEB-3291] dev: app sidebar revamp (#6578)
* chore: workspace constant and types updated * chore: workspace service, store and app theme store updated * dev: extended sidebar implementation and code refactor * chore: ux improvements * chore: sidebar preference endpoint updated * chore: sidebar preference endpoint updated * chore: sidebar preference endpoint updated * chore: code refactor * chore: code refactor * chore: radix-ui react-scroll-area added to plane ui package * chore: scrollbar color token added to tailwind config * dev: scroll area component * chore-scroll-area-component-improvement * fix: build error * chore: code refactor --------- Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
This commit is contained in:
parent
a9aeeb6707
commit
473932af0a
25 changed files with 1155 additions and 253 deletions
|
|
@ -69,7 +69,11 @@ export const SidebarDropdown = observer(() => {
|
|||
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
|
||||
// TODO: fix workspaces list scroll
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-x-3 gap-y-2">
|
||||
<div
|
||||
className={cn("flex items-center justify-center gap-x-3 gap-y-2", {
|
||||
"flex-col gap-y-3": sidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<Menu
|
||||
as="div"
|
||||
className={cn("relative h-full truncate text-left flex-grow flex justify-stretch", {
|
||||
|
|
@ -185,70 +189,68 @@ export const SidebarDropdown = observer(() => {
|
|||
</>
|
||||
)}
|
||||
</Menu>
|
||||
{!sidebarCollapsed && (
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
<Menu.Button className="grid place-items-center outline-none" ref={setReferenceElement}>
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={getFileURL(currentUser?.avatar_url ?? "")}
|
||||
size={24}
|
||||
shape="square"
|
||||
className="!text-base"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute left-0 z-[21] mt-1 flex w-52 origin-top-left flex-col divide-y
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
<Menu.Button className="grid place-items-center outline-none" ref={setReferenceElement}>
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={getFileURL(currentUser?.avatar_url ?? "")}
|
||||
size={24}
|
||||
shape="circle"
|
||||
className="!text-base"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute left-0 z-[21] mt-1 flex w-52 origin-top-left flex-col divide-y
|
||||
divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
|
||||
ref={setPopperElement as Ref<HTMLDivElement>}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
||||
<Link href="/profile">
|
||||
<Menu.Item as="div">
|
||||
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
|
||||
<Settings className="h-4 w-4 stroke-[1.5]" />
|
||||
<span>{t("settings")}</span>
|
||||
ref={setPopperElement as Ref<HTMLDivElement>}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
||||
<Link href="/profile">
|
||||
<Menu.Item as="div">
|
||||
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
|
||||
<Settings className="h-4 w-4 stroke-[1.5]" />
|
||||
<span>{t("settings")}</span>
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`pt-2 ${isUserInstanceAdmin || false ? "pb-2" : ""}`}>
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="size-4 stroke-[1.5]" />
|
||||
{t("sign_out")}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
{isUserInstanceAdmin && (
|
||||
<div className="p-2 pb-0">
|
||||
<Link href={GOD_MODE_URL}>
|
||||
<Menu.Item as="button" type="button" className="w-full">
|
||||
<span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200">
|
||||
{t("enter_god_mode")}
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`pt-2 ${isUserInstanceAdmin || false ? "pb-2" : ""}`}>
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="size-4 stroke-[1.5]" />
|
||||
{t("sign_out")}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
{isUserInstanceAdmin && (
|
||||
<div className="p-2 pb-0">
|
||||
<Link href={GOD_MODE_URL}>
|
||||
<Menu.Item as="button" type="button" className="w-full">
|
||||
<span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200">
|
||||
{t("enter_god_mode")}
|
||||
</span>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
)}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@ import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/el
|
|||
import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useParams } from "next/navigation";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { LinkIcon, Star, Settings, Share2, LogOut, MoreHorizontal, ChevronRight } from "lucide-react";
|
||||
import { LinkIcon, Settings, Share2, LogOut, MoreHorizontal, ChevronRight } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// plane helpers
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { CustomMenu, Tooltip, ArchiveIcon, setPromiseToast, DropIndicator, DragHandle, ControlLink } from "@plane/ui";
|
||||
import { CustomMenu, Tooltip, ArchiveIcon, DropIndicator, DragHandle, ControlLink } from "@plane/ui";
|
||||
// components
|
||||
import { Logo } from "@/components/common";
|
||||
import { LeaveProjectModal, PublishProjectModal } from "@/components/project";
|
||||
|
|
@ -52,7 +52,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
||||
const { t } = useTranslation();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { addProjectToFavorites, removeProjectFromFavorites, getPartialProjectById } = useProject();
|
||||
const { getPartialProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// states
|
||||
|
|
@ -67,7 +67,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
const projectRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId: URLProjectId } = useParams();
|
||||
// derived values
|
||||
const project = getPartialProjectById(projectId);
|
||||
|
|
@ -85,40 +84,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
project?.id
|
||||
);
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug || !project) return;
|
||||
|
||||
const addToFavoritePromise = addProjectToFavorites(workspaceSlug.toString(), project.id);
|
||||
setPromiseToast(addToFavoritePromise, {
|
||||
loading: t("adding_project_to_favorites"),
|
||||
success: {
|
||||
title: t("success"),
|
||||
message: () => t("project_added_to_favorites"),
|
||||
},
|
||||
error: {
|
||||
title: t("error"),
|
||||
message: () => t("couldnt_add_the_project_to_favorites"),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = () => {
|
||||
if (!workspaceSlug || !project) return;
|
||||
|
||||
const removeFromFavoritePromise = removeProjectFromFavorites(workspaceSlug.toString(), project.id);
|
||||
setPromiseToast(removeFromFavoritePromise, {
|
||||
loading: t("removing_project_from_favorites"),
|
||||
success: {
|
||||
title: t("success"),
|
||||
message: () => t("project_removed_from_favorites"),
|
||||
},
|
||||
error: {
|
||||
title: t("error"),
|
||||
message: () => t("couldnt_remove_the_project_from_favorites"),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLeaveProject = () => {
|
||||
setTrackElement("APP_SIDEBAR_PROJECT_DROPDOWN");
|
||||
setLeaveProjectModal(true);
|
||||
|
|
@ -222,11 +187,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
if (URLProjectId === project.id) setIsProjectListOpen(true);
|
||||
}, [URLProjectId]);
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (!isProjectListOpen && !isMobile) router.push(`/${workspaceSlug}/projects/${project.id}/issues`);
|
||||
setIsProjectListOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleItemClick = () => setIsProjectListOpen((prev) => !prev);
|
||||
return (
|
||||
<>
|
||||
<PublishProjectModal isOpen={publishModalOpen} project={project} onClose={() => setPublishModal(false)} />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { Briefcase, ChevronRight, Plus } from "lucide-react";
|
||||
import { Briefcase, ChevronRight, Ellipsis, Plus } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
|
@ -13,6 +13,7 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { Loader, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { CreateProjectModal } from "@/components/project";
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
import { SidebarProjectsListItem } from "@/components/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
|
@ -37,7 +38,7 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { toggleCreateProjectModal } = useCommandPalette();
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const { sidebarCollapsed, toggleExtendedProjectSidebar } = useAppTheme();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
|
|
@ -237,12 +238,12 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
{isAllProjectsListOpen && (
|
||||
<Disclosure.Panel
|
||||
as="div"
|
||||
className={cn("space-y-1", {
|
||||
className={cn("flex flex-col gap-0.5", {
|
||||
"space-y-0 ml-0": isCollapsed,
|
||||
})}
|
||||
static
|
||||
>
|
||||
{joinedProjects.map((projectId, index) => (
|
||||
{joinedProjects.slice(0, 7).map((projectId, index) => (
|
||||
<SidebarProjectsListItem
|
||||
key={projectId}
|
||||
projectId={projectId}
|
||||
|
|
@ -277,6 +278,21 @@ export const SidebarProjectsList: FC = observer(() => {
|
|||
{!isCollapsed && t("add_project")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{joinedProjects.length > 7 && (
|
||||
<SidebarNavItem className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}>
|
||||
<button
|
||||
onClick={() => toggleExtendedProjectSidebar()}
|
||||
id="extended-project-sidebar-toggle"
|
||||
className={cn("flex items-center gap-1.5 text-sm font-medium flex-grow text-custom-text-350", {
|
||||
"justify-center": sidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<Ellipsis className="size-4" />
|
||||
{!sidebarCollapsed && <span>More</span>}
|
||||
</button>
|
||||
</SidebarNavItem>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
69
web/core/components/workspace/sidebar/sidebar-menu-items.tsx
Normal file
69
web/core/components/workspace/sidebar/sidebar-menu-items.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"use client";
|
||||
import React, { useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Ellipsis } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS,
|
||||
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS,
|
||||
} from "@plane/constants";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
// store hooks
|
||||
import { useAppTheme, useWorkspace } from "@/hooks/store";
|
||||
// plane-web imports
|
||||
import { SidebarItem } from "@/plane-web/components/workspace/sidebar";
|
||||
|
||||
export const SidebarMenuItems = observer(() => {
|
||||
// routers
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { sidebarCollapsed, toggleExtendedSidebar } = useAppTheme();
|
||||
const { getNavigationPreferences } = useWorkspace();
|
||||
|
||||
// derived values
|
||||
const currentWorkspaceNavigationPreferences = getNavigationPreferences(workspaceSlug.toString());
|
||||
|
||||
const sortedNavigationItems = useMemo(
|
||||
() =>
|
||||
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.map((item) => {
|
||||
const preference = currentWorkspaceNavigationPreferences?.[item.key];
|
||||
return {
|
||||
...item,
|
||||
sort_order: preference ? preference.sort_order : 0,
|
||||
};
|
||||
}).sort((a, b) => a.sort_order - b.sort_order),
|
||||
[currentWorkspaceNavigationPreferences]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn("flex flex-col gap-0.5", {
|
||||
"space-y-0": sidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
{WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS.map((item, _index) => (
|
||||
<SidebarItem key={`static_${_index}`} item={item} />
|
||||
))}
|
||||
{sortedNavigationItems.map((item, _index) => (
|
||||
<SidebarItem key={`dynamic_${_index}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
<SidebarNavItem className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}>
|
||||
<button
|
||||
onClick={() => toggleExtendedSidebar()}
|
||||
className={cn("flex items-center gap-1.5 text-sm font-medium flex-grow text-custom-text-350", {
|
||||
"justify-center": sidebarCollapsed,
|
||||
})}
|
||||
id="extended-sidebar-toggle"
|
||||
>
|
||||
<Ellipsis className="size-4" />
|
||||
{!sidebarCollapsed && <span>More</span>}
|
||||
</button>
|
||||
</SidebarNavItem>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -65,12 +65,12 @@ export const SidebarWorkspaceMenuItem: FC<SidebarWorkspaceMenuItemProps> = obser
|
|||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<item.Icon
|
||||
className={cn("size-4", {
|
||||
"rotate-180": item.key === "active-cycles",
|
||||
"rotate-180": item.key === "active_cycles",
|
||||
})}
|
||||
/>
|
||||
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>}
|
||||
</div>
|
||||
{!sidebarCollapsed && item.key === "active-cycles" && (
|
||||
{!sidebarCollapsed && item.key === "active_cycles" && (
|
||||
<div className="flex-shrink-0">
|
||||
<UpgradeBadge />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import React, { useEffect } from "react";
|
||||
|
||||
const useExtendedSidebarOutsideClickDetector = (
|
||||
ref: React.RefObject<HTMLElement>,
|
||||
callback: () => void,
|
||||
targetId: string
|
||||
) => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
// check for the closest element with attribute name data-prevent-outside-click
|
||||
const preventOutsideClickElement = (event.target as HTMLElement | undefined)?.closest(
|
||||
"[data-prevent-outside-click]"
|
||||
);
|
||||
// if the closest element with attribute name data-prevent-outside-click is found, return
|
||||
if (preventOutsideClickElement) {
|
||||
return;
|
||||
}
|
||||
// check if the click target is the current issue element or its children
|
||||
let targetElement = event.target as HTMLElement | null;
|
||||
while (targetElement) {
|
||||
if (targetElement.id === targetId) {
|
||||
// if the click target is the current issue element, return
|
||||
return;
|
||||
}
|
||||
targetElement = targetElement.parentElement;
|
||||
}
|
||||
const delayOutsideClickElement = (event.target as HTMLElement | undefined)?.closest("[data-delay-outside-click]");
|
||||
if (delayOutsideClickElement) {
|
||||
// if the click target is the closest element with attribute name data-delay-outside-click, delay the callback
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
// else, call the callback immediately
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClick);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useExtendedSidebarOutsideClickDetector;
|
||||
|
|
@ -44,7 +44,7 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
|||
const {
|
||||
workspace: { fetchWorkspaceMembers },
|
||||
} = useMember();
|
||||
const { workspaces } = useWorkspace();
|
||||
const { workspaces, fetchSidebarNavigationPreferences } = useWorkspace();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { loader, workspaceInfoBySlug, fetchUserWorkspaceInfo, fetchUserProjectPermissions, allowPermissions } =
|
||||
useUserPermissions();
|
||||
|
|
@ -101,6 +101,13 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
|
|||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// fetch workspace sidebar preferences
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_SIDEBAR_PREFERENCES_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchSidebarNavigationPreferences(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// initialize the local database
|
||||
const { isLoading: isDBInitializing } = useSWRImmutable(
|
||||
workspaceSlug ? `WORKSPACE_DB_${workspaceSlug}` : null,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
TWidgetEntityData,
|
||||
TActivityEntityData,
|
||||
} from "@plane/types";
|
||||
import { IWorkspaceSidebarNavigationItem, IWorkspaceSidebarNavigation } from "@plane/types/src/workspace";
|
||||
import { APIService } from "@/services/api.service";
|
||||
// helpers
|
||||
// types
|
||||
|
|
@ -362,4 +363,24 @@ export class WorkspaceService extends APIService {
|
|||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchSidebarNavigationPreferences(workspaceSlug: string): Promise<IWorkspaceSidebarNavigation> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/sidebar-preferences/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async updateSidebarPreference(
|
||||
workspaceSlug: string,
|
||||
key: string,
|
||||
data: Partial<IWorkspaceSidebarNavigationItem>
|
||||
): Promise<IWorkspaceSidebarNavigationItem> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/sidebar-preferences/${key}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { action, observable, makeObservable } from "mobx";
|
|||
export interface IThemeStore {
|
||||
// observables
|
||||
sidebarCollapsed: boolean | undefined;
|
||||
extendedSidebarCollapsed: boolean | undefined;
|
||||
extendedProjectSidebarCollapsed: boolean | undefined;
|
||||
profileSidebarCollapsed: boolean | undefined;
|
||||
workspaceAnalyticsSidebarCollapsed: boolean | undefined;
|
||||
issueDetailSidebarCollapsed: boolean | undefined;
|
||||
|
|
@ -11,6 +13,8 @@ export interface IThemeStore {
|
|||
projectOverviewSidebarCollapsed: boolean | undefined;
|
||||
// actions
|
||||
toggleSidebar: (collapsed?: boolean) => void;
|
||||
toggleExtendedSidebar: (collapsed?: boolean) => void;
|
||||
toggleExtendedProjectSidebar: (collapsed?: boolean) => void;
|
||||
toggleProfileSidebar: (collapsed?: boolean) => void;
|
||||
toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void;
|
||||
toggleIssueDetailSidebar: (collapsed?: boolean) => void;
|
||||
|
|
@ -22,6 +26,8 @@ export interface IThemeStore {
|
|||
export class ThemeStore implements IThemeStore {
|
||||
// observables
|
||||
sidebarCollapsed: boolean | undefined = undefined;
|
||||
extendedSidebarCollapsed: boolean | undefined = undefined;
|
||||
extendedProjectSidebarCollapsed: boolean | undefined = undefined;
|
||||
profileSidebarCollapsed: boolean | undefined = undefined;
|
||||
workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined;
|
||||
issueDetailSidebarCollapsed: boolean | undefined = undefined;
|
||||
|
|
@ -33,6 +39,8 @@ export class ThemeStore implements IThemeStore {
|
|||
makeObservable(this, {
|
||||
// observable
|
||||
sidebarCollapsed: observable.ref,
|
||||
extendedSidebarCollapsed: observable.ref,
|
||||
extendedProjectSidebarCollapsed: observable.ref,
|
||||
profileSidebarCollapsed: observable.ref,
|
||||
workspaceAnalyticsSidebarCollapsed: observable.ref,
|
||||
issueDetailSidebarCollapsed: observable.ref,
|
||||
|
|
@ -41,6 +49,8 @@ export class ThemeStore implements IThemeStore {
|
|||
projectOverviewSidebarCollapsed: observable.ref,
|
||||
// action
|
||||
toggleSidebar: action,
|
||||
toggleExtendedSidebar: action,
|
||||
toggleExtendedProjectSidebar: action,
|
||||
toggleProfileSidebar: action,
|
||||
toggleWorkspaceAnalyticsSidebar: action,
|
||||
toggleIssueDetailSidebar: action,
|
||||
|
|
@ -63,6 +73,32 @@ export class ThemeStore implements IThemeStore {
|
|||
localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the extended sidebar collapsed state
|
||||
* @param collapsed
|
||||
*/
|
||||
toggleExtendedSidebar = (collapsed?: boolean) => {
|
||||
if (collapsed === undefined) {
|
||||
this.extendedSidebarCollapsed = !this.extendedSidebarCollapsed;
|
||||
} else {
|
||||
this.extendedSidebarCollapsed = collapsed;
|
||||
}
|
||||
localStorage.setItem("extended_sidebar_collapsed", this.extendedSidebarCollapsed.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the extended project sidebar collapsed state
|
||||
* @param collapsed
|
||||
*/
|
||||
toggleExtendedProjectSidebar = (collapsed?: boolean) => {
|
||||
if (collapsed === undefined) {
|
||||
this.extendedProjectSidebarCollapsed = !this.extendedProjectSidebarCollapsed;
|
||||
} else {
|
||||
this.extendedProjectSidebarCollapsed = collapsed;
|
||||
}
|
||||
localStorage.setItem("extended_project_sidebar_collapsed", this.extendedProjectSidebarCollapsed.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the profile sidebar collapsed state
|
||||
* @param collapsed
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import set from "lodash/set";
|
||||
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
||||
// types
|
||||
import { IWorkspace } from "@plane/types";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { IWorkspaceSidebarNavigationItem, IWorkspace, IWorkspaceSidebarNavigation } from "@plane/types";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// store
|
||||
|
|
@ -18,6 +19,7 @@ export interface IWorkspaceRootStore {
|
|||
// computed
|
||||
currentWorkspace: IWorkspace | null;
|
||||
workspacesCreatedByCurrentUser: IWorkspace[] | null;
|
||||
navigationPreferencesMap: Record<string, IWorkspaceSidebarNavigation>;
|
||||
// computed actions
|
||||
getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null;
|
||||
getWorkspaceById: (workspaceId: string) => IWorkspace | null;
|
||||
|
|
@ -28,6 +30,13 @@ export interface IWorkspaceRootStore {
|
|||
updateWorkspace: (workspaceSlug: string, data: Partial<IWorkspace>) => Promise<IWorkspace>;
|
||||
updateWorkspaceLogo: (workspaceSlug: string, logoURL: string) => void;
|
||||
deleteWorkspace: (workspaceSlug: string) => Promise<void>;
|
||||
fetchSidebarNavigationPreferences: (workspaceSlug: string) => Promise<void>;
|
||||
updateSidebarPreference: (
|
||||
workspaceSlug: string,
|
||||
key: string,
|
||||
data: Partial<IWorkspaceSidebarNavigationItem>
|
||||
) => Promise<IWorkspaceSidebarNavigationItem | undefined>;
|
||||
getNavigationPreferences: (workspaceSlug: string) => IWorkspaceSidebarNavigation | undefined;
|
||||
// sub-stores
|
||||
webhook: IWebhookStore;
|
||||
apiToken: IApiTokenStore;
|
||||
|
|
@ -38,6 +47,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
|
|||
loader: boolean = false;
|
||||
// observables
|
||||
workspaces: Record<string, IWorkspace> = {};
|
||||
navigationPreferencesMap: Record<string, IWorkspaceSidebarNavigation> = {};
|
||||
// services
|
||||
workspaceService;
|
||||
// root store
|
||||
|
|
@ -53,6 +63,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
|
|||
loader: observable.ref,
|
||||
// observables
|
||||
workspaces: observable,
|
||||
navigationPreferencesMap: observable,
|
||||
// computed
|
||||
currentWorkspace: computed,
|
||||
workspacesCreatedByCurrentUser: computed,
|
||||
|
|
@ -65,6 +76,8 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
|
|||
updateWorkspace: action,
|
||||
updateWorkspaceLogo: action,
|
||||
deleteWorkspace: action,
|
||||
fetchSidebarNavigationPreferences: action,
|
||||
updateSidebarPreference: action,
|
||||
});
|
||||
|
||||
// services
|
||||
|
|
@ -183,4 +196,41 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
|
|||
this.workspaces = updatedWorkspacesList;
|
||||
});
|
||||
});
|
||||
|
||||
fetchSidebarNavigationPreferences = async (workspaceSlug: string) => {
|
||||
try {
|
||||
const response = await this.workspaceService.fetchSidebarNavigationPreferences(workspaceSlug);
|
||||
|
||||
runInAction(() => {
|
||||
this.navigationPreferencesMap[workspaceSlug] = response;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch sidebar preferences:", error);
|
||||
}
|
||||
};
|
||||
|
||||
updateSidebarPreference = async (
|
||||
workspaceSlug: string,
|
||||
key: string,
|
||||
data: Partial<IWorkspaceSidebarNavigationItem>
|
||||
) => {
|
||||
try {
|
||||
const response = await this.workspaceService.updateSidebarPreference(workspaceSlug, key, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.navigationPreferencesMap[workspaceSlug] = {
|
||||
...this.navigationPreferencesMap[workspaceSlug],
|
||||
[key]: response,
|
||||
};
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Failed to update sidebar preference:", error);
|
||||
}
|
||||
};
|
||||
|
||||
getNavigationPreferences = computedFn(
|
||||
(workspaceSlug: string): IWorkspaceSidebarNavigation | undefined => this.navigationPreferencesMap[workspaceSlug]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue