diff --git a/web/ce/components/sidebar/index.ts b/web/ce/components/sidebar/index.ts index 5cda1afb5fe..129f4202072 100644 --- a/web/ce/components/sidebar/index.ts +++ b/web/ce/components/sidebar/index.ts @@ -1 +1,2 @@ export * from "./app-switcher"; +export * from "./project-navigation-root"; diff --git a/web/ce/components/sidebar/project-navigation-root.tsx b/web/ce/components/sidebar/project-navigation-root.tsx new file mode 100644 index 00000000000..25a0dd9d8ca --- /dev/null +++ b/web/ce/components/sidebar/project-navigation-root.tsx @@ -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 = (props) => { + const { workspaceSlug, projectId } = props; + return ; +}; diff --git a/web/core/components/workspace/sidebar/index.ts b/web/core/components/workspace/sidebar/index.ts index c1759bf0ff2..564e3177157 100644 --- a/web/core/components/workspace/sidebar/index.ts +++ b/web/core/components/workspace/sidebar/index.ts @@ -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"; diff --git a/web/core/components/workspace/sidebar/project-navigation.tsx b/web/core/components/workspace/sidebar/project-navigation.tsx new file mode 100644 index 00000000000..3e75adbfdd0 --- /dev/null +++ b/web/core/components/workspace/sidebar/project-navigation.tsx @@ -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 = 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 ( + + + +
+ + {!isSidebarCollapsed && {item.name}} +
+
+ +
+ ); + })} + + ); +}); diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index 209832c6a3f..9e1e4d769ad 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -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 = 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 = 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 = 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 = observer((props) => { > {isProjectListOpen && ( - {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 - ) && ( - - - -
- - {!isSidebarCollapsed && {item.name}} -
-
- -
- )} - - ); - })} +
)} diff --git a/web/core/local-db/utils/load-workspace.ts b/web/core/local-db/utils/load-workspace.ts index b0e2f60485f..42a79f9dd64 100644 --- a/web/core/local-db/utils/load-workspace.ts +++ b/web/core/local-db/utils/load-workspace.ts @@ -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 = {