From fd6fe07ff401f73fa3581f73b91ca8e73e79ff9e Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:23:59 +0530 Subject: [PATCH] [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 --- apiserver/plane/ee/views/app/epic/base.py | 6 ++ .../(detail)/[dashboardId]/header.tsx | 85 ++++++++----------- .../(detail)/[initiativeId]/header.tsx | 56 +++++++++--- .../[projectId]/pages/(detail)/header.tsx | 3 +- .../customers/actions/quick-actions.tsx | 5 +- web/ee/components/customers/detail/header.tsx | 56 +++++++++--- .../customers/requests/customer-dropdown.tsx | 21 +---- .../components/dashboards/quick-actions.tsx | 14 ++- .../initiatives/components/quick-actions.tsx | 5 +- web/ee/store/initiatives/initiatives.store.ts | 5 ++ 10 files changed, 157 insertions(+), 99 deletions(-) diff --git a/apiserver/plane/ee/views/app/epic/base.py b/apiserver/plane/ee/views/app/epic/base.py index ba1141b85e..00102a347a 100644 --- a/apiserver/plane/ee/views/app/epic/base.py +++ b/apiserver/plane/ee/views/app/epic/base.py @@ -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 diff --git a/web/app/[workspaceSlug]/(projects)/dashboards/(detail)/[dashboardId]/header.tsx b/web/app/[workspaceSlug]/(projects)/dashboards/(detail)/[dashboardId]/header.tsx index b16e8efdd4..0afd35bf32 100644 --- a/web/app/[workspaceSlug]/(projects)/dashboards/(detail)/[dashboardId]/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/dashboards/(detail)/[dashboardId]/header.tsx @@ -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 ( - - - - {truncateText(dashboard.name ?? "", 40)} - - - ); -}; - 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: ( + + + + ), + }; + }) + .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; + return (
@@ -93,36 +91,19 @@ export const WorkspaceDashboardDetailsHeader = observer(() => { - - -
-

{dashboardDetails?.name ?? ""}

-
- - } - className="ml-1.5 flex-shrink-0 truncate" - placement="bottom-start" - > - {currentWorkspaceDashboardIds?.map((dashboardId) => ( - - ))} -
- {dashboardDetails && !isViewModeEnabled && ( - - {t("dashboards.common.editing")} - - )} - + } + value={dashboardId.toString()} + onChange={() => {}} + options={switcherOptions} + /> } />
{dashboardDetails && ( - + {!isViewModeEnabled && canCurrentUserCreateWidget && ( { > {t(isViewModeEnabled ? "common.edit" : "common.view")} + )}
diff --git a/web/app/[workspaceSlug]/(projects)/initiatives/(detail)/[initiativeId]/header.tsx b/web/app/[workspaceSlug]/(projects)/initiatives/(detail)/[initiativeId]/header.tsx index 70d52f6f69..cca3040b23 100644 --- a/web/app/[workspaceSlug]/(projects)/initiatives/(detail)/[initiativeId]/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/initiatives/(detail)/[initiativeId]/header.tsx @@ -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(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: ( + + + + ), + }; + }) + .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; + return (
@@ -48,23 +66,39 @@ export const InitiativesDetailsHeader = observer(() => { /> } /> - } /> + } + value={initiativeId.toString()} + onChange={() => {}} + options={switcherOptions} + /> + } + /> {initiativesDetails && (
+ - toggleInitiativesSidebar()} + customClassName="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70" />
)} diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx index 2062aa32df..18e9504a14 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx @@ -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) => { diff --git a/web/ee/components/customers/actions/quick-actions.tsx b/web/ee/components/customers/actions/quick-actions.tsx index af77a90f92..ab3e563b06 100644 --- a/web/ee/components/customers/actions/quick-actions.tsx +++ b/web/ee/components/customers/actions/quick-actions.tsx @@ -17,10 +17,11 @@ type Props = { customerId: string; workspaceSlug: string; parentRef: React.RefObject | null; + customClassName?: string; }; export const CustomerQuickActions: React.FC = 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 = observer((props) => { handleClose={() => setIsDeleteModalOpen(false)} /> {parentRef && } - + {MENU_ITEMS.map((item) => { if (item.shouldRender === false) return null; return ( diff --git a/web/ee/components/customers/detail/header.tsx b/web/ee/components/customers/detail/header.tsx index 36d085ef0c..89c43205b2 100644 --- a/web/ee/components/customers/detail/header.tsx +++ b/web/ee/components/customers/detail/header.tsx @@ -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: ( + + + + ), + }; + }) + .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; + return ( <>
@@ -45,23 +63,39 @@ export const CustomerDetailHeader: FC = observer(() => { /> } /> - } /> + + } + value={customerId.toString()} + onChange={() => {}} + options={switcherOptions} + /> + } + /> {customer && (
+ - toggleCustomerDetailSidebar()} + customClassName="p-1 rounded outline-none hover:bg-custom-sidebar-background-80 bg-custom-background-80/70" />
)} diff --git a/web/ee/components/customers/requests/customer-dropdown.tsx b/web/ee/components/customers/requests/customer-dropdown.tsx index 2d7fa94de4..f1154911f1 100644 --- a/web/ee/components/customers/requests/customer-dropdown.tsx +++ b/web/ee/components/customers/requests/customer-dropdown.tsx @@ -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 = observer((props) => { return { value: customer?.id, query: `${customer?.name}`, - content: ( -
-
- {customer?.logo_url ? ( - customer-logo - ) : ( -
- -
- )} -
-

{customer?.name}

-
- ), + content: , }; }); diff --git a/web/ee/components/dashboards/quick-actions.tsx b/web/ee/components/dashboards/quick-actions.tsx index 0b16ee9396..c69915a012 100644 --- a/web/ee/components/dashboards/quick-actions.tsx +++ b/web/ee/components/dashboards/quick-actions.tsx @@ -13,10 +13,12 @@ import { DashboardDeleteModal } from "./modals/delete-modal"; type Props = { dashboardId: string; parentRef: React.RefObject; + showEdit?: boolean; + customClassName?: string; }; export const DashboardQuickActions: React.FC = 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 = 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 = observer((props) => { isOpen={isDeleteModalOpen} /> {parentRef && } - + {MENU_ITEMS.map((item) => { if (item.shouldRender === false) return null; return ( diff --git a/web/ee/components/initiatives/components/quick-actions.tsx b/web/ee/components/initiatives/components/quick-actions.tsx index 1f7224ea85..46084b6151 100644 --- a/web/ee/components/initiatives/components/quick-actions.tsx +++ b/web/ee/components/initiatives/components/quick-actions.tsx @@ -20,10 +20,11 @@ type Props = { initiative: TInitiative; workspaceSlug: string; disabled?: boolean; + customClassName?: string; }; export const InitiativeQuickActions: React.FC = 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 = observer((props) => { )} - + {MENU_ITEMS.map((item) => { if (item.shouldRender === false) return null; return ( diff --git a/web/ee/store/initiatives/initiatives.store.ts b/web/ee/store/initiatives/initiatives.store.ts index cc8997457b..69e2c124ab 100644 --- a/web/ee/store/initiatives/initiatives.store.ts +++ b/web/ee/store/initiatives/initiatives.store.ts @@ -30,6 +30,7 @@ type InitiativeCollapsible = "links" | "attachments" | "projects" | "epics"; export interface IInitiativeStore { initiativesMap: Record | undefined; + initiativeIds: string[]; initiativesStatsMap: Record | 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;