diff --git a/apiserver/plane/app/views/workspace/user_preference.py b/apiserver/plane/app/views/workspace/user_preference.py index 66daffd1933..07ae70ac08b 100644 --- a/apiserver/plane/app/views/workspace/user_preference.py +++ b/apiserver/plane/app/views/workspace/user_preference.py @@ -30,7 +30,6 @@ def get(self, request, slug): keys = [ key for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices - if key not in ["projects"] ] for preference in keys: @@ -40,20 +39,28 @@ def get(self, request, slug): preference = WorkspaceUserPreference.objects.bulk_create( [ 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, ignore_conflicts=True, ) - preference = WorkspaceUserPreference.objects.filter( + preferences = WorkspaceUserPreference.objects.filter( 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( - preference.values("key", "is_pinned", "sort_order"), + user_preferences, status=status.HTTP_200_OK, ) diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index a7b4855790c..2c0370a61ad 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -391,13 +391,13 @@ def __str__(self): class WorkspaceUserPreference(BaseModel): """Preference for the workspace for a user""" - class UserPreferenceKeys(models.TextChoices): - PROJECTS = "projects", "Projects" - ANALYTICS = "analytics", "Analytics" - CYCLES = "cycles", "Cycles" + class UserPreferenceKeys(models.TextChoices): VIEWS = "views", "Views" + ACTIVE_CYCLES = "active_cycles", "Active Cycles" + ANALYTICS = "analytics", "Analytics" + DRAFTS = "drafts", "Drafts" YOUR_WORK = "your_work", "Your Work" - + ARCHIVES = "archives", "Archives" workspace = models.ForeignKey( "db.Workspace", diff --git a/packages/constants/src/workspace.ts b/packages/constants/src/workspace.ts index 3bbb5b1f31f..5466a7fb818 100644 --- a/packages/constants/src/workspace.ts +++ b/packages/constants/src/workspace.ts @@ -84,48 +84,42 @@ export const WORKSPACE_SETTINGS = { i18n_label: "workspace_settings.settings.general.title", href: `/settings`, access: [EUserWorkspaceRoles.ADMIN], - highlight: (pathname: string, baseUrl: string) => - pathname === `${baseUrl}/settings/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`, }, members: { key: "members", i18n_label: "workspace_settings.settings.members.title", href: `/settings/members`, access: [EUserWorkspaceRoles.ADMIN], - highlight: (pathname: string, baseUrl: string) => - pathname === `${baseUrl}/settings/members/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, }, "billing-and-plans": { key: "billing-and-plans", i18n_label: "workspace_settings.settings.billing_and_plans.title", href: `/settings/billing`, access: [EUserWorkspaceRoles.ADMIN], - highlight: (pathname: string, baseUrl: string) => - pathname === `${baseUrl}/settings/billing/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing/`, }, export: { key: "export", i18n_label: "workspace_settings.settings.exports.title", href: `/settings/exports`, access: [EUserWorkspaceRoles.ADMIN], - highlight: (pathname: string, baseUrl: string) => - pathname === `${baseUrl}/settings/exports/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`, }, webhooks: { key: "webhooks", i18n_label: "workspace_settings.settings.webhooks.title", href: `/settings/webhooks`, access: [EUserWorkspaceRoles.ADMIN], - highlight: (pathname: string, baseUrl: string) => - pathname === `${baseUrl}/settings/webhooks/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`, }, "api-tokens": { key: "api-tokens", i18n_label: "workspace_settings.settings.api_tokens.title", href: `/settings/api-tokens`, access: [EUserWorkspaceRoles.ADMIN], - highlight: (pathname: string, baseUrl: string) => - pathname === `${baseUrl}/settings/api-tokens/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`, }, }; @@ -256,3 +250,84 @@ export const DEFAULT_GLOBAL_VIEWS_LIST: { 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 = { + "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 = { + 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"], +]; diff --git a/packages/hooks/src/use-outside-click-detector.tsx b/packages/hooks/src/use-outside-click-detector.tsx index 9436b51bf37..54979f96ac1 100644 --- a/packages/hooks/src/use-outside-click-detector.tsx +++ b/packages/hooks/src/use-outside-click-detector.tsx @@ -8,9 +8,9 @@ export const useOutsideClickDetector = ( const handleClick = (event: MouseEvent) => { if (ref.current && !ref.current.contains(event.target as any)) { // check for the closest element with attribute name data-prevent-outside-click - const preventOutsideClickElement = ( - event.target as unknown as HTMLElement | undefined - )?.closest("[data-prevent-outside-click]"); + const preventOutsideClickElement = (event.target as unknown 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; diff --git a/packages/types/src/workspace.d.ts b/packages/types/src/workspace.d.ts index 3cec14baa4d..4393a911f6d 100644 --- a/packages/types/src/workspace.d.ts +++ b/packages/types/src/workspace.d.ts @@ -1,10 +1,4 @@ -import type { - ICycle, - IProjectMember, - IUser, - IUserLite, - IWorkspaceViewProps, -} from "@plane/types"; +import type { ICycle, IProjectMember, IUser, IUserLite, IWorkspaceViewProps, TPaginationInfo } from "@plane/types"; import { EUserWorkspaceRoles } from "@plane/constants"; // TODO: check if importing this over here causes circular dependency import { TUserPermissions } from "./enums"; @@ -229,3 +223,13 @@ export interface IWorkspaceAnalyticsResponse { export type TWorkspacePaginationInfo = TPaginationInfo & { results: IWorkspace[]; }; + +export interface IWorkspaceSidebarNavigationItem { + key?: string; + is_pinned: boolean; + sort_order: number; +} + +export interface IWorkspaceSidebarNavigation { + [key: string]: IWorkspaceSidebarNavigationItem; +} diff --git a/packages/ui/src/icons/overview-icon.tsx b/packages/ui/src/icons/overview-icon.tsx index 81b07224b69..6c32881d885 100644 --- a/packages/ui/src/icons/overview-icon.tsx +++ b/packages/ui/src/icons/overview-icon.tsx @@ -2,20 +2,13 @@ import * as React from "react"; import { ISvgIcons } from "./type"; -export const OverviewIcon: React.FC = ({ width = "16", height = "16", className = "" }) => ( - +export const OverviewIcon: React.FC = ({ className = "text-current", ...rest }) => ( + ); diff --git a/web/app/[workspaceSlug]/(projects)/extended-project-sidebar.tsx b/web/app/[workspaceSlug]/(projects)/extended-project-sidebar.tsx new file mode 100644 index 00000000000..dc0d7054a29 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/extended-project-sidebar.tsx @@ -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(null); + const [searchQuery, setSearchQuery] = useState(""); + // 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 && ( + setIsProjectModalOpen(false)} + setToFavorite={false} + workspaceSlug={workspaceSlug.toString()} + /> + )} +
+
+
+ Projects + {isAuthorizedUser && ( + + + + )} +
+
+ + setSearchQuery(e.target.value)} + /> +
+
+
+ {filteredProjects.map((projectId, index) => ( + {}} + projectListType={"JOINED"} + disableDrag={false} + disableDrop={false} + isLastChild={index === joinedProjects.length - 1} + handleOnProjectDrop={handleOnProjectDrop} + /> + ))} +
+
+ + ); +}); diff --git a/web/app/[workspaceSlug]/(projects)/extended-sidebar.tsx b/web/app/[workspaceSlug]/(projects)/extended-sidebar.tsx new file mode 100644 index 00000000000..09bca6af34a --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/extended-sidebar.tsx @@ -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(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 ( +
+ {sortedNavigationItems.map((item, index) => ( + + ))} +
+ ); +}); diff --git a/web/app/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/[workspaceSlug]/(projects)/sidebar.tsx index 6b98e6bdbff..ff345dcf04f 100644 --- a/web/app/[workspaceSlug]/(projects)/sidebar.tsx +++ b/web/app/[workspaceSlug]/(projects)/sidebar.tsx @@ -5,16 +5,10 @@ import { observer } from "mobx-react"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useOutsideClickDetector } from "@plane/hooks"; // components -import { - SidebarDropdown, - SidebarHelpSection, - SidebarProjectsList, - SidebarQuickActions, - SidebarUserMenu, - SidebarWorkspaceMenu, -} from "@/components/workspace"; -// helpers +import { SidebarDropdown, SidebarHelpSection, SidebarProjectsList, SidebarQuickActions } from "@/components/workspace"; 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"; // hooks import { useAppTheme, useUserPermissions } from "@/hooks/store"; @@ -23,6 +17,8 @@ import useSize from "@/hooks/use-window-size"; // plane web components import { SidebarAppSwitcher } from "@/plane-web/components/sidebar"; 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(() => { // store hooks @@ -55,62 +51,63 @@ export const AppSidebar: FC = observer(() => { const isFavoriteEmpty = isEmpty(groupedFavorites); return ( -
+ <>
- {/* Workspace switcher and settings */} - -
- {/* App switcher */} - {canPerformWorkspaceMemberActions && } - {/* Quick actions */} - -
-
-
- {/* User Menu */} - - {/* Workspace Menu */} - +
+ {/* Workspace switcher and settings */} + +
+ {/* App switcher */} + {canPerformWorkspaceMemberActions && } + {/* Quick actions */} + +

- {/* Favorites Menu */} - {canPerformWorkspaceMemberActions && !isFavoriteEmpty && } - {/* Teams List */} - - {/* Projects List */} - +
+ +
+ {/* Favorites Menu */} + {canPerformWorkspaceMemberActions && !isFavoriteEmpty && } + {/* Teams List */} + + {/* Projects List */} + +
+ {/* Help Section */} +
- {/* Help Section */} -
-
+ + + ); }); diff --git a/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx b/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx new file mode 100644 index 00000000000..663776eb4c6 --- /dev/null +++ b/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx @@ -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 = 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(null); + const dragHandleRef = useRef(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 ( +
+ +
+ {!disableDrag && ( + + + + )} + + handleLinkClick()} className="group flex-grow"> +
+ {icon} +

{t(item.labelTranslationKey)}

+
+ +
+ {item.key === "active_cycles" && ( +
+ +
+ )} + {isPinned ? ( + + unPinNavigationItem(workspaceSlug.toString(), item.key)} + /> + + ) : ( + + pinNavigationItem(workspaceSlug.toString(), item.key)} + /> + + )} +
+
+
+ {isLastChild && } +
+ ); +}); diff --git a/web/ce/components/workspace/sidebar/helper.tsx b/web/ce/components/workspace/sidebar/helper.tsx new file mode 100644 index 00000000000..b2dafda09f1 --- /dev/null +++ b/web/ce/components/workspace/sidebar/helper.tsx @@ -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 ; + case "notifications": + return ; + case "projects": + return ; + case "views": + return ; + case "active_cycles": + return ; + case "analytics": + return ; + case "your_work": + return ; + case "drafts": + return ; + case "archives": + return ; + } +}; diff --git a/web/ce/components/workspace/sidebar/index.ts b/web/ce/components/workspace/sidebar/index.ts new file mode 100644 index 00000000000..d9edcc13d30 --- /dev/null +++ b/web/ce/components/workspace/sidebar/index.ts @@ -0,0 +1,3 @@ +export * from "./extended-sidebar-item"; +export * from "./helper"; +export * from "./sidebar-item"; diff --git a/web/ce/components/workspace/sidebar/sidebar-item.tsx b/web/ce/components/workspace/sidebar/sidebar-item.tsx new file mode 100644 index 00000000000..99b535972b9 --- /dev/null +++ b/web/ce/components/workspace/sidebar/sidebar-item.tsx @@ -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 = 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 ( + + handleLinkClick()}> + + {/* */} +
+ {icon} + {!sidebarCollapsed &&

{t(item.labelTranslationKey)}

} +
+ {!sidebarCollapsed && item.key === "active_cycles" && ( +
+ +
+ )} +
+ +
+ ); +}); diff --git a/web/core/components/workspace/sidebar/dropdown.tsx b/web/core/components/workspace/sidebar/dropdown.tsx index 0719d95c0aa..f9e2aca5989 100644 --- a/web/core/components/workspace/sidebar/dropdown.tsx +++ b/web/core/components/workspace/sidebar/dropdown.tsx @@ -69,7 +69,11 @@ export const SidebarDropdown = observer(() => { const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {})); // TODO: fix workspaces list scroll return ( -
+
{ )} - {!sidebarCollapsed && ( - - - - - - + + + + + } - style={styles.popper} - {...attributes.popper} - > -
- {currentUser?.email} - - - - - {t("settings")} + ref={setPopperElement as Ref} + style={styles.popper} + {...attributes.popper} + > +
+ {currentUser?.email} + + + + + {t("settings")} + + + +
+
+ + + {t("sign_out")} + +
+ {isUserInstanceAdmin && ( +
+ + + + {t("enter_god_mode")}
-
- - - {t("sign_out")} - -
- {isUserInstanceAdmin && ( -
- - - - {t("enter_god_mode")} - - - -
- )} - - -
- )} + )} + + +
); }); diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index 644bf4ecf0c..ed0259d1c06 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -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 = 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 = observer((props) => { const projectRef = useRef(null); const dragHandleRef = useRef(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 = 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 = 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 ( <> setPublishModal(false)} /> diff --git a/web/core/components/workspace/sidebar/projects-list.tsx b/web/core/components/workspace/sidebar/projects-list.tsx index 2014062c4d4..5ff345bc801 100644 --- a/web/core/components/workspace/sidebar/projects-list.tsx +++ b/web/core/components/workspace/sidebar/projects-list.tsx @@ -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 && ( - {joinedProjects.map((projectId, index) => ( + {joinedProjects.slice(0, 7).map((projectId, index) => ( { {!isCollapsed && t("add_project")} )} + + {joinedProjects.length > 7 && ( + + + + )}
); diff --git a/web/core/components/workspace/sidebar/sidebar-menu-items.tsx b/web/core/components/workspace/sidebar/sidebar-menu-items.tsx new file mode 100644 index 00000000000..9e3bfc3bc3a --- /dev/null +++ b/web/core/components/workspace/sidebar/sidebar-menu-items.tsx @@ -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 ( + <> +
+ {WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS.map((item, _index) => ( + + ))} + {sortedNavigationItems.map((item, _index) => ( + + ))} +
+ + + + + ); +}); diff --git a/web/core/components/workspace/sidebar/workspace-menu-item.tsx b/web/core/components/workspace/sidebar/workspace-menu-item.tsx index 0fd72761406..6b06390e462 100644 --- a/web/core/components/workspace/sidebar/workspace-menu-item.tsx +++ b/web/core/components/workspace/sidebar/workspace-menu-item.tsx @@ -65,12 +65,12 @@ export const SidebarWorkspaceMenuItem: FC = obser
{!sidebarCollapsed &&

{t(item.labelTranslationKey)}

}
- {!sidebarCollapsed && item.key === "active-cycles" && ( + {!sidebarCollapsed && item.key === "active_cycles" && (
diff --git a/web/core/hooks/use-extended-sidebar-overview-outside-click.tsx b/web/core/hooks/use-extended-sidebar-overview-outside-click.tsx new file mode 100644 index 00000000000..7c3705251e5 --- /dev/null +++ b/web/core/hooks/use-extended-sidebar-overview-outside-click.tsx @@ -0,0 +1,49 @@ +import React, { useEffect } from "react"; + +const useExtendedSidebarOutsideClickDetector = ( + ref: React.RefObject, + 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; diff --git a/web/core/layouts/auth-layout/workspace-wrapper.tsx b/web/core/layouts/auth-layout/workspace-wrapper.tsx index 55a335cc712..8ced177dd3f 100644 --- a/web/core/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/core/layouts/auth-layout/workspace-wrapper.tsx @@ -44,7 +44,7 @@ export const WorkspaceAuthWrapper: FC = 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 = 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, diff --git a/web/core/services/workspace.service.ts b/web/core/services/workspace.service.ts index ef3699fad50..95ed2c976b3 100644 --- a/web/core/services/workspace.service.ts +++ b/web/core/services/workspace.service.ts @@ -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 { + 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 + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/sidebar-preferences/${key}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } } diff --git a/web/core/store/theme.store.ts b/web/core/store/theme.store.ts index cf37b1ae8f5..a5d961e2db2 100644 --- a/web/core/store/theme.store.ts +++ b/web/core/store/theme.store.ts @@ -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 diff --git a/web/core/store/workspace/index.ts b/web/core/store/workspace/index.ts index 112d4395518..f46ecc273e2 100644 --- a/web/core/store/workspace/index.ts +++ b/web/core/store/workspace/index.ts @@ -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; // computed actions getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null; getWorkspaceById: (workspaceId: string) => IWorkspace | null; @@ -28,6 +30,13 @@ export interface IWorkspaceRootStore { updateWorkspace: (workspaceSlug: string, data: Partial) => Promise; updateWorkspaceLogo: (workspaceSlug: string, logoURL: string) => void; deleteWorkspace: (workspaceSlug: string) => Promise; + fetchSidebarNavigationPreferences: (workspaceSlug: string) => Promise; + updateSidebarPreference: ( + workspaceSlug: string, + key: string, + data: Partial + ) => Promise; + 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 = {}; + navigationPreferencesMap: Record = {}; // 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 + ) => { + 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] + ); } diff --git a/web/ee/components/workspace/sidebar/index.ts b/web/ee/components/workspace/sidebar/index.ts new file mode 100644 index 00000000000..eb17e4018f4 --- /dev/null +++ b/web/ee/components/workspace/sidebar/index.ts @@ -0,0 +1 @@ +export * from "ce/components/workspace/sidebar"; diff --git a/yarn.lock b/yarn.lock index f82913e39a6..45d9a5d68a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1102,127 +1102,127 @@ "@esbuild/aix-ppc64@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== "@esbuild/android-arm64@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== "@esbuild/android-arm@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== "@esbuild/android-x64@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== "@esbuild/darwin-arm64@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== "@esbuild/darwin-x64@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== "@esbuild/freebsd-arm64@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== "@esbuild/freebsd-x64@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== "@esbuild/linux-arm64@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== "@esbuild/linux-arm@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== "@esbuild/linux-ia32@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== "@esbuild/linux-loong64@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== "@esbuild/linux-mips64el@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== "@esbuild/linux-ppc64@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== "@esbuild/linux-riscv64@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== "@esbuild/linux-s390x@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== "@esbuild/linux-x64@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== "@esbuild/netbsd-arm64@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== "@esbuild/netbsd-x64@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== "@esbuild/openbsd-arm64@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== "@esbuild/openbsd-x64@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== "@esbuild/sunos-x64@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== "@esbuild/win32-arm64@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== "@esbuild/win32-ia32@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== "@esbuild/win32-x64@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== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": @@ -4199,29 +4199,22 @@ resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== -"@types/react@*": - version "19.0.8" - resolved "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz#7098e6159f2a61e4f4cef2c1223c044a9bec590e" - integrity sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw== +"@types/react@*", "@types/react@^18.3.11": + version "18.3.18" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.18.tgz#9b382c4cd32e13e463f97df07c2ee3bbcd26904b" + integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ== dependencies: + "@types/prop-types" "*" 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" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.1.tgz#fed43985caa834a2084d002e4771e15dfcbdbe8e" integrity sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw== dependencies: "@types/prop-types" "*" csstype "^3.0.2" -"@types/react@^18.3.11": - version "18.3.18" - resolved "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz#9b382c4cd32e13e463f97df07c2ee3bbcd26904b" - integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - "@types/reactcss@*": version "1.2.13" resolved "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.13.tgz#11c7468cc96b5353f7af998a5664deae21c7af08" @@ -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: 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== optionalDependencies: "@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: 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== napi-build-utils@^2.0.0: