[WEB-3759]chore: header revamp (#2872)

* chore: updated header for customers

* chore: updated header for initiatives and dashboards

* chore: header revamp for cycles, modules, pages and views

* feat: search functionality for project level epics

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
This commit is contained in:
Vamsi Krishna
2025-04-09 15:23:59 +05:30
committed by GitHub
parent 9b05964f8a
commit fd6fe07ff4
10 changed files with 157 additions and 99 deletions

View File

@@ -198,10 +198,16 @@ class EpicViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@check_feature_flag(FeatureFlag.EPICS)
def list(self, request, slug, project_id):
search = request.GET.get("search", None)
filters = issue_filters(request.query_params, "GET")
epics = self.get_queryset().filter(**filters)
order_by_param = request.GET.get("order_by", "-created_at")
# Add search functionality
if search:
epics = epics.filter(Q(name__icontains=search))
# epics queryset
issue_queryset, order_by_param = order_issue_queryset(
issue_queryset=epics, order_by_param=order_by_param

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
@@ -6,37 +6,19 @@ import { Eye, LayoutGrid, Pencil, Plus } from "lucide-react";
// plane imports
import { EWidgetChartModels, EWidgetChartTypes } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs, Button, CustomMenu, getButtonStyling, Header, setToast, TOAST_TYPE } from "@plane/ui";
import { ICustomSearchSelectOption } from "@plane/types";
import { Breadcrumbs, Button, CustomSearchSelect, getButtonStyling, Header, setToast, TOAST_TYPE } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
// helpers
import { truncateText } from "@/helpers/string.helper";
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
// plane web components
import { DashboardQuickActions } from "@/plane-web/components/dashboards/quick-actions";
import { DashboardWidgetChartTypesDropdown } from "@/plane-web/components/dashboards/widgets/dropdown";
// plane web hooks
import { useDashboards } from "@/plane-web/hooks/store";
const DashboardDropdownOption: React.FC<{ dashboardId: string }> = ({ dashboardId }) => {
// router
const { workspaceSlug } = useParams();
// store hooks
const { getDashboardById } = useDashboards();
// derived values
const dashboard = getDashboardById(dashboardId);
if (!dashboard) return null;
return (
<CustomMenu.MenuItem key={dashboard.id}>
<Link href={`/${workspaceSlug}/dashboards/${dashboard.id}`} className="flex items-center gap-1.5">
<LayoutGrid className="flex-shrink-0 size-3" />
{truncateText(dashboard.name ?? "", 40)}
</Link>
</CustomMenu.MenuItem>
);
};
export const WorkspaceDashboardDetailsHeader = observer(() => {
// refs
const parentRef = useRef(null);
// states
const [isAddingWidget, setIsAddingWidget] = useState(false);
// navigation
@@ -75,6 +57,22 @@ export const WorkspaceDashboardDetailsHeader = observer(() => {
}
};
const switcherOptions = currentWorkspaceDashboardIds
.map((id) => {
const _dashboard = getDashboardById(id);
if (!_dashboard?.id || !_dashboard?.name) return null;
return {
value: _dashboard.id,
query: _dashboard.name,
content: (
<Link href={`/${workspaceSlug}/dashboards/${_dashboard.id}`}>
<SwitcherLabel name={_dashboard.name} LabelIcon={LayoutGrid} />
</Link>
),
};
})
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
return (
<Header>
<Header.LeftItem>
@@ -93,36 +91,19 @@ export const WorkspaceDashboardDetailsHeader = observer(() => {
<Breadcrumbs.BreadcrumbItem
type="component"
component={
<div className="flex items-center gap-2">
<CustomMenu
label={
<>
<LayoutGrid className="flex-shrink-0 size-3" />
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
<p className="truncate">{dashboardDetails?.name ?? ""}</p>
</div>
</>
}
className="ml-1.5 flex-shrink-0 truncate"
placement="bottom-start"
>
{currentWorkspaceDashboardIds?.map((dashboardId) => (
<DashboardDropdownOption key={dashboardId} dashboardId={dashboardId} />
))}
</CustomMenu>
{dashboardDetails && !isViewModeEnabled && (
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 rounded px-1 py-0.5 text-sm">
{t("dashboards.common.editing")}
</span>
)}
</div>
<CustomSearchSelect
label={<SwitcherLabel name={dashboardDetails?.name} LabelIcon={LayoutGrid} />}
value={dashboardId.toString()}
onChange={() => {}}
options={switcherOptions}
/>
}
/>
</Breadcrumbs>
</div>
</Header.LeftItem>
{dashboardDetails && (
<Header.RightItem>
<Header.RightItem className="items-center">
{!isViewModeEnabled && canCurrentUserCreateWidget && (
<DashboardWidgetChartTypesDropdown
buttonClassName={getButtonStyling("neutral-primary", "sm")}
@@ -145,6 +126,12 @@ export const WorkspaceDashboardDetailsHeader = observer(() => {
>
{t(isViewModeEnabled ? "common.edit" : "common.view")}
</Button>
<DashboardQuickActions
dashboardId={dashboardId.toString()}
parentRef={parentRef}
showEdit={false}
customClassName="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
/>
</Header.RightItem>
)}
</Header>

View File

@@ -1,13 +1,15 @@
"use client";
import { useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { Sidebar } from "lucide-react";
import { PanelRight } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// ui
import { Breadcrumbs, Header, InitiativeIcon } from "@plane/ui";
import { ICustomSearchSelectOption } from "@plane/types";
import { Breadcrumbs, CustomSearchSelect, Header, InitiativeIcon } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
@@ -25,7 +27,7 @@ export const InitiativesDetailsHeader = observer(() => {
const parentRef = useRef<HTMLDivElement>(null);
// store hooks
const {
initiative: { getInitiativeById },
initiative: { getInitiativeById, initiativeIds },
} = useInitiatives();
const { initiativesSidebarCollapsed, toggleInitiativesSidebar } = useAppTheme();
@@ -33,6 +35,22 @@ export const InitiativesDetailsHeader = observer(() => {
// derived values
const initiativesDetails = initiativeId ? getInitiativeById(initiativeId.toString()) : undefined;
const switcherOptions = initiativeIds
.map((id) => {
const _initiative = getInitiativeById(id);
if (!_initiative?.id || !_initiative?.name) return null;
return {
value: _initiative.id,
query: _initiative.name,
content: (
<Link href={`/${workspaceSlug}/initiatives/${_initiative.id}`}>
<SwitcherLabel name={_initiative.name} LabelIcon={InitiativeIcon} />
</Link>
),
};
})
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
return (
<Header>
<Header.LeftItem>
@@ -48,23 +66,39 @@ export const InitiativesDetailsHeader = observer(() => {
/>
}
/>
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={initiativesDetails?.name} />} />
<Breadcrumbs.BreadcrumbItem
type="component"
component={
<CustomSearchSelect
label={<SwitcherLabel name={initiativesDetails?.name} LabelIcon={InitiativeIcon} />}
value={initiativeId.toString()}
onChange={() => {}}
options={switcherOptions}
/>
}
/>
</Breadcrumbs>
</div>
</Header.LeftItem>
<Header.RightItem>
{initiativesDetails && (
<div ref={parentRef} className="flex items-center gap-2">
<button
type="button"
className="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
onClick={() => toggleInitiativesSidebar()}
>
<PanelRight
className={cn("size-4 cursor-pointer", {
"text-custom-primary-100": !initiativesSidebarCollapsed,
})}
/>
</button>
<InitiativeQuickActions
workspaceSlug={workspaceSlug.toString()}
parentRef={parentRef}
initiative={initiativesDetails}
/>
<Sidebar
className={cn("size-4 cursor-pointer", {
"text-custom-primary-100": !initiativesSidebarCollapsed,
})}
onClick={() => toggleInitiativesSidebar()}
customClassName="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
/>
</div>
)}

View File

@@ -11,7 +11,6 @@ import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui";
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
import { PageEditInformationPopover } from "@/components/pages";
// helpers
// hooks
import { useProject } from "@/hooks/store";
// plane web components
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
@@ -35,7 +34,7 @@ export const PageDetailsHeader = observer(() => {
const { getPageById, getCurrentProjectPageIds } = usePageStore(EPageStoreType.PROJECT);
// derived values
const projectPageIds = getCurrentProjectPageIds(projectId?.toString());
if (!page) return null;
const switcherOptions = projectPageIds
.map((id) => {

View File

@@ -17,10 +17,11 @@ type Props = {
customerId: string;
workspaceSlug: string;
parentRef: React.RefObject<HTMLDivElement> | null;
customClassName?: string;
};
export const CustomerQuickActions: React.FC<Props> = observer((props) => {
const { customerId, workspaceSlug, parentRef } = props;
const { customerId, workspaceSlug, parentRef, customClassName } = props;
// states
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
// i18n
@@ -82,7 +83,7 @@ export const CustomerQuickActions: React.FC<Props> = observer((props) => {
handleClose={() => setIsDeleteModalOpen(false)}
/>
{parentRef && <ContextMenu parentRef={parentRef} items={MENU_ITEMS} />}
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (

View File

@@ -1,13 +1,15 @@
"use client";
import React, { FC, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
// plane imports
import { Sidebar } from "lucide-react";
import { Breadcrumbs, CustomersIcon, Header } from "@plane/ui";
import { PanelRight } from "lucide-react";
import { ICustomSearchSelectOption } from "@plane/types";
import { Breadcrumbs, CustomersIcon, Header, CustomSearchSelect } from "@plane/ui";
// components
import { cn } from "@plane/utils";
import { BreadcrumbLink } from "@/components/common";
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
// hooks
import { useWorkspace } from "@/hooks/store";
// plane web imports
@@ -18,7 +20,7 @@ export const CustomerDetailHeader: FC = observer(() => {
const { workspaceSlug, customerId } = useParams();
// hooks
const { currentWorkspace } = useWorkspace();
const { getCustomerById } = useCustomers();
const { getCustomerById, customerIds } = useCustomers();
const { toggleCustomerDetailSidebar, customerDetailSidebarCollapsed } = useCustomers();
// derived values
const workspaceId = currentWorkspace?.id || undefined;
@@ -28,6 +30,22 @@ export const CustomerDetailHeader: FC = observer(() => {
const customer = getCustomerById(customerId.toString());
if (!workspaceSlug || !workspaceId) return <></>;
const switcherOptions = customerIds
.map((id) => {
const _customer = getCustomerById(id);
if (!_customer?.id || !_customer?.name) return null;
return {
value: _customer.id,
query: _customer.name,
content: (
<Link href={`/${workspaceSlug}/customers/${_customer.id}`}>
<SwitcherLabel logo_url={_customer.logo_url} name={_customer.name} LabelIcon={CustomersIcon} />
</Link>
),
};
})
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
return (
<>
<Header>
@@ -45,23 +63,39 @@ export const CustomerDetailHeader: FC = observer(() => {
/>
}
/>
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={customer?.name} />} />
<Breadcrumbs.BreadcrumbItem
type="component"
component={
<CustomSearchSelect
label={
<SwitcherLabel logo_url={customer?.logo_url} name={customer?.name} LabelIcon={CustomersIcon} />
}
value={customerId.toString()}
onChange={() => {}}
options={switcherOptions}
/>
}
/>
</Breadcrumbs>
</div>
</Header.LeftItem>
<Header.RightItem>
{customer && (
<div ref={parentRef} className="flex gap-2 items-center">
<button
type="button"
className="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
onClick={() => toggleCustomerDetailSidebar()}
>
<PanelRight
className={cn("h-4 w-4", !customerDetailSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")}
/>
</button>
<CustomerQuickActions
customerId={customerId.toString()}
workspaceSlug={workspaceSlug.toString()}
parentRef={parentRef}
/>
<Sidebar
className={cn("size-4 cursor-pointer", {
"text-custom-primary-100": !customerDetailSidebarCollapsed,
})}
onClick={() => toggleCustomerDetailSidebar()}
customClassName="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70"
/>
</div>
)}

View File

@@ -2,7 +2,7 @@ import React, { FC } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { CustomersIcon, CustomSearchSelect } from "@plane/ui";
import { getFileURL } from "@plane/utils";
import { SwitcherLabel } from "@/components/common";
import { useCustomers } from "@/plane-web/hooks/store";
type TProps = {
@@ -43,24 +43,7 @@ export const CustomerDropDown: FC<TProps> = observer((props) => {
return {
value: customer?.id,
query: `${customer?.name}`,
content: (
<div className="flex items-center gap-2">
<div className="p-1">
{customer?.logo_url ? (
<img
src={getFileURL(customer.logo_url)}
alt="customer-logo"
className="rounded-sm w-3 h-3 object-cover"
/>
) : (
<div className="bg-custom-background-90 rounded-md flex items-center justify-center h-3 w-3">
<CustomersIcon className="size-4 opacity-50" />
</div>
)}
</div>
<p className="text-sm">{customer?.name}</p>
</div>
),
content: <SwitcherLabel logo_url={customer?.logo_url} name={customer?.name} LabelIcon={CustomersIcon} />,
};
});

View File

@@ -13,10 +13,12 @@ import { DashboardDeleteModal } from "./modals/delete-modal";
type Props = {
dashboardId: string;
parentRef: React.RefObject<HTMLElement>;
showEdit?: boolean;
customClassName?: string;
};
export const DashboardQuickActions: React.FC<Props> = observer((props) => {
const { dashboardId, parentRef } = props;
const { dashboardId, parentRef, showEdit = true, customClassName } = props;
// states
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
// store hooks
@@ -41,7 +43,7 @@ export const DashboardQuickActions: React.FC<Props> = observer((props) => {
},
title: t("common.actions.edit"),
icon: Pencil,
shouldRender: !!canCurrentUserEditDashboard,
shouldRender: !!canCurrentUserEditDashboard && showEdit,
},
{
key: "open-in-new-tab",
@@ -92,7 +94,13 @@ export const DashboardQuickActions: React.FC<Props> = observer((props) => {
isOpen={isDeleteModalOpen}
/>
{parentRef && <ContextMenu parentRef={parentRef} items={MENU_ITEMS} />}
<CustomMenu placement="bottom-end" optionsClassName="max-h-[90vh]" ellipsis closeOnSelect>
<CustomMenu
placement="bottom-end"
optionsClassName="max-h-[90vh]"
buttonClassName={customClassName}
ellipsis
closeOnSelect
>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (

View File

@@ -20,10 +20,11 @@ type Props = {
initiative: TInitiative;
workspaceSlug: string;
disabled?: boolean;
customClassName?: string;
};
export const InitiativeQuickActions: React.FC<Props> = observer((props) => {
const { parentRef, initiative, workspaceSlug, disabled = false } = props;
const { parentRef, initiative, workspaceSlug, disabled = false, customClassName } = props;
// states
const [updateModal, setUpdateModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
@@ -101,7 +102,7 @@ export const InitiativeQuickActions: React.FC<Props> = observer((props) => {
</div>
)}
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (

View File

@@ -30,6 +30,7 @@ type InitiativeCollapsible = "links" | "attachments" | "projects" | "epics";
export interface IInitiativeStore {
initiativesMap: Record<string, TInitiative> | undefined;
initiativeIds: string[];
initiativesStatsMap: Record<string, TInitiativeStats> | undefined;
initiativeLinks: IInitiativeLinkStore;
initiativeCommentActivities: IInitiativeCommentActivityStore;
@@ -156,6 +157,10 @@ export class InitiativeStore implements IInitiativeStore {
return this.getGroupedInitiativeIds(workspaceSlug);
}
get initiativeIds() {
return Object.keys(this.initiativesMap ?? {});
}
getGroupedInitiativeIds = computedFn((workspaceSlug: string) => {
const workspace = this.rootStore.workspaceRoot.getWorkspaceBySlug(workspaceSlug);
if (!workspace) return;