[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:
Anmol Singh Bhatia 2025-02-17 23:46:55 +05:30 committed by GitHub
parent a9aeeb6707
commit 473932af0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1155 additions and 253 deletions

View file

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

View file

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

View file

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

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

View file

@ -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>

View file

@ -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;

View file

@ -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,

View file

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

View file

@ -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

View file

@ -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]
);
}