mirror of
https://github.com/makeplane/plane.git
synced 2026-02-04 21:21:03 -06:00
[WEB-2818] chore: project navigation items code refactor (#6170)
* chore: project navigation items code refactor * fix: build error * chore: code refactor * chore: code refactor
This commit is contained in:
committed by
GitHub
parent
a85e592ada
commit
5c907db0e2
@@ -1 +1,2 @@
|
||||
export * from "./app-switcher";
|
||||
export * from "./project-navigation-root";
|
||||
|
||||
15
web/ce/components/sidebar/project-navigation-root.tsx
Normal file
15
web/ce/components/sidebar/project-navigation-root.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
// components
|
||||
import { ProjectNavigation } from "@/components/workspace";
|
||||
|
||||
type TProjectItemsRootProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const ProjectNavigationRoot: FC<TProjectItemsRootProps> = (props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
return <ProjectNavigation workspaceSlug={workspaceSlug} projectId={projectId} />;
|
||||
};
|
||||
@@ -3,6 +3,7 @@ export * from "./favorites";
|
||||
export * from "./help-section";
|
||||
export * from "./projects-list-item";
|
||||
export * from "./projects-list";
|
||||
export * from "./project-navigation";
|
||||
export * from "./quick-actions";
|
||||
export * from "./user-menu";
|
||||
export * from "./workspace-menu";
|
||||
|
||||
163
web/core/components/workspace/sidebar/project-navigation.tsx
Normal file
163
web/core/components/workspace/sidebar/project-navigation.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, useCallback, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { FileText, Layers } from "lucide-react";
|
||||
// plane ui
|
||||
import { Tooltip, DiceIcon, ContrastIcon, LayersIcon, Intake } from "@plane/ui";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
// hooks
|
||||
import { useAppTheme, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane-web constants
|
||||
import { EUserPermissions } from "@/plane-web/constants";
|
||||
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
export type TNavigationItem = {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: React.ElementType;
|
||||
access: EUserPermissions[];
|
||||
shouldRender: boolean;
|
||||
sortOrder: number;
|
||||
};
|
||||
|
||||
type TProjectItemsProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
additionalNavigationItems?: (workspaceSlug: string, projectId: string) => TNavigationItem[];
|
||||
};
|
||||
|
||||
export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
|
||||
const { workspaceSlug, projectId, additionalNavigationItems } = props;
|
||||
// store hooks
|
||||
const { sidebarCollapsed: isSidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||
const { getProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// pathname
|
||||
const pathname = usePathname();
|
||||
// derived values
|
||||
const project = getProjectById(projectId);
|
||||
// handlers
|
||||
const handleProjectClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
if (!project) return null;
|
||||
|
||||
const baseNavigation = useCallback(
|
||||
(workspaceSlug: string, projectId: string): TNavigationItem[] => [
|
||||
{
|
||||
name: "Issues",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/issues`,
|
||||
icon: LayersIcon,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
shouldRender: true,
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
name: "Cycles",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/cycles`,
|
||||
icon: ContrastIcon,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
shouldRender: project.cycle_view,
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
name: "Modules",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/modules`,
|
||||
icon: DiceIcon,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
shouldRender: project.module_view,
|
||||
sortOrder: 3,
|
||||
},
|
||||
{
|
||||
name: "Views",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/views`,
|
||||
icon: Layers,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
shouldRender: project.issue_views_view,
|
||||
sortOrder: 4,
|
||||
},
|
||||
{
|
||||
name: "Pages",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/pages`,
|
||||
icon: FileText,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
shouldRender: project.page_view,
|
||||
sortOrder: 5,
|
||||
},
|
||||
{
|
||||
name: "Intake",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/inbox`,
|
||||
icon: Intake,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
shouldRender: project.inbox_view,
|
||||
sortOrder: 6,
|
||||
},
|
||||
],
|
||||
[project]
|
||||
);
|
||||
|
||||
// memoized navigation items and adding additional navigation items
|
||||
const navigationItemsMemo = useMemo(() => {
|
||||
const navigationItems = (workspaceSlug: string, projectId: string): TNavigationItem[] => {
|
||||
const navItems = baseNavigation(workspaceSlug, projectId);
|
||||
|
||||
if (additionalNavigationItems) {
|
||||
navItems.push(...additionalNavigationItems(workspaceSlug, projectId));
|
||||
}
|
||||
|
||||
return navItems;
|
||||
};
|
||||
|
||||
// sort navigation items by sortOrder
|
||||
const sortedNavigationItems = navigationItems(workspaceSlug, projectId).sort(
|
||||
(a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)
|
||||
);
|
||||
|
||||
return sortedNavigationItems;
|
||||
}, [workspaceSlug, projectId, baseNavigation, additionalNavigationItems]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{navigationItemsMemo.map((item) => {
|
||||
if (!item.shouldRender) return;
|
||||
|
||||
const hasAccess = allowPermissions(item.access, EUserPermissionsLevel.PROJECT, workspaceSlug, project.id);
|
||||
if (!hasAccess) return null;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={item.name}
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${project?.name}: ${item.name}`}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!isSidebarCollapsed}
|
||||
>
|
||||
<Link href={item.href} onClick={handleProjectClick}>
|
||||
<SidebarNavItem
|
||||
className={`pl-[18px] ${isSidebarCollapsed ? "p-0 size-7 justify-center mx-auto" : ""}`}
|
||||
isActive={pathname.includes(item.href)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<item.icon
|
||||
className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`}
|
||||
/>
|
||||
{!isSidebarCollapsed && <span className="text-xs font-medium">{item.name}</span>}
|
||||
</div>
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -8,46 +8,24 @@ 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, usePathname, useRouter } from "next/navigation";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import {
|
||||
PenSquare,
|
||||
LinkIcon,
|
||||
Star,
|
||||
FileText,
|
||||
Settings,
|
||||
Share2,
|
||||
LogOut,
|
||||
MoreHorizontal,
|
||||
ChevronRight,
|
||||
Layers,
|
||||
} from "lucide-react";
|
||||
import { LinkIcon, Star, Settings, Share2, LogOut, MoreHorizontal, ChevronRight } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// ui
|
||||
import {
|
||||
CustomMenu,
|
||||
Tooltip,
|
||||
ArchiveIcon,
|
||||
DiceIcon,
|
||||
ContrastIcon,
|
||||
LayersIcon,
|
||||
setPromiseToast,
|
||||
DropIndicator,
|
||||
DragHandle,
|
||||
Intake,
|
||||
ControlLink,
|
||||
} from "@plane/ui";
|
||||
import { CustomMenu, Tooltip, ArchiveIcon, setPromiseToast, DropIndicator, DragHandle, ControlLink } from "@plane/ui";
|
||||
// components
|
||||
import { Logo } from "@/components/common";
|
||||
import { LeaveProjectModal, PublishProjectModal } from "@/components/project";
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppTheme, useEventTracker, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane-web components
|
||||
import { ProjectNavigationRoot } from "@/plane-web/components/sidebar";
|
||||
// constants
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
import { HIGHLIGHT_CLASS, highlightIssueOnDrop } from "../../issues/issue-layouts/utils";
|
||||
@@ -66,50 +44,11 @@ type Props = {
|
||||
isLastChild: boolean;
|
||||
};
|
||||
|
||||
const navigation = (workspaceSlug: string, projectId: string) => [
|
||||
{
|
||||
name: "Issues",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/issues`,
|
||||
Icon: LayersIcon,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
},
|
||||
{
|
||||
name: "Cycles",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/cycles`,
|
||||
Icon: ContrastIcon,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
},
|
||||
{
|
||||
name: "Modules",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/modules`,
|
||||
Icon: DiceIcon,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
},
|
||||
{
|
||||
name: "Views",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/views`,
|
||||
Icon: Layers,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
},
|
||||
{
|
||||
name: "Pages",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/pages`,
|
||||
Icon: FileText,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
},
|
||||
{
|
||||
name: "Intake",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/inbox`,
|
||||
Icon: Intake,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||
},
|
||||
];
|
||||
|
||||
export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||
const { projectId, handleCopyText, disableDrag, disableDrop, isLastChild, handleOnProjectDrop, projectListType } =
|
||||
props;
|
||||
// store hooks
|
||||
const { sidebarCollapsed: isSidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
@@ -128,8 +67,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId: URLProjectId } = useParams();
|
||||
// pathname
|
||||
const pathname = usePathname();
|
||||
// derived values
|
||||
const project = getProjectById(projectId);
|
||||
// auth
|
||||
@@ -185,12 +122,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||
setLeaveProjectModal(true);
|
||||
};
|
||||
|
||||
const handleProjectClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const element = projectRef.current;
|
||||
const dragHandleElement = dragHandleRef.current;
|
||||
@@ -503,50 +434,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||
>
|
||||
{isProjectListOpen && (
|
||||
<Disclosure.Panel as="div" className="flex flex-col gap-0.5 mt-1">
|
||||
{navigation(workspaceSlug?.toString(), project?.id).map((item) => {
|
||||
if (
|
||||
(item.name === "Cycles" && !project.cycle_view) ||
|
||||
(item.name === "Modules" && !project.module_view) ||
|
||||
(item.name === "Views" && !project.issue_views_view) ||
|
||||
(item.name === "Pages" && !project.page_view) ||
|
||||
(item.name === "Intake" && !project.inbox_view)
|
||||
)
|
||||
return;
|
||||
return (
|
||||
<>
|
||||
{allowPermissions(
|
||||
item.access,
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug.toString(),
|
||||
project.id
|
||||
) && (
|
||||
<Tooltip
|
||||
key={item.name}
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${project?.name}: ${item.name}`}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!isSidebarCollapsed}
|
||||
>
|
||||
<Link key={item.name} href={item.href} onClick={handleProjectClick}>
|
||||
<SidebarNavItem
|
||||
key={item.name}
|
||||
className={`pl-[18px] ${isSidebarCollapsed ? "p-0 size-7 justify-center mx-auto" : ""}`}
|
||||
isActive={pathname.includes(item.href)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
<item.Icon
|
||||
className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`}
|
||||
/>
|
||||
{!isSidebarCollapsed && <span className="text-xs font-medium">{item.name}</span>}
|
||||
</div>
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
<ProjectNavigationRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
</Disclosure.Panel>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
@@ -150,7 +150,7 @@ export const syncIssuesWithDeletedModules = async (deletedModuleIds: string[]) =
|
||||
return;
|
||||
}
|
||||
|
||||
const issues = await persistence.getIssues("", "", { modules: deletedModuleIds.join(","), cursor: "10000:0:0" }, {});
|
||||
const issues = await persistence.getIssues("", "", { module: deletedModuleIds.join(","), cursor: "10000:0:0" }, {});
|
||||
if (issues?.results && Array.isArray(issues.results)) {
|
||||
const promises = issues.results.map(async (issue: TIssue) => {
|
||||
const updatedIssue = {
|
||||
@@ -177,7 +177,7 @@ export const syncIssuesWithDeletedCycles = async (deletedCycleIds: string[]) =>
|
||||
return;
|
||||
}
|
||||
|
||||
const issues = await persistence.getIssues("", "", { cycles: deletedCycleIds.join(","), cursor: "10000:0:0" }, {});
|
||||
const issues = await persistence.getIssues("", "", { cycle: deletedCycleIds.join(","), cursor: "10000:0:0" }, {});
|
||||
if (issues?.results && Array.isArray(issues.results)) {
|
||||
const promises = issues.results.map(async (issue: TIssue) => {
|
||||
const updatedIssue = {
|
||||
@@ -204,7 +204,7 @@ export const syncIssuesWithDeletedStates = async (deletedStateIds: string[]) =>
|
||||
return;
|
||||
}
|
||||
|
||||
const issues = await persistence.getIssues("", "", { states: deletedStateIds.join(","), cursor: "10000:0:0" }, {});
|
||||
const issues = await persistence.getIssues("", "", { state: deletedStateIds.join(","), cursor: "10000:0:0" }, {});
|
||||
if (issues?.results && Array.isArray(issues.results)) {
|
||||
const promises = issues.results.map(async (issue: TIssue) => {
|
||||
const updatedIssue = {
|
||||
|
||||
Reference in New Issue
Block a user