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