+
router.back()} isLoading={loader === "init-loader"} className="flex-grow-0">
+
{issuesCount && issuesCount > 0 ? (
diff --git a/web/ce/components/projects/navigation/helper.tsx b/web/ce/components/projects/navigation/helper.tsx
new file mode 100644
index 00000000000..1a99262c6a1
--- /dev/null
+++ b/web/ce/components/projects/navigation/helper.tsx
@@ -0,0 +1,77 @@
+import { FileText, Layers } from "lucide-react";
+import { EUserPermissions, EProjectFeatureKey } from "@plane/constants";
+import { ContrastIcon, DiceIcon, Intake, LayersIcon } from "@plane/ui";
+import { TNavigationItem } from "@/components/workspace";
+
+export const getProjectFeatureNavigation = (
+ workspaceSlug: string,
+ projectId: string,
+ project: {
+ cycle_view: boolean;
+ module_view: boolean;
+ issue_views_view: boolean;
+ page_view: boolean;
+ inbox_view: boolean;
+ }
+): TNavigationItem[] => [
+ {
+ i18n_key: "sidebar.work_items",
+ key: EProjectFeatureKey.WORK_ITEMS,
+ name: "Work items",
+ href: `/${workspaceSlug}/projects/${projectId}/issues`,
+ icon: LayersIcon,
+ access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
+ shouldRender: true,
+ sortOrder: 1,
+ },
+ {
+ i18n_key: "sidebar.cycles",
+ key: EProjectFeatureKey.CYCLES,
+ name: "Cycles",
+ href: `/${workspaceSlug}/projects/${projectId}/cycles`,
+ icon: ContrastIcon,
+ access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
+ shouldRender: project.cycle_view,
+ sortOrder: 2,
+ },
+ {
+ i18n_key: "sidebar.modules",
+ key: EProjectFeatureKey.MODULES,
+ name: "Modules",
+ href: `/${workspaceSlug}/projects/${projectId}/modules`,
+ icon: DiceIcon,
+ access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
+ shouldRender: project.module_view,
+ sortOrder: 3,
+ },
+ {
+ i18n_key: "sidebar.views",
+ key: EProjectFeatureKey.VIEWS,
+ name: "Views",
+ href: `/${workspaceSlug}/projects/${projectId}/views`,
+ icon: Layers,
+ access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
+ shouldRender: project.issue_views_view,
+ sortOrder: 4,
+ },
+ {
+ i18n_key: "sidebar.pages",
+ key: EProjectFeatureKey.PAGES,
+ name: "Pages",
+ href: `/${workspaceSlug}/projects/${projectId}/pages`,
+ icon: FileText,
+ access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
+ shouldRender: project.page_view,
+ sortOrder: 5,
+ },
+ {
+ i18n_key: "sidebar.intake",
+ key: EProjectFeatureKey.INTAKE,
+ name: "Intake",
+ href: `/${workspaceSlug}/projects/${projectId}/intake`,
+ icon: Intake,
+ access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
+ shouldRender: project.inbox_view,
+ sortOrder: 6,
+ },
+];
diff --git a/web/ce/components/projects/navigation/index.ts b/web/ce/components/projects/navigation/index.ts
new file mode 100644
index 00000000000..b9755e783ec
--- /dev/null
+++ b/web/ce/components/projects/navigation/index.ts
@@ -0,0 +1 @@
+export * from "./helper";
diff --git a/web/ce/components/projects/settings/intake/header.tsx b/web/ce/components/projects/settings/intake/header.tsx
index 32a93894f1e..9b0c994b514 100644
--- a/web/ce/components/projects/settings/intake/header.tsx
+++ b/web/ce/components/projects/settings/intake/header.tsx
@@ -5,16 +5,15 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { RefreshCcw } from "lucide-react";
// ui
-import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
+import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
-import { Breadcrumbs, Button, Intake, Header } from "@plane/ui";
+import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
-import { BreadcrumbLink } from "@/components/common";
import { InboxIssueCreateModalRoot } from "@/components/inbox";
// hooks
import { useProject, useProjectInbox, useUserPermissions } from "@/hooks/store";
// plane web
-import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
+import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs";
export const ProjectInboxHeader: FC = observer(() => {
// states
@@ -37,13 +36,13 @@ export const ProjectInboxHeader: FC = observer(() => {
return (
-
+
-
-
- } />}
+
diff --git a/web/core/components/common/breadcrumb-link.tsx b/web/core/components/common/breadcrumb-link.tsx
index bf421a50c2d..1dc85e3e2ae 100644
--- a/web/core/components/common/breadcrumb-link.tsx
+++ b/web/core/components/common/breadcrumb-link.tsx
@@ -1,44 +1,75 @@
"use client";
-import { ReactNode } from "react";
+import React, { ReactNode, useMemo, FC } from "react";
+import { observer } from "mobx-react-lite";
import Link from "next/link";
-import { Tooltip } from "@plane/ui";
+import { Breadcrumbs } from "@plane/ui";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
- label?: string | ReactNode;
+ label?: string;
href?: string;
- icon?: React.ReactNode | undefined;
+ icon?: React.ReactNode;
disableTooltip?: boolean;
+ isLast?: boolean;
};
-export const BreadcrumbLink: React.FC
= (props) => {
- const { href, label, icon, disableTooltip = false } = props;
- const { isMobile } = usePlatformOS();
+const IconWrapper = React.memo(({ icon }: { icon: React.ReactNode }) => (
+ {icon}
+));
+
+IconWrapper.displayName = "IconWrapper";
+
+const LabelWrapper = React.memo(({ label }: { label: ReactNode }) => (
+ {label}
+));
+
+LabelWrapper.displayName = "LabelWrapper";
+
+const BreadcrumbContent = React.memo(({ icon, label }: { icon?: React.ReactNode; label?: ReactNode }) => {
+ if (!icon && !label) return null;
+
return (
-
-
-
- {href ? (
-
- {icon && (
-
{icon}
- )}
- {label && (
-
{label}
- )}
-
- ) : (
-
- {icon &&
{icon}
}
-
{label}
-
- )}
-
-
-
+ <>
+ {icon && }
+ {label && }
+ >
);
-};
+});
+
+BreadcrumbContent.displayName = "BreadcrumbContent";
+
+const ItemWrapper = React.memo(({ children, ...props }: React.ComponentProps) => (
+ {children}
+));
+
+ItemWrapper.displayName = "ItemWrapper";
+
+export const BreadcrumbLink: FC = observer((props) => {
+ const { href, label, icon, disableTooltip = false, isLast = false } = props;
+ const { isMobile } = usePlatformOS();
+
+ const itemWrapperProps = useMemo(
+ () => ({
+ label: label?.toString(),
+ disableTooltip: isMobile || disableTooltip,
+ type: (href && href !== "" ? "link" : "text") as "link" | "text",
+ isLast,
+ }),
+ [href, label, isMobile, disableTooltip, isLast]
+ );
+
+ const content = useMemo(() => , [icon, label]);
+
+ if (href) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return {content};
+});
+
+BreadcrumbLink.displayName = "BreadcrumbLink";
diff --git a/web/core/components/common/switcher-label.tsx b/web/core/components/common/switcher-label.tsx
index 6bb4c346f1c..d1f1fc4bc83 100644
--- a/web/core/components/common/switcher-label.tsx
+++ b/web/core/components/common/switcher-label.tsx
@@ -2,6 +2,32 @@ import { FC } from "react";
import { TLogoProps } from "@plane/types";
import { ISvgIcons, Logo } from "@plane/ui";
import { getFileURL, truncateText } from "@plane/utils";
+
+type TSwitcherIconProps = {
+ logo_props?: TLogoProps;
+ logo_url?: string;
+ LabelIcon: FC;
+ size?: number;
+};
+
+export const SwitcherIcon: FC = ({ logo_props, logo_url, LabelIcon, size = 12 }) => {
+ if (logo_props?.in_use) {
+ return ;
+ }
+
+ if (logo_url) {
+ return (
+
+ );
+ }
+ return ;
+};
+
type TSwitcherLabelProps = {
logo_props?: TLogoProps;
logo_url?: string;
@@ -13,13 +39,7 @@ export const SwitcherLabel: FC = (props) => {
const { logo_props, name, LabelIcon, logo_url } = props;
return (
- {logo_props?.in_use ? (
-
- ) : logo_url ? (
-
})
- ) : (
-
- )}
+
{truncateText(name ?? "", 40)}
);
diff --git a/web/core/components/project/header.tsx b/web/core/components/project/header.tsx
index 17adc130ee4..e40280373c3 100644
--- a/web/core/components/project/header.tsx
+++ b/web/core/components/project/header.tsx
@@ -37,16 +37,15 @@ export const ProjectsBaseHeader = observer(() => {
- }
/>
}
/>
- {isArchived && } />}
+ {isArchived && } />}
diff --git a/web/core/components/workspace-notifications/sidebar/header/root.tsx b/web/core/components/workspace-notifications/sidebar/header/root.tsx
index cdda51fbfa6..7ed9ea5289d 100644
--- a/web/core/components/workspace-notifications/sidebar/header/root.tsx
+++ b/web/core/components/workspace-notifications/sidebar/header/root.tsx
@@ -26,9 +26,8 @@ export const NotificationSidebarHeader: FC = observe
- }
diff --git a/web/core/components/workspace/views/default-view-quick-action.tsx b/web/core/components/workspace/views/default-view-quick-action.tsx
index e7f0e276a3d..869c2d09656 100644
--- a/web/core/components/workspace/views/default-view-quick-action.tsx
+++ b/web/core/components/workspace/views/default-view-quick-action.tsx
@@ -1,19 +1,16 @@
"use client";
import { observer } from "mobx-react";
-import Link from "next/link";
import { ExternalLink, LinkIcon } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// ui
import { TStaticViewTypes } from "@plane/types";
-import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
+import { CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
import { copyUrlToClipboard, cn } from "@plane/utils";
// helpers
type Props = {
- parentRef: React.RefObject;
workspaceSlug: string;
- globalViewId: string | undefined;
view: {
key: TStaticViewTypes;
i18n_label: string;
@@ -21,7 +18,7 @@ type Props = {
};
export const DefaultWorkspaceViewQuickActions: React.FC = observer((props) => {
- const { parentRef, globalViewId, view, workspaceSlug } = props;
+ const { workspaceSlug, view } = props;
const { t } = useTranslation();
@@ -53,43 +50,11 @@ export const DefaultWorkspaceViewQuickActions: React.FC = observer((props
return (
<>
-
-
- {view.key === globalViewId ? (
-
- {t(view.i18n_label)}
-
- ) : (
-
-
- {t(view.i18n_label)}
-
-
- )}
- >
- }
+ ellipsis
placement="bottom-end"
- menuItemsClassName="z-20"
closeOnSelect
+ buttonClassName="flex-shrink-0 flex items-center justify-center size-[26px] bg-custom-background-80/70 rounded"
>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
diff --git a/web/core/components/workspace/views/header.tsx b/web/core/components/workspace/views/header.tsx
index f449c553b5e..e7312980062 100644
--- a/web/core/components/workspace/views/header.tsx
+++ b/web/core/components/workspace/views/header.tsx
@@ -37,13 +37,7 @@ const ViewTab = observer((props: { viewId: string }) => {
return (
-
+
);
});
@@ -63,12 +57,7 @@ const DefaultViewTab = (props: {
if (!workspaceSlug || !globalViewId) return null;
return (
-
+
);
};
diff --git a/web/core/components/workspace/views/quick-action.tsx b/web/core/components/workspace/views/quick-action.tsx
index e0950a78ac0..4db3d537cd3 100644
--- a/web/core/components/workspace/views/quick-action.tsx
+++ b/web/core/components/workspace/views/quick-action.tsx
@@ -2,13 +2,12 @@
import { useState } from "react";
import { observer } from "mobx-react";
-import Link from "next/link";
-import { ExternalLink, LinkIcon, Pencil, Trash2, Lock } from "lucide-react";
+import { ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
// types
-import { EViewAccess, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
+import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IWorkspaceView } from "@plane/types";
-import { ContextMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
+import { CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
import { copyUrlToClipboard, cn } from "@plane/utils";
// components
import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace";
@@ -18,15 +17,12 @@ import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/compone
import { useUser, useUserPermissions } from "@/hooks/store";
type Props = {
- parentRef: React.RefObject;
workspaceSlug: string;
- globalViewId: string;
- viewId: string;
view: IWorkspaceView;
};
export const WorkspaceViewQuickActions: React.FC = observer((props) => {
- const { parentRef, view, globalViewId, viewId, workspaceSlug } = props;
+ const { workspaceSlug, view } = props;
// states
const [updateViewModal, setUpdateViewModal] = useState(false);
const [deleteViewModal, setDeleteViewModal] = useState(false);
@@ -78,42 +74,53 @@ export const WorkspaceViewQuickActions: React.FC = observer((props) => {
},
];
- const isSelected = viewId === globalViewId;
- const isPrivateView = view.access === EViewAccess.PRIVATE;
-
- let customButton = (
-
-
- {view.name}
-
- {isPrivateView && (
-
- )}
-
- );
-
- if (!isSelected) {
- customButton = (
-
- {customButton}
-
- );
- }
-
return (
<>
setUpdateViewModal(false)} />
setDeleteViewModal(false)} />
-
-
- {customButton}
+
+ {MENU_ITEMS.map((item) => {
+ if (item.shouldRender === false) return null;
+ return (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ item.action();
+ }}
+ className={cn(
+ "flex items-center gap-2",
+ {
+ "text-custom-text-400": item.disabled,
+ },
+ item.className
+ )}
+ disabled={item.disabled}
+ >
+ {item.icon && }
+
+
{item.title}
+ {item.description && (
+
+ {item.description}
+
+ )}
+
+
+ );
+ })}
+
>
);
});