[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

@ -30,7 +30,6 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView):
keys = [ keys = [
key key
for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices
if key not in ["projects"]
] ]
for preference in keys: for preference in keys:
@ -40,20 +39,28 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView):
preference = WorkspaceUserPreference.objects.bulk_create( preference = WorkspaceUserPreference.objects.bulk_create(
[ [
WorkspaceUserPreference( WorkspaceUserPreference(
key=key, user=request.user, workspace=workspace key=key, user=request.user, workspace=workspace, sort_order=(65535 + (i*10000))
) )
for key in create_preference_keys for i, key in enumerate(create_preference_keys)
], ],
batch_size=10, batch_size=10,
ignore_conflicts=True, ignore_conflicts=True,
) )
preference = WorkspaceUserPreference.objects.filter( preferences = WorkspaceUserPreference.objects.filter(
user=request.user, workspace_id=workspace.id user=request.user, workspace_id=workspace.id
) ).order_by("sort_order").values("key", "is_pinned", "sort_order")
user_preferences = {}
for preference in preferences:
user_preferences[(str(preference["key"]))] = {
"is_pinned": preference["is_pinned"],
"sort_order": preference["sort_order"],
}
return Response( return Response(
preference.values("key", "is_pinned", "sort_order"), user_preferences,
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )

View file

@ -392,12 +392,12 @@ class WorkspaceUserPreference(BaseModel):
"""Preference for the workspace for a user""" """Preference for the workspace for a user"""
class UserPreferenceKeys(models.TextChoices): class UserPreferenceKeys(models.TextChoices):
PROJECTS = "projects", "Projects"
ANALYTICS = "analytics", "Analytics"
CYCLES = "cycles", "Cycles"
VIEWS = "views", "Views" VIEWS = "views", "Views"
ACTIVE_CYCLES = "active_cycles", "Active Cycles"
ANALYTICS = "analytics", "Analytics"
DRAFTS = "drafts", "Drafts"
YOUR_WORK = "your_work", "Your Work" YOUR_WORK = "your_work", "Your Work"
ARCHIVES = "archives", "Archives"
workspace = models.ForeignKey( workspace = models.ForeignKey(
"db.Workspace", "db.Workspace",

View file

@ -84,48 +84,42 @@ export const WORKSPACE_SETTINGS = {
i18n_label: "workspace_settings.settings.general.title", i18n_label: "workspace_settings.settings.general.title",
href: `/settings`, href: `/settings`,
access: [EUserWorkspaceRoles.ADMIN], access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
pathname === `${baseUrl}/settings/`,
}, },
members: { members: {
key: "members", key: "members",
i18n_label: "workspace_settings.settings.members.title", i18n_label: "workspace_settings.settings.members.title",
href: `/settings/members`, href: `/settings/members`,
access: [EUserWorkspaceRoles.ADMIN], access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
pathname === `${baseUrl}/settings/members/`,
}, },
"billing-and-plans": { "billing-and-plans": {
key: "billing-and-plans", key: "billing-and-plans",
i18n_label: "workspace_settings.settings.billing_and_plans.title", i18n_label: "workspace_settings.settings.billing_and_plans.title",
href: `/settings/billing`, href: `/settings/billing`,
access: [EUserWorkspaceRoles.ADMIN], access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing/`,
pathname === `${baseUrl}/settings/billing/`,
}, },
export: { export: {
key: "export", key: "export",
i18n_label: "workspace_settings.settings.exports.title", i18n_label: "workspace_settings.settings.exports.title",
href: `/settings/exports`, href: `/settings/exports`,
access: [EUserWorkspaceRoles.ADMIN], access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`,
pathname === `${baseUrl}/settings/exports/`,
}, },
webhooks: { webhooks: {
key: "webhooks", key: "webhooks",
i18n_label: "workspace_settings.settings.webhooks.title", i18n_label: "workspace_settings.settings.webhooks.title",
href: `/settings/webhooks`, href: `/settings/webhooks`,
access: [EUserWorkspaceRoles.ADMIN], access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`,
pathname === `${baseUrl}/settings/webhooks/`,
}, },
"api-tokens": { "api-tokens": {
key: "api-tokens", key: "api-tokens",
i18n_label: "workspace_settings.settings.api_tokens.title", i18n_label: "workspace_settings.settings.api_tokens.title",
href: `/settings/api-tokens`, href: `/settings/api-tokens`,
access: [EUserWorkspaceRoles.ADMIN], access: [EUserWorkspaceRoles.ADMIN],
highlight: (pathname: string, baseUrl: string) => highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`,
pathname === `${baseUrl}/settings/api-tokens/`,
}, },
}; };
@ -256,3 +250,84 @@ export const DEFAULT_GLOBAL_VIEWS_LIST: {
i18n_label: "default_global_view.subscribed", i18n_label: "default_global_view.subscribed",
}, },
]; ];
export interface IWorkspaceSidebarNavigationItem {
key: string;
labelTranslationKey: string;
href: string;
access: EUserWorkspaceRoles[];
}
export const WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS: Record<string, IWorkspaceSidebarNavigationItem> = {
"your-work": {
key: "your_work",
labelTranslationKey: "your_work",
href: `/profile/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
},
views: {
key: "views",
labelTranslationKey: "views",
href: `/workspace-views/all-issues/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
},
"active-cycles": {
key: "active_cycles",
labelTranslationKey: "cycles",
href: `/active-cycles/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
},
analytics: {
key: "analytics",
labelTranslationKey: "analytics",
href: `/analytics/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
},
drafts: {
key: "drafts",
labelTranslationKey: "drafts",
href: `/drafts/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
},
archives: {
key: "archives",
labelTranslationKey: "archives",
href: `/projects/archives/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
},
};
export const WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS: IWorkspaceSidebarNavigationItem[] = [
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["views"],
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["active-cycles"],
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["analytics"],
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["your-work"],
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["drafts"],
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS["archives"],
];
export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS: Record<string, IWorkspaceSidebarNavigationItem> = {
home: {
key: "home",
labelTranslationKey: "home.title",
href: `/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
},
notifications: {
key: "notifications",
labelTranslationKey: "notification.label",
href: `/notifications/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
},
projects: {
key: "projects",
labelTranslationKey: "projects",
href: `/projects/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
},
};
export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS: IWorkspaceSidebarNavigationItem[] = [
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["home"],
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["notifications"],
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["projects"],
];

View file

@ -8,9 +8,9 @@ export const useOutsideClickDetector = (
const handleClick = (event: MouseEvent) => { const handleClick = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as any)) { if (ref.current && !ref.current.contains(event.target as any)) {
// check for the closest element with attribute name data-prevent-outside-click // check for the closest element with attribute name data-prevent-outside-click
const preventOutsideClickElement = ( const preventOutsideClickElement = (event.target as unknown as HTMLElement | undefined)?.closest(
event.target as unknown as HTMLElement | undefined "[data-prevent-outside-click]"
)?.closest("[data-prevent-outside-click]"); );
// if the closest element with attribute name data-prevent-outside-click is found, return // if the closest element with attribute name data-prevent-outside-click is found, return
if (preventOutsideClickElement) { if (preventOutsideClickElement) {
return; return;

View file

@ -1,10 +1,4 @@
import type { import type { ICycle, IProjectMember, IUser, IUserLite, IWorkspaceViewProps, TPaginationInfo } from "@plane/types";
ICycle,
IProjectMember,
IUser,
IUserLite,
IWorkspaceViewProps,
} from "@plane/types";
import { EUserWorkspaceRoles } from "@plane/constants"; // TODO: check if importing this over here causes circular dependency import { EUserWorkspaceRoles } from "@plane/constants"; // TODO: check if importing this over here causes circular dependency
import { TUserPermissions } from "./enums"; import { TUserPermissions } from "./enums";
@ -229,3 +223,13 @@ export interface IWorkspaceAnalyticsResponse {
export type TWorkspacePaginationInfo = TPaginationInfo & { export type TWorkspacePaginationInfo = TPaginationInfo & {
results: IWorkspace[]; results: IWorkspace[];
}; };
export interface IWorkspaceSidebarNavigationItem {
key?: string;
is_pinned: boolean;
sort_order: number;
}
export interface IWorkspaceSidebarNavigation {
[key: string]: IWorkspaceSidebarNavigationItem;
}

View file

@ -2,20 +2,13 @@ import * as React from "react";
import { ISvgIcons } from "./type"; import { ISvgIcons } from "./type";
export const OverviewIcon: React.FC<ISvgIcons> = ({ width = "16", height = "16", className = "" }) => ( export const OverviewIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg <svg viewBox="0 0 16 16" className={className} xmlns="http://www.w3.org/2000/svg" {...rest}>
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M2.5 3C2.5 2.86739 2.55268 2.74021 2.64645 2.64645C2.74021 2.55268 2.86739 2.5 3 2.5H3.5C9.02267 2.5 13.5 6.97733 13.5 12.5V13C13.5 13.1326 13.4473 13.2598 13.3536 13.3536C13.2598 13.4473 13.1326 13.5 13 13.5H12.5C12.3674 13.5 12.2402 13.4473 12.1464 13.3536C12.0527 13.2598 12 13.1326 12 13V12.5C12 7.80533 8.19467 4 3.5 4H3C2.86739 4 2.74021 3.94732 2.64645 3.85355C2.55268 3.75979 2.5 3.63261 2.5 3.5V3ZM2.5 7.5C2.5 7.36739 2.55268 7.24022 2.64645 7.14645C2.74021 7.05268 2.86739 7 3 7H3.5C4.22227 7 4.93747 7.14226 5.60476 7.41866C6.27205 7.69506 6.87837 8.10019 7.38909 8.61091C7.89981 9.12164 8.30494 9.72795 8.58134 10.3952C8.85774 11.0625 9 11.7777 9 12.5V13C9 13.1326 8.94732 13.2598 8.85355 13.3536C8.75978 13.4473 8.63261 13.5 8.5 13.5H8C7.86739 13.5 7.74022 13.4473 7.64645 13.3536C7.55268 13.2598 7.5 13.1326 7.5 13V12.5C7.5 11.4391 7.07857 10.4217 6.32843 9.67157C5.57828 8.92143 4.56087 8.5 3.5 8.5H3C2.86739 8.5 2.74021 8.44732 2.64645 8.35355C2.55268 8.25978 2.5 8.13261 2.5 8V7.5ZM2.5 12.5C2.5 12.2348 2.60536 11.9804 2.79289 11.7929C2.98043 11.6054 3.23478 11.5 3.5 11.5C3.76522 11.5 4.01957 11.6054 4.20711 11.7929C4.39464 11.9804 4.5 12.2348 4.5 12.5C4.5 12.7652 4.39464 13.0196 4.20711 13.2071C4.01957 13.3946 3.76522 13.5 3.5 13.5C3.23478 13.5 2.98043 13.3946 2.79289 13.2071C2.60536 13.0196 2.5 12.7652 2.5 12.5Z" d="M2.5 3C2.5 2.86739 2.55268 2.74021 2.64645 2.64645C2.74021 2.55268 2.86739 2.5 3 2.5H3.5C9.02267 2.5 13.5 6.97733 13.5 12.5V13C13.5 13.1326 13.4473 13.2598 13.3536 13.3536C13.2598 13.4473 13.1326 13.5 13 13.5H12.5C12.3674 13.5 12.2402 13.4473 12.1464 13.3536C12.0527 13.2598 12 13.1326 12 13V12.5C12 7.80533 8.19467 4 3.5 4H3C2.86739 4 2.74021 3.94732 2.64645 3.85355C2.55268 3.75979 2.5 3.63261 2.5 3.5V3ZM2.5 7.5C2.5 7.36739 2.55268 7.24022 2.64645 7.14645C2.74021 7.05268 2.86739 7 3 7H3.5C4.22227 7 4.93747 7.14226 5.60476 7.41866C6.27205 7.69506 6.87837 8.10019 7.38909 8.61091C7.89981 9.12164 8.30494 9.72795 8.58134 10.3952C8.85774 11.0625 9 11.7777 9 12.5V13C9 13.1326 8.94732 13.2598 8.85355 13.3536C8.75978 13.4473 8.63261 13.5 8.5 13.5H8C7.86739 13.5 7.74022 13.4473 7.64645 13.3536C7.55268 13.2598 7.5 13.1326 7.5 13V12.5C7.5 11.4391 7.07857 10.4217 6.32843 9.67157C5.57828 8.92143 4.56087 8.5 3.5 8.5H3C2.86739 8.5 2.74021 8.44732 2.64645 8.35355C2.55268 8.25978 2.5 8.13261 2.5 8V7.5ZM2.5 12.5C2.5 12.2348 2.60536 11.9804 2.79289 11.7929C2.98043 11.6054 3.23478 11.5 3.5 11.5C3.76522 11.5 4.01957 11.6054 4.20711 11.7929C4.39464 11.9804 4.5 12.2348 4.5 12.5C4.5 12.7652 4.39464 13.0196 4.20711 13.2071C4.01957 13.3946 3.76522 13.5 3.5 13.5C3.23478 13.5 2.98043 13.3946 2.79289 13.2071C2.60536 13.0196 2.5 12.7652 2.5 12.5Z"
fill="#455068" fill="currentColor"
/> />
</svg> </svg>
); );

View file

@ -0,0 +1,155 @@
"use client";
import React, { useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { Plus, Search } from "lucide-react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { CreateProjectModal } from "@/components/project";
import { SidebarProjectsListItem } from "@/components/workspace";
// hooks
import { orderJoinedProjects } from "@/helpers/project.helper";
import { useAppTheme, useProject, useUserPermissions } from "@/hooks/store";
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
import { TProject } from "@/plane-web/types";
export const ExtendedProjectSidebar = observer(() => {
// refs
const extendedProjectSidebarRef = useRef<HTMLDivElement | null>(null);
const [searchQuery, setSearchQuery] = useState<string>("");
// states
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
// routers
const { workspaceSlug } = useParams();
// store hooks
const { t } = useTranslation();
const { sidebarCollapsed, extendedProjectSidebarCollapsed, toggleExtendedProjectSidebar } = useAppTheme();
const { getPartialProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject();
const { allowPermissions } = useUserPermissions();
const handleOnProjectDrop = (
sourceId: string | undefined,
destinationId: string | undefined,
shouldDropAtEnd: boolean
) => {
if (!sourceId || !destinationId || !workspaceSlug) return;
if (sourceId === destinationId) return;
const joinedProjectsList: TProject[] = [];
joinedProjects.map((projectId) => {
const projectDetails = getPartialProjectById(projectId);
if (projectDetails) joinedProjectsList.push(projectDetails);
});
const sourceIndex = joinedProjects.indexOf(sourceId);
const destinationIndex = shouldDropAtEnd ? joinedProjects.length : joinedProjects.indexOf(destinationId);
if (joinedProjectsList.length <= 0) return;
const updatedSortOrder = orderJoinedProjects(sourceIndex, destinationIndex, sourceId, joinedProjectsList);
if (updatedSortOrder != undefined)
updateProjectView(workspaceSlug.toString(), sourceId, { sort_order: updatedSortOrder }).catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
message: t("something_went_wrong"),
});
});
};
// filter projects based on search query
const filteredProjects = joinedProjects.filter((projectId) => {
const project = getPartialProjectById(projectId);
if (!project) return false;
return project.name.toLowerCase().includes(searchQuery.toLowerCase()) || project.identifier.includes(searchQuery);
});
// auth
const isAuthorizedUser = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
useExtendedSidebarOutsideClickDetector(
extendedProjectSidebarRef,
() => {
if (!isProjectModalOpen) {
toggleExtendedProjectSidebar(false);
}
},
"extended-project-sidebar-toggle"
);
return (
<>
{workspaceSlug && (
<CreateProjectModal
isOpen={isProjectModalOpen}
onClose={() => setIsProjectModalOpen(false)}
setToFavorite={false}
workspaceSlug={workspaceSlug.toString()}
/>
)}
<div
ref={extendedProjectSidebarRef}
className={cn(
"fixed top-0 h-full z-[19] flex flex-col gap-2 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-md",
{
"translate-x-0 opacity-100": extendedProjectSidebarCollapsed,
"-translate-x-full opacity-0": !extendedProjectSidebarCollapsed,
"left-[70px]": sidebarCollapsed,
"left-[250px]": !sidebarCollapsed,
}
)}
>
<div className="flex flex-col gap-1 w-full">
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-custom-text-300 py-1.5">Projects</span>
{isAuthorizedUser && (
<Tooltip tooltipHeading={t("create_project")} tooltipContent="">
<button
type="button"
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
onClick={() => {
setIsProjectModalOpen(true);
}}
>
<Plus className="size-3" />
</button>
</Tooltip>
)}
</div>
<div className="ml-auto flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1 w-full">
<Search className="h-3.5 w-3.5 text-custom-text-400" />
<input
className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
placeholder={t("search")}
value={searchQuery}
autoFocus
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
<div className="flex flex-col gap-0.5">
{filteredProjects.map((projectId, index) => (
<SidebarProjectsListItem
key={projectId}
projectId={projectId}
handleCopyText={() => {}}
projectListType={"JOINED"}
disableDrag={false}
disableDrop={false}
isLastChild={index === joinedProjects.length - 1}
handleOnProjectDrop={handleOnProjectDrop}
/>
))}
</div>
</div>
</>
);
});

View file

@ -0,0 +1,126 @@
"use client";
import React, { useMemo, useRef } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EUserWorkspaceRoles, WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS } from "@plane/constants";
import { cn } from "@plane/utils";
// hooks
import { useAppTheme, useWorkspace } from "@/hooks/store";
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
// plane-web imports
import { ExtendedSidebarItem } from "@/plane-web/components/workspace/sidebar";
export const ExtendedAppSidebar = observer(() => {
// refs
const extendedSidebarRef = useRef<HTMLDivElement | null>(null);
// routers
const { workspaceSlug } = useParams();
// store hooks
const { sidebarCollapsed, extendedSidebarCollapsed, toggleExtendedSidebar } = useAppTheme();
const { updateSidebarPreference, 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]
);
const sortedNavigationItemsKeys = sortedNavigationItems.map((item) => item.key);
const orderNavigationItem = (
sourceIndex: number,
destinationIndex: number,
navigationList: {
sort_order: number;
key: string;
labelTranslationKey: string;
href: string;
access: EUserWorkspaceRoles[];
}[]
): number | undefined => {
if (sourceIndex < 0 || destinationIndex < 0 || navigationList.length <= 0) return undefined;
let updatedSortOrder: number | undefined = undefined;
const sortOrderDefaultValue = 10000;
if (destinationIndex === 0) {
// updating project at the top of the project
const currentSortOrder = navigationList[destinationIndex].sort_order || 0;
updatedSortOrder = currentSortOrder - sortOrderDefaultValue;
} else if (destinationIndex === navigationList.length) {
// updating project at the bottom of the project
const currentSortOrder = navigationList[destinationIndex - 1].sort_order || 0;
updatedSortOrder = currentSortOrder + sortOrderDefaultValue;
} else {
// updating project in the middle of the project
const destinationTopProjectSortOrder = navigationList[destinationIndex - 1].sort_order || 0;
const destinationBottomProjectSortOrder = navigationList[destinationIndex].sort_order || 0;
const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2;
updatedSortOrder = updatedValue;
}
return updatedSortOrder;
};
const handleOnNavigationItemDrop = (
sourceId: string | undefined,
destinationId: string | undefined,
shouldDropAtEnd: boolean
) => {
if (!sourceId || !destinationId || !workspaceSlug) return;
if (sourceId === destinationId) return;
const sourceIndex = sortedNavigationItemsKeys.indexOf(sourceId);
const destinationIndex = shouldDropAtEnd
? sortedNavigationItemsKeys.length
: sortedNavigationItemsKeys.indexOf(destinationId);
const updatedSortOrder = orderNavigationItem(sourceIndex, destinationIndex, sortedNavigationItems);
if (updatedSortOrder != undefined)
updateSidebarPreference(workspaceSlug.toString(), sourceId, {
sort_order: updatedSortOrder,
});
};
useExtendedSidebarOutsideClickDetector(
extendedSidebarRef,
() => toggleExtendedSidebar(false),
"extended-sidebar-toggle"
);
return (
<div
ref={extendedSidebarRef}
className={cn(
"fixed top-0 h-full z-[19] flex flex-col w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-md",
{
"translate-x-0 opacity-100": extendedSidebarCollapsed,
"-translate-x-full opacity-0": !extendedSidebarCollapsed,
"left-[70px]": sidebarCollapsed,
"left-[250px]": !sidebarCollapsed,
}
)}
>
{sortedNavigationItems.map((item, index) => (
<ExtendedSidebarItem
key={item.key}
item={item}
isLastChild={index === sortedNavigationItems.length - 1}
handleOnNavigationItemDrop={handleOnNavigationItemDrop}
/>
))}
</div>
);
});

View file

@ -5,16 +5,10 @@ import { observer } from "mobx-react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks"; import { useOutsideClickDetector } from "@plane/hooks";
// components // components
import { import { SidebarDropdown, SidebarHelpSection, SidebarProjectsList, SidebarQuickActions } from "@/components/workspace";
SidebarDropdown,
SidebarHelpSection,
SidebarProjectsList,
SidebarQuickActions,
SidebarUserMenu,
SidebarWorkspaceMenu,
} from "@/components/workspace";
// helpers
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu"; import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
import { SidebarMenuItems } from "@/components/workspace/sidebar/sidebar-menu-items";
// helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { useAppTheme, useUserPermissions } from "@/hooks/store"; import { useAppTheme, useUserPermissions } from "@/hooks/store";
@ -23,6 +17,8 @@ import useSize from "@/hooks/use-window-size";
// plane web components // plane web components
import { SidebarAppSwitcher } from "@/plane-web/components/sidebar"; import { SidebarAppSwitcher } from "@/plane-web/components/sidebar";
import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list"; import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list";
import { ExtendedProjectSidebar } from "./extended-project-sidebar";
import { ExtendedAppSidebar } from "./extended-sidebar";
export const AppSidebar: FC = observer(() => { export const AppSidebar: FC = observer(() => {
// store hooks // store hooks
@ -55,62 +51,63 @@ export const AppSidebar: FC = observer(() => {
const isFavoriteEmpty = isEmpty(groupedFavorites); const isFavoriteEmpty = isEmpty(groupedFavorites);
return ( return (
<div <>
className={cn(
"fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 w-[250px] md:relative md:ml-0",
{
"w-[70px] -ml-[250px]": sidebarCollapsed,
}
)}
>
<div <div
ref={ref} className={cn(
className={cn("size-full flex flex-col flex-1 pt-4 pb-0", { "fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 w-[250px] md:relative md:ml-0",
"p-2 pt-4": sidebarCollapsed, {
})} "w-[70px] -ml-[250px]": sidebarCollapsed,
}
)}
> >
<div <div
className={cn("px-2", { ref={ref}
"px-4": !sidebarCollapsed, className={cn("size-full flex flex-col flex-1 pt-4 pb-0", {
"p-2 pt-4": sidebarCollapsed,
})} })}
> >
{/* Workspace switcher and settings */} <div
<SidebarDropdown /> className={cn("px-2", {
<div className="flex-shrink-0 h-4" /> "px-4": !sidebarCollapsed,
{/* App switcher */} })}
{canPerformWorkspaceMemberActions && <SidebarAppSwitcher />} >
{/* Quick actions */} {/* Workspace switcher and settings */}
<SidebarQuickActions /> <SidebarDropdown />
</div> <div className="flex-shrink-0 h-4" />
<hr {/* App switcher */}
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", { {canPerformWorkspaceMemberActions && <SidebarAppSwitcher />}
"opacity-0": !sidebarCollapsed, {/* Quick actions */}
})} <SidebarQuickActions />
/> </div>
<div
className={cn("overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto px-2 py-0.5", {
"vertical-scrollbar px-4": !sidebarCollapsed,
})}
>
{/* User Menu */}
<SidebarUserMenu />
{/* Workspace Menu */}
<SidebarWorkspaceMenu />
<hr <hr
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", { className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
"opacity-0": !sidebarCollapsed, "opacity-0": !sidebarCollapsed,
})} })}
/> />
{/* Favorites Menu */} <div
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />} className={cn("overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto px-2 py-0.5", {
{/* Teams List */} "vertical-scrollbar px-4": !sidebarCollapsed,
<SidebarTeamsList /> })}
{/* Projects List */} >
<SidebarProjectsList /> <SidebarMenuItems />
<hr
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
"opacity-0": !sidebarCollapsed,
})}
/>
{/* Favorites Menu */}
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
{/* Teams List */}
<SidebarTeamsList />
{/* Projects List */}
<SidebarProjectsList />
</div>
{/* Help Section */}
<SidebarHelpSection />
</div> </div>
{/* Help Section */}
<SidebarHelpSection />
</div> </div>
</div> <ExtendedAppSidebar />
<ExtendedProjectSidebar />
</>
); );
}); });

View file

@ -0,0 +1,220 @@
import { FC, useEffect, useRef, useState } from "react";
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { Eye, EyeClosed } from "lucide-react";
// plane imports
import { EUserPermissionsLevel, IWorkspaceSidebarNavigationItem } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { DragHandle, DropIndicator, Tooltip } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { SidebarNavItem } from "@/components/sidebar";
// hooks
import { useAppTheme, useUser, useUserPermissions, useWorkspace } from "@/hooks/store";
// plane web imports
// local imports
import { UpgradeBadge } from "../upgrade-badge";
import { getSidebarNavigationItemIcon } from "./helper";
type TExtendedSidebarItemProps = {
item: IWorkspaceSidebarNavigationItem;
handleOnNavigationItemDrop?: (
sourceId: string | undefined,
destinationId: string | undefined,
shouldDropAtEnd: boolean
) => void;
disableDrag?: boolean;
disableDrop?: boolean;
isLastChild: boolean;
};
export const ExtendedSidebarItem: FC<TExtendedSidebarItemProps> = observer((props) => {
const { item, handleOnNavigationItemDrop, disableDrag = false, disableDrop = false, isLastChild } = props;
const { t } = useTranslation();
// states
const [isDragging, setIsDragging] = useState(false);
const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined);
// refs
const navigationIemRef = useRef<HTMLDivElement | null>(null);
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
// nextjs hooks
const pathname = usePathname();
const { workspaceSlug } = useParams();
// store hooks
const { getNavigationPreferences, updateSidebarPreference } = useWorkspace();
const { toggleExtendedSidebar } = useAppTheme();
const { data } = useUser();
const { allowPermissions } = useUserPermissions();
// derived values
const sidebarPreference = getNavigationPreferences(workspaceSlug.toString());
const isPinned = sidebarPreference?.[item.key]?.is_pinned;
const handleLinkClick = () => toggleExtendedSidebar();
if (!allowPermissions(item.access as any, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString())) {
return null;
}
const itemHref =
item.key === "your_work"
? `/${workspaceSlug.toString()}${item.href}${data?.id}`
: `/${workspaceSlug.toString()}${item.href}`;
const isActive = itemHref === pathname;
const pinNavigationItem = (workspaceSlug: string, key: string) => {
updateSidebarPreference(workspaceSlug, key, { is_pinned: true });
};
const unPinNavigationItem = (workspaceSlug: string, key: string) => {
updateSidebarPreference(workspaceSlug, key, { is_pinned: false });
};
const icon = getSidebarNavigationItemIcon(item.key);
useEffect(() => {
const element = navigationIemRef.current;
const dragHandleElement = dragHandleRef.current;
if (!element) return;
return combine(
draggable({
element,
canDrag: () => !disableDrag,
dragHandle: dragHandleElement ?? undefined,
getInitialData: () => ({ id: item.key, dragInstanceId: "NAVIGATION" }), // var1
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
},
}),
dropTargetForElements({
element,
canDrop: ({ source }) =>
!disableDrop && source?.data?.id !== item.key && source?.data?.dragInstanceId === "NAVIGATION",
getData: ({ input, element }) => {
const data = { id: item.key };
// attach instruction for last in list
return attachInstruction(data, {
input,
element,
currentLevel: 0,
indentPerLevel: 0,
mode: isLastChild ? "last-in-group" : "standard",
});
},
onDrag: ({ self }) => {
const extractedInstruction = extractInstruction(self?.data)?.type;
// check if the highlight is to be shown above or below
setInstruction(
extractedInstruction
? extractedInstruction === "reorder-below" && isLastChild
? "DRAG_BELOW"
: "DRAG_OVER"
: undefined
);
},
onDragLeave: () => {
setInstruction(undefined);
},
onDrop: ({ self, source }) => {
setInstruction(undefined);
const extractedInstruction = extractInstruction(self?.data)?.type;
const currentInstruction = extractedInstruction
? extractedInstruction === "reorder-below" && isLastChild
? "DRAG_BELOW"
: "DRAG_OVER"
: undefined;
if (!currentInstruction) return;
const sourceId = source?.data?.id as string | undefined;
const destinationId = self?.data?.id as string | undefined;
if (handleOnNavigationItemDrop)
handleOnNavigationItemDrop(sourceId, destinationId, currentInstruction === "DRAG_BELOW");
},
})
);
}, [isLastChild, handleOnNavigationItemDrop, disableDrag, disableDrop, item.key]);
return (
<div
id={`sidebar-${item.key}`}
className={cn("relative", { "bg-custom-sidebar-background-80 opacity-60": isDragging })}
ref={navigationIemRef}
>
<DropIndicator classNames="absolute top-0" isVisible={instruction === "DRAG_OVER"} />
<div
className={cn(
"group/project-item relative w-full flex items-center rounded-md text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-90"
)}
id={`${item.key}`}
>
{!disableDrag && (
<Tooltip
// isMobile={isMobile}
tooltipContent={t("drag_to_rearrange")}
position="top-right"
disabled={isDragging}
>
<button
type="button"
className={cn(
"hidden group-hover/project-item:flex items-center justify-center absolute top-1/2 -left-3 -translate-y-1/2 rounded text-custom-sidebar-text-400 cursor-grab",
{
// "cursor-not-allowed opacity-60": project.sort_order === null,
"cursor-grabbing": isDragging,
// "!hidden": isSidebarCollapsed,
}
)}
ref={dragHandleRef}
>
<DragHandle className="bg-transparent" />
</button>
</Tooltip>
)}
<SidebarNavItem isActive={isActive}>
<Link href={itemHref} onClick={() => handleLinkClick()} className="group flex-grow">
<div className="flex items-center gap-1.5 py-[1px]">
{icon}
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
</div>
</Link>
<div className="flex items-center gap-2">
{item.key === "active_cycles" && (
<div className="flex-shrink-0">
<UpgradeBadge />
</div>
)}
{isPinned ? (
<Tooltip tooltipContent="Hide tab">
<Eye
className="size-4 flex-shrink-0 invisible group-hover:visible text-custom-text-300 outline-none"
onClick={() => unPinNavigationItem(workspaceSlug.toString(), item.key)}
/>
</Tooltip>
) : (
<Tooltip tooltipContent="Show tab">
<EyeClosed
className="size-4 flex-shrink-0 invisible group-hover:visible text-custom-text-400 outline-none"
onClick={() => pinNavigationItem(workspaceSlug.toString(), item.key)}
/>
</Tooltip>
)}
</div>
</SidebarNavItem>
</div>
{isLastChild && <DropIndicator isVisible={instruction === "DRAG_BELOW"} />}
</div>
);
});

View file

@ -0,0 +1,26 @@
import { BarChart2, Briefcase, Home, Inbox, Layers, PenSquare } from "lucide-react";
import { ArchiveIcon, ContrastIcon, UserActivityIcon } from "@plane/ui";
import { cn } from "@plane/utils";
export const getSidebarNavigationItemIcon = (key: string, className: string = "") => {
switch (key) {
case "home":
return <Home className={cn("size-4 flex-shrink-0", className)} />;
case "notifications":
return <Inbox className={cn("size-4 flex-shrink-0", className)} />;
case "projects":
return <Briefcase className={cn("size-4 flex-shrink-0", className)} />;
case "views":
return <Layers className={cn("size-4 flex-shrink-0", className)} />;
case "active_cycles":
return <ContrastIcon className={cn("size-4 flex-shrink-0", className)} />;
case "analytics":
return <BarChart2 className={cn("size-4 flex-shrink-0", className)} />;
case "your_work":
return <UserActivityIcon className={cn("size-4 flex-shrink-0", className)} />;
case "drafts":
return <PenSquare className={cn("size-4 flex-shrink-0", className)} />;
case "archives":
return <ArchiveIcon className={cn("size-4 flex-shrink-0", className)} />;
}
};

View file

@ -0,0 +1,3 @@
export * from "./extended-sidebar-item";
export * from "./helper";
export * from "./sidebar-item";

View file

@ -0,0 +1,91 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, IWorkspaceSidebarNavigationItem } from "@plane/constants";
import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { Tooltip } from "@plane/ui";
// components
import { SidebarNavItem } from "@/components/sidebar";
// hooks
import { useAppTheme, useUser, useUserPermissions, useWorkspace } from "@/hooks/store";
// plane web imports
import { UpgradeBadge } from "@/plane-web/components/workspace";
// local imports
import { getSidebarNavigationItemIcon } from "./helper";
type TSidebarItemProps = {
item: IWorkspaceSidebarNavigationItem;
};
export const SidebarItem: FC<TSidebarItemProps> = observer((props) => {
const { item } = props;
const { t } = useTranslation();
// nextjs hooks
const pathname = usePathname();
const { workspaceSlug } = useParams();
const { allowPermissions } = useUserPermissions();
const { getNavigationPreferences } = useWorkspace();
const { data } = useUser();
// store hooks
const { toggleSidebar, sidebarCollapsed, extendedSidebarCollapsed, toggleExtendedSidebar } = useAppTheme();
const { isMobile } = usePlatformOS();
const handleLinkClick = () => {
if (window.innerWidth < 768) {
toggleSidebar();
}
if (extendedSidebarCollapsed) toggleExtendedSidebar();
};
const staticItems = ["home", "notifications", "pi-chat", "projects"];
if (!allowPermissions(item.access as any, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString())) {
return null;
}
const itemHref =
item.key === "your_work"
? `/${workspaceSlug.toString()}${item.href}/${data?.id}`
: `/${workspaceSlug.toString()}${item.href}`;
const isActive = itemHref === pathname;
const sidebarPreference = getNavigationPreferences(workspaceSlug.toString());
const isPinned = sidebarPreference?.[item.key]?.is_pinned;
if (!isPinned && !staticItems.includes(item.key)) return null;
const icon = getSidebarNavigationItemIcon(item.key);
return (
<Tooltip
tooltipContent={t(item.labelTranslationKey)}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
isMobile={isMobile}
>
<Link href={itemHref} onClick={() => handleLinkClick()}>
<SidebarNavItem
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
isActive={isActive}
>
{/* <icon className="size-4" /> */}
<div className="flex items-center gap-1.5 py-[1px]">
{icon}
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>}
</div>
{!sidebarCollapsed && item.key === "active_cycles" && (
<div className="flex-shrink-0">
<UpgradeBadge />
</div>
)}
</SidebarNavItem>
</Link>
</Tooltip>
);
});

View file

@ -69,7 +69,11 @@ export const SidebarDropdown = observer(() => {
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {})); const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
// TODO: fix workspaces list scroll // TODO: fix workspaces list scroll
return ( 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 <Menu
as="div" as="div"
className={cn("relative h-full truncate text-left flex-grow flex justify-stretch", { className={cn("relative h-full truncate text-left flex-grow flex justify-stretch", {
@ -185,70 +189,68 @@ export const SidebarDropdown = observer(() => {
</> </>
)} )}
</Menu> </Menu>
{!sidebarCollapsed && ( <Menu as="div" className="relative flex-shrink-0">
<Menu as="div" className="relative flex-shrink-0"> <Menu.Button className="grid place-items-center outline-none" ref={setReferenceElement}>
<Menu.Button className="grid place-items-center outline-none" ref={setReferenceElement}> <Avatar
<Avatar name={currentUser?.display_name}
name={currentUser?.display_name} src={getFileURL(currentUser?.avatar_url ?? "")}
src={getFileURL(currentUser?.avatar_url ?? "")} size={24}
size={24} shape="circle"
shape="square" className="!text-base"
className="!text-base" />
/> </Menu.Button>
</Menu.Button> <Transition
<Transition as={Fragment}
as={Fragment} enter="transition ease-out duration-100"
enter="transition ease-out duration-100" enterFrom="transform opacity-0 scale-95"
enterFrom="transform opacity-0 scale-95" enterTo="transform opacity-100 scale-100"
enterTo="transform opacity-100 scale-100" leave="transition ease-in duration-75"
leave="transition ease-in duration-75" leaveFrom="transform opacity-100 scale-100"
leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95"
leaveTo="transform opacity-0 scale-95" >
> <Menu.Items
<Menu.Items className="absolute left-0 z-[21] mt-1 flex w-52 origin-top-left flex-col divide-y
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" 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>} ref={setPopperElement as Ref<HTMLDivElement>}
style={styles.popper} style={styles.popper}
{...attributes.popper} {...attributes.popper}
> >
<div className="flex flex-col gap-2.5 pb-2"> <div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span> <span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
<Link href="/profile"> <Link href="/profile">
<Menu.Item as="div"> <Menu.Item as="div">
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"> <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]" /> <Settings className="h-4 w-4 stroke-[1.5]" />
<span>{t("settings")}</span> <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> </span>
</Menu.Item> </Menu.Item>
</Link> </Link>
</div> </div>
<div className={`pt-2 ${isUserInstanceAdmin || false ? "pb-2" : ""}`}> )}
<Menu.Item </Menu.Items>
as="button" </Transition>
type="button" </Menu>
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>
)}
</div> </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 { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
import { useParams, useRouter } from "next/navigation"; import { useParams } from "next/navigation";
import { createRoot } from "react-dom/client"; 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"; import { Disclosure, Transition } from "@headlessui/react";
// plane helpers // plane helpers
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks"; import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// ui // ui
import { CustomMenu, Tooltip, ArchiveIcon, setPromiseToast, DropIndicator, DragHandle, ControlLink } from "@plane/ui"; import { CustomMenu, Tooltip, ArchiveIcon, DropIndicator, DragHandle, ControlLink } from "@plane/ui";
// components // components
import { Logo } from "@/components/common"; import { Logo } from "@/components/common";
import { LeaveProjectModal, PublishProjectModal } from "@/components/project"; import { LeaveProjectModal, PublishProjectModal } from "@/components/project";
@ -52,7 +52,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { addProjectToFavorites, removeProjectFromFavorites, getPartialProjectById } = useProject(); const { getPartialProjectById } = useProject();
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
// states // states
@ -67,7 +67,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
const projectRef = useRef<HTMLDivElement | null>(null); const projectRef = useRef<HTMLDivElement | null>(null);
const dragHandleRef = useRef<HTMLButtonElement | null>(null); const dragHandleRef = useRef<HTMLButtonElement | null>(null);
// router // router
const router = useRouter();
const { workspaceSlug, projectId: URLProjectId } = useParams(); const { workspaceSlug, projectId: URLProjectId } = useParams();
// derived values // derived values
const project = getPartialProjectById(projectId); const project = getPartialProjectById(projectId);
@ -85,40 +84,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
project?.id 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 = () => { const handleLeaveProject = () => {
setTrackElement("APP_SIDEBAR_PROJECT_DROPDOWN"); setTrackElement("APP_SIDEBAR_PROJECT_DROPDOWN");
setLeaveProjectModal(true); setLeaveProjectModal(true);
@ -222,11 +187,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
if (URLProjectId === project.id) setIsProjectListOpen(true); if (URLProjectId === project.id) setIsProjectListOpen(true);
}, [URLProjectId]); }, [URLProjectId]);
const handleItemClick = () => { const handleItemClick = () => setIsProjectListOpen((prev) => !prev);
if (!isProjectListOpen && !isMobile) router.push(`/${workspaceSlug}/projects/${project.id}/issues`);
setIsProjectListOpen((prev) => !prev);
};
return ( return (
<> <>
<PublishProjectModal isOpen={publishModalOpen} project={project} onClose={() => setPublishModal(false)} /> <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 { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation"; 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 { Disclosure, Transition } from "@headlessui/react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
@ -13,6 +13,7 @@ import { useTranslation } from "@plane/i18n";
import { Loader, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; import { Loader, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
// components // components
import { CreateProjectModal } from "@/components/project"; import { CreateProjectModal } from "@/components/project";
import { SidebarNavItem } from "@/components/sidebar";
import { SidebarProjectsListItem } from "@/components/workspace"; import { SidebarProjectsListItem } from "@/components/workspace";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
@ -37,7 +38,7 @@ export const SidebarProjectsList: FC = observer(() => {
// store hooks // store hooks
const { t } = useTranslation(); const { t } = useTranslation();
const { toggleCreateProjectModal } = useCommandPalette(); const { toggleCreateProjectModal } = useCommandPalette();
const { sidebarCollapsed } = useAppTheme(); const { sidebarCollapsed, toggleExtendedProjectSidebar } = useAppTheme();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
@ -237,12 +238,12 @@ export const SidebarProjectsList: FC = observer(() => {
{isAllProjectsListOpen && ( {isAllProjectsListOpen && (
<Disclosure.Panel <Disclosure.Panel
as="div" as="div"
className={cn("space-y-1", { className={cn("flex flex-col gap-0.5", {
"space-y-0 ml-0": isCollapsed, "space-y-0 ml-0": isCollapsed,
})} })}
static static
> >
{joinedProjects.map((projectId, index) => ( {joinedProjects.slice(0, 7).map((projectId, index) => (
<SidebarProjectsListItem <SidebarProjectsListItem
key={projectId} key={projectId}
projectId={projectId} projectId={projectId}
@ -277,6 +278,21 @@ export const SidebarProjectsList: FC = observer(() => {
{!isCollapsed && t("add_project")} {!isCollapsed && t("add_project")}
</button> </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> </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]"> <div className="flex items-center gap-1.5 py-[1px]">
<item.Icon <item.Icon
className={cn("size-4", { 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>} {!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>}
</div> </div>
{!sidebarCollapsed && item.key === "active-cycles" && ( {!sidebarCollapsed && item.key === "active_cycles" && (
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<UpgradeBadge /> <UpgradeBadge />
</div> </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 { const {
workspace: { fetchWorkspaceMembers }, workspace: { fetchWorkspaceMembers },
} = useMember(); } = useMember();
const { workspaces } = useWorkspace(); const { workspaces, fetchSidebarNavigationPreferences } = useWorkspace();
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const { loader, workspaceInfoBySlug, fetchUserWorkspaceInfo, fetchUserProjectPermissions, allowPermissions } = const { loader, workspaceInfoBySlug, fetchUserWorkspaceInfo, fetchUserProjectPermissions, allowPermissions } =
useUserPermissions(); useUserPermissions();
@ -101,6 +101,13 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
{ revalidateIfStale: false, revalidateOnFocus: false } { 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 // initialize the local database
const { isLoading: isDBInitializing } = useSWRImmutable( const { isLoading: isDBInitializing } = useSWRImmutable(
workspaceSlug ? `WORKSPACE_DB_${workspaceSlug}` : null, workspaceSlug ? `WORKSPACE_DB_${workspaceSlug}` : null,

View file

@ -18,6 +18,7 @@ import {
TWidgetEntityData, TWidgetEntityData,
TActivityEntityData, TActivityEntityData,
} from "@plane/types"; } from "@plane/types";
import { IWorkspaceSidebarNavigationItem, IWorkspaceSidebarNavigation } from "@plane/types/src/workspace";
import { APIService } from "@/services/api.service"; import { APIService } from "@/services/api.service";
// helpers // helpers
// types // types
@ -362,4 +363,24 @@ export class WorkspaceService extends APIService {
throw error?.response; 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 { export interface IThemeStore {
// observables // observables
sidebarCollapsed: boolean | undefined; sidebarCollapsed: boolean | undefined;
extendedSidebarCollapsed: boolean | undefined;
extendedProjectSidebarCollapsed: boolean | undefined;
profileSidebarCollapsed: boolean | undefined; profileSidebarCollapsed: boolean | undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined;
issueDetailSidebarCollapsed: boolean | undefined; issueDetailSidebarCollapsed: boolean | undefined;
@ -11,6 +13,8 @@ export interface IThemeStore {
projectOverviewSidebarCollapsed: boolean | undefined; projectOverviewSidebarCollapsed: boolean | undefined;
// actions // actions
toggleSidebar: (collapsed?: boolean) => void; toggleSidebar: (collapsed?: boolean) => void;
toggleExtendedSidebar: (collapsed?: boolean) => void;
toggleExtendedProjectSidebar: (collapsed?: boolean) => void;
toggleProfileSidebar: (collapsed?: boolean) => void; toggleProfileSidebar: (collapsed?: boolean) => void;
toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void; toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void;
toggleIssueDetailSidebar: (collapsed?: boolean) => void; toggleIssueDetailSidebar: (collapsed?: boolean) => void;
@ -22,6 +26,8 @@ export interface IThemeStore {
export class ThemeStore implements IThemeStore { export class ThemeStore implements IThemeStore {
// observables // observables
sidebarCollapsed: boolean | undefined = undefined; sidebarCollapsed: boolean | undefined = undefined;
extendedSidebarCollapsed: boolean | undefined = undefined;
extendedProjectSidebarCollapsed: boolean | undefined = undefined;
profileSidebarCollapsed: boolean | undefined = undefined; profileSidebarCollapsed: boolean | undefined = undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined;
issueDetailSidebarCollapsed: boolean | undefined = undefined; issueDetailSidebarCollapsed: boolean | undefined = undefined;
@ -33,6 +39,8 @@ export class ThemeStore implements IThemeStore {
makeObservable(this, { makeObservable(this, {
// observable // observable
sidebarCollapsed: observable.ref, sidebarCollapsed: observable.ref,
extendedSidebarCollapsed: observable.ref,
extendedProjectSidebarCollapsed: observable.ref,
profileSidebarCollapsed: observable.ref, profileSidebarCollapsed: observable.ref,
workspaceAnalyticsSidebarCollapsed: observable.ref, workspaceAnalyticsSidebarCollapsed: observable.ref,
issueDetailSidebarCollapsed: observable.ref, issueDetailSidebarCollapsed: observable.ref,
@ -41,6 +49,8 @@ export class ThemeStore implements IThemeStore {
projectOverviewSidebarCollapsed: observable.ref, projectOverviewSidebarCollapsed: observable.ref,
// action // action
toggleSidebar: action, toggleSidebar: action,
toggleExtendedSidebar: action,
toggleExtendedProjectSidebar: action,
toggleProfileSidebar: action, toggleProfileSidebar: action,
toggleWorkspaceAnalyticsSidebar: action, toggleWorkspaceAnalyticsSidebar: action,
toggleIssueDetailSidebar: action, toggleIssueDetailSidebar: action,
@ -63,6 +73,32 @@ export class ThemeStore implements IThemeStore {
localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString()); 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 * Toggle the profile sidebar collapsed state
* @param collapsed * @param collapsed

View file

@ -1,7 +1,8 @@
import set from "lodash/set"; import set from "lodash/set";
import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { action, computed, observable, makeObservable, runInAction } from "mobx";
// types // types
import { IWorkspace } from "@plane/types"; import { computedFn } from "mobx-utils";
import { IWorkspaceSidebarNavigationItem, IWorkspace, IWorkspaceSidebarNavigation } from "@plane/types";
// services // services
import { WorkspaceService } from "@/plane-web/services"; import { WorkspaceService } from "@/plane-web/services";
// store // store
@ -18,6 +19,7 @@ export interface IWorkspaceRootStore {
// computed // computed
currentWorkspace: IWorkspace | null; currentWorkspace: IWorkspace | null;
workspacesCreatedByCurrentUser: IWorkspace[] | null; workspacesCreatedByCurrentUser: IWorkspace[] | null;
navigationPreferencesMap: Record<string, IWorkspaceSidebarNavigation>;
// computed actions // computed actions
getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null; getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null;
getWorkspaceById: (workspaceId: string) => IWorkspace | null; getWorkspaceById: (workspaceId: string) => IWorkspace | null;
@ -28,6 +30,13 @@ export interface IWorkspaceRootStore {
updateWorkspace: (workspaceSlug: string, data: Partial<IWorkspace>) => Promise<IWorkspace>; updateWorkspace: (workspaceSlug: string, data: Partial<IWorkspace>) => Promise<IWorkspace>;
updateWorkspaceLogo: (workspaceSlug: string, logoURL: string) => void; updateWorkspaceLogo: (workspaceSlug: string, logoURL: string) => void;
deleteWorkspace: (workspaceSlug: string) => Promise<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 // sub-stores
webhook: IWebhookStore; webhook: IWebhookStore;
apiToken: IApiTokenStore; apiToken: IApiTokenStore;
@ -38,6 +47,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
loader: boolean = false; loader: boolean = false;
// observables // observables
workspaces: Record<string, IWorkspace> = {}; workspaces: Record<string, IWorkspace> = {};
navigationPreferencesMap: Record<string, IWorkspaceSidebarNavigation> = {};
// services // services
workspaceService; workspaceService;
// root store // root store
@ -53,6 +63,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
loader: observable.ref, loader: observable.ref,
// observables // observables
workspaces: observable, workspaces: observable,
navigationPreferencesMap: observable,
// computed // computed
currentWorkspace: computed, currentWorkspace: computed,
workspacesCreatedByCurrentUser: computed, workspacesCreatedByCurrentUser: computed,
@ -65,6 +76,8 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
updateWorkspace: action, updateWorkspace: action,
updateWorkspaceLogo: action, updateWorkspaceLogo: action,
deleteWorkspace: action, deleteWorkspace: action,
fetchSidebarNavigationPreferences: action,
updateSidebarPreference: action,
}); });
// services // services
@ -183,4 +196,41 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
this.workspaces = updatedWorkspacesList; 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]
);
} }

View file

@ -0,0 +1 @@
export * from "ce/components/workspace/sidebar";

View file

@ -1102,127 +1102,127 @@
"@esbuild/aix-ppc64@0.25.0": "@esbuild/aix-ppc64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64"
integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==
"@esbuild/android-arm64@0.25.0": "@esbuild/android-arm64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f"
integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==
"@esbuild/android-arm@0.25.0": "@esbuild/android-arm@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b"
integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==
"@esbuild/android-x64@0.25.0": "@esbuild/android-x64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163"
integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==
"@esbuild/darwin-arm64@0.25.0": "@esbuild/darwin-arm64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c"
integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==
"@esbuild/darwin-x64@0.25.0": "@esbuild/darwin-x64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a"
integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==
"@esbuild/freebsd-arm64@0.25.0": "@esbuild/freebsd-arm64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce"
integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==
"@esbuild/freebsd-x64@0.25.0": "@esbuild/freebsd-x64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7"
integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==
"@esbuild/linux-arm64@0.25.0": "@esbuild/linux-arm64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73"
integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==
"@esbuild/linux-arm@0.25.0": "@esbuild/linux-arm@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3"
integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==
"@esbuild/linux-ia32@0.25.0": "@esbuild/linux-ia32@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19"
integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==
"@esbuild/linux-loong64@0.25.0": "@esbuild/linux-loong64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7"
integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==
"@esbuild/linux-mips64el@0.25.0": "@esbuild/linux-mips64el@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1"
integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==
"@esbuild/linux-ppc64@0.25.0": "@esbuild/linux-ppc64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951"
integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==
"@esbuild/linux-riscv64@0.25.0": "@esbuild/linux-riscv64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987"
integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==
"@esbuild/linux-s390x@0.25.0": "@esbuild/linux-s390x@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4"
integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==
"@esbuild/linux-x64@0.25.0": "@esbuild/linux-x64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a"
integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==
"@esbuild/netbsd-arm64@0.25.0": "@esbuild/netbsd-arm64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b"
integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==
"@esbuild/netbsd-x64@0.25.0": "@esbuild/netbsd-x64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b"
integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==
"@esbuild/openbsd-arm64@0.25.0": "@esbuild/openbsd-arm64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7"
integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==
"@esbuild/openbsd-x64@0.25.0": "@esbuild/openbsd-x64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde"
integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==
"@esbuild/sunos-x64@0.25.0": "@esbuild/sunos-x64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92"
integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==
"@esbuild/win32-arm64@0.25.0": "@esbuild/win32-arm64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c"
integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==
"@esbuild/win32-ia32@0.25.0": "@esbuild/win32-ia32@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079"
integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==
"@esbuild/win32-x64@0.25.0": "@esbuild/win32-x64@0.25.0":
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b"
integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ== integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
@ -4199,25 +4199,18 @@
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044"
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
"@types/react@*": "@types/react@*", "@types/react@^18.3.11":
version "19.0.8" version "18.3.18"
resolved "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz#7098e6159f2a61e4f4cef2c1223c044a9bec590e" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.18.tgz#9b382c4cd32e13e463f97df07c2ee3bbcd26904b"
integrity sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw== integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==
dependencies:
csstype "^3.0.2"
"@types/react@18.3.1":
version "18.3.1"
resolved "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz#fed43985caa834a2084d002e4771e15dfcbdbe8e"
integrity sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==
dependencies: dependencies:
"@types/prop-types" "*" "@types/prop-types" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/react@^18.3.11": "@types/react@18.3.1":
version "18.3.18" version "18.3.1"
resolved "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz#9b382c4cd32e13e463f97df07c2ee3bbcd26904b" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.1.tgz#fed43985caa834a2084d002e4771e15dfcbdbe8e"
integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ== integrity sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==
dependencies: dependencies:
"@types/prop-types" "*" "@types/prop-types" "*"
csstype "^3.0.2" csstype "^3.0.2"
@ -6579,7 +6572,7 @@ esbuild-register@^3.5.0:
esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.19.2: esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.19.2:
version "0.25.0" version "0.25.0"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92"
integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==
optionalDependencies: optionalDependencies:
"@esbuild/aix-ppc64" "0.25.0" "@esbuild/aix-ppc64" "0.25.0"
@ -9117,7 +9110,7 @@ mz@^2.7.0:
nanoid@3.3.8, nanoid@^3.3.6, nanoid@^3.3.8: nanoid@3.3.8, nanoid@^3.3.6, nanoid@^3.3.8:
version "3.3.8" version "3.3.8"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
napi-build-utils@^2.0.0: napi-build-utils@^2.0.0: