From ad8d36757a074ff37441f8161640e2718d79beae Mon Sep 17 00:00:00 2001 From: vamsikrishnamathala Date: Mon, 7 Apr 2025 09:14:29 +0530 Subject: [PATCH 1/2] chore: header revamp for cycles, modules, pages and views --- packages/types/src/common.d.ts | 8 ++ packages/ui/src/dropdowns/helper.tsx | 11 +- .../[projectId]/cycles/(detail)/header.tsx | 99 ++++++++----- .../[projectId]/modules/(detail)/header.tsx | 76 ++++++---- .../[projectId]/pages/(detail)/header.tsx | 133 ++++++------------ .../views/(detail)/[viewId]/header.tsx | 91 ++++++------ web/core/components/common/index.ts | 1 + web/core/components/common/switcher-label.tsx | 27 ++++ .../analytics-sidebar/sidebar-header.tsx | 94 ------------- web/core/components/cycles/quick-actions.tsx | 5 +- .../modules/analytics-sidebar/root.tsx | 128 +---------------- web/core/components/modules/quick-actions.tsx | 5 +- web/core/components/pages/list/root.tsx | 4 +- .../pages/pages-list-main-content.tsx | 6 +- web/core/components/views/quick-actions.tsx | 5 +- web/core/store/pages/project-page.store.ts | 25 +++- 16 files changed, 265 insertions(+), 453 deletions(-) create mode 100644 web/core/components/common/switcher-label.tsx diff --git a/packages/types/src/common.d.ts b/packages/types/src/common.d.ts index c45236a9fa8..b35e408d6e2 100644 --- a/packages/types/src/common.d.ts +++ b/packages/types/src/common.d.ts @@ -26,3 +26,11 @@ export type TLogoProps = { export type TNameDescriptionLoader = "submitting" | "submitted" | "saved"; export type TFetchStatus = "partial" | "complete" | undefined; + +export type ICustomSearchSelectOption = { + value: any; + query: string; + content: React.ReactNode; + disabled?: boolean; + tooltip?: string | React.ReactNode; +}; diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx index 4ce12d9f8ca..0e758705110 100644 --- a/packages/ui/src/dropdowns/helper.tsx +++ b/packages/ui/src/dropdowns/helper.tsx @@ -1,5 +1,6 @@ // FIXME: fix this!!! import { Placement } from "@blueprintjs/popover2"; +import { ICustomSearchSelectOption } from "@plane/types"; export interface IDropdownProps { customButtonClassName?: string; @@ -44,15 +45,7 @@ interface CustomSearchSelectProps { onChange: any; onClose?: () => void; noResultsMessage?: string; - options: - | { - value: any; - query: string; - content: React.ReactNode; - disabled?: boolean; - tooltip?: string | React.ReactNode; - }[] - | undefined; + options?: ICustomSearchSelectOption[]; } interface SingleValueProps { diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx index 106dfa4d2aa..21a00ce3d27 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useState } from "react"; +import { useCallback, useRef, useState } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; import { useParams } from "next/navigation"; @@ -18,12 +18,18 @@ import { // i18n import { useTranslation } from "@plane/i18n"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; +import { + ICustomSearchSelectOption, + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, +} from "@plane/types"; // ui -import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header } from "@plane/ui"; +import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip, Header, CustomSearchSelect } from "@plane/ui"; // components import { ProjectAnalyticsModal } from "@/components/analytics"; -import { BreadcrumbLink } from "@/components/common"; +import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; +import { CycleQuickActions } from "@/components/cycles"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; @@ -69,6 +75,8 @@ const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { }; export const CycleIssuesHeader: React.FC = observer(() => { + // refs + const parentRef = useRef(null); // states const [analyticsModal, setAnalyticsModal] = useState(false); // router @@ -159,6 +167,25 @@ export const CycleIssuesHeader: React.FC = observer(() => { EUserPermissionsLevel.PROJECT ); + const switcherOptions = currentProjectCycleIds + ?.map((id) => { + const _cycle = id === cycleId ? cycleDetails : getCycleById(id); + if (!_cycle) return; + const cycleLink = `/${workspaceSlug}/projects/${projectId}/cycles/${_cycle.id}`; + return { + value: _cycle.id, + query: _cycle.name, + content: ( + + + + ), + }; + }) + .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; + + const workItemsCount = getGroupIssueCount(undefined, undefined, false); + const issuesCount = getGroupIssueCount(undefined, undefined, false); return ( @@ -201,33 +228,29 @@ export const CycleIssuesHeader: React.FC = observer(() => { {}} label={ - <> - -
-

{cycleDetails?.name && cycleDetails.name}

- {issuesCount && issuesCount > 0 ? ( - 1 ? "work items" : "work item" - } in this cycle`} - position="bottom" - > - - {issuesCount} - - - ) : null} -
- +
+ + {workItemsCount && workItemsCount > 0 ? ( + 1 ? "work items" : "work item" + } in this cycle`} + position="bottom" + > + + {workItemsCount} + + + ) : null} +
} - className="ml-1.5 flex-shrink-0 truncate" - placement="bottom-start" - > - {currentProjectCycleIds?.map((cycleId) => )} - + /> } /> @@ -302,19 +325,19 @@ export const CycleIssuesHeader: React.FC = observer(() => { )} + - diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx index 3c5eb7cc729..3c453fd7601 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx @@ -16,12 +16,17 @@ import { EUserPermissionsLevel, } from "@plane/constants"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; +import { + ICustomSearchSelectOption, + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, +} from "@plane/types"; // ui -import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header } from "@plane/ui"; +import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui"; // components import { ProjectAnalyticsModal } from "@/components/analytics"; -import { BreadcrumbLink } from "@/components/common"; +import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; @@ -155,7 +160,24 @@ export const ModuleIssuesHeader: React.FC = observer(() => { EUserPermissionsLevel.PROJECT ); - const issuesCount = getGroupIssueCount(undefined, undefined, false); + const workItemsCount = getGroupIssueCount(undefined, undefined, false); + + const switcherOptions = projectModuleIds + ?.map((id) => { + const _module = id === moduleId ? moduleDetails : getModuleById(id); + if (!_module) return; + const moduleLink = `/${workspaceSlug}/projects/${projectId}/modules/${_module.id}`; + return { + value: _module.id, + query: _module.name, + content: ( + + + + ), + }; + }) + .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; return ( <> @@ -196,33 +218,29 @@ export const ModuleIssuesHeader: React.FC = observer(() => { - -
-

{moduleDetails?.name && moduleDetails.name}

- {issuesCount && issuesCount > 0 ? ( - 1 ? "work items" : "work item" - } in this module`} - position="bottom" - > - - {issuesCount} - - - ) : null} -
- +
+ + {workItemsCount && workItemsCount > 0 ? ( + 1 ? "work items" : "work item" + } in this module`} + position="bottom" + > + + {workItemsCount} + + + ) : null} +
} - className="ml-1.5 flex-shrink-0" - placement="bottom-start" - > - {projectModuleIds?.map((moduleId) => )} - + value={moduleId} + onChange={() => {}} + /> } /> 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 f8292f44261..6ca74df9125 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 @@ -1,27 +1,24 @@ "use client"; - -import { useState } from "react"; import { observer } from "mobx-react"; +import Link from "next/link"; import { useParams } from "next/navigation"; -import { FileText } from "lucide-react"; +import useSWR from "swr"; +import { Archive, Earth, FileText, Layers, Lock } from "lucide-react"; // types -import { TLogoProps } from "@plane/types"; +import { ICustomSearchSelectOption } from "@plane/types"; // ui -import { Breadcrumbs, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, Tooltip, setToast, Header } from "@plane/ui"; +import { Breadcrumbs, Header, CustomSearchSelect, Tooltip } from "@plane/ui"; // components -import { BreadcrumbLink, Logo } from "@/components/common"; +import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { PageEditInformationPopover } from "@/components/pages"; // helpers -import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; -import { getPageName } from "@/helpers/page.helper"; // hooks import { useProject } from "@/hooks/store"; -import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages"; // plane web hooks -import { EPageStoreType, usePage } from "@/plane-web/hooks/store"; +import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store"; export interface IPagesHeaderProps { showButton?: boolean; @@ -29,42 +26,42 @@ export interface IPagesHeaderProps { export const PageDetailsHeader = observer(() => { // router - const { workspaceSlug, pageId } = useParams(); - // state - const [isOpen, setIsOpen] = useState(false); + const { workspaceSlug, pageId, projectId } = useParams(); // store hooks const { currentProjectDetails, loader } = useProject(); const page = usePage({ pageId: pageId?.toString() ?? "", storeType: EPageStoreType.PROJECT, }); - if (!page) return null; + const { getPageById, getCurrentProjectPageIds, fetchPagesList } = usePageStore(EPageStoreType.PROJECT); + // derived values // derived values - const { name, logo_props, updatePageLogo, isContentEditable } = page; - // use platform - const { isMobile } = usePlatformOS(); + const projectPageIds = getCurrentProjectPageIds(projectId?.toString()); - const handlePageLogoUpdate = async (data: TLogoProps) => { - if (data) { - updatePageLogo(data) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Logo Updated successfully.", - }); - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Something went wrong. Please try again.", - }); - }); - } - }; + // fetching pages list + useSWR( + workspaceSlug && projectId ? `PROJECT_PAGES_${projectId}` : null, + workspaceSlug && projectId ? () => fetchPagesList(workspaceSlug.toString(), projectId.toString()) : null + ); + if (!page) return null; + const switcherOptions = projectPageIds + .map((id) => { + const _page = id === pageId ? page : getPageById(id); + if (!_page) return; + const pageLink = `/${workspaceSlug}/projects/${projectId}/pages/${_page.id}`; + return { + value: _page.id, + query: _page.name, + content: ( + + + + ), + }; + }) + .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; - const pageTitle = getPageName(name); + if (!page) return null; return (
@@ -99,60 +96,14 @@ export const PageDetailsHeader = observer(() => { } /> -
-
-
- setIsOpen(val)} - className="flex items-center justify-center" - buttonClassName="flex items-center justify-center" - label={ - <> - {logo_props?.in_use ? ( - - ) : ( - - )} - - } - onChange={(val) => { - let logoValue = {}; - - if (val?.type === "emoji") - logoValue = { - value: convertHexEmojiToDecimal(val.value.unified), - url: val.value.imageUrl, - }; - else if (val?.type === "icon") logoValue = val.value; - - handlePageLogoUpdate({ - in_use: val?.type, - [val?.type]: logoValue, - }).finally(() => setIsOpen(false)); - }} - defaultIconColor={ - logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined - } - defaultOpen={ - logo_props?.in_use && logo_props?.in_use === "emoji" - ? EmojiIconPickerTypes.EMOJI - : EmojiIconPickerTypes.ICON - } - disabled={!isContentEditable} - /> -
- -
- {pageTitle} -
-
-
-
- + type="component" + component={ + } + onChange={() => {}} + /> } /> diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx index cc9a8fff6eb..4875c05487f 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx @@ -16,17 +16,21 @@ import { EUserPermissionsLevel, } from "@plane/constants"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; +import { + ICustomSearchSelectOption, + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, +} from "@plane/types"; // ui -import { Breadcrumbs, Button, CustomMenu, Tooltip, Header } from "@plane/ui"; +import { Breadcrumbs, Button, Tooltip, Header, CustomSearchSelect } from "@plane/ui"; // components -import { BreadcrumbLink, Logo } from "@/components/common"; +import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; // constants import { ViewQuickActions } from "@/components/views"; // helpers import { isIssueFilterActive } from "@/helpers/filter.helper"; -import { truncateText } from "@/helpers/string.helper"; // hooks import { useCommandPalette, @@ -143,6 +147,23 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { if (!viewDetails) return; + const switcherOptions = projectViewIds + ?.map((id) => { + const _view = id === viewId ? viewDetails : getViewById(id); + if (!_view) return; + const viewLink = `/${workspaceSlug}/projects/${projectId}/views/${_view.id}`; + return { + value: _view.id, + query: _view.name, + content: ( + + + + ), + }; + }) + .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; + return (
@@ -161,42 +182,12 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { - {viewDetails?.logo_props?.in_use ? ( - - ) : ( - - )} - {viewDetails?.name && truncateText(viewDetails.name, 40)} - - } - className="ml-1.5" - placement="bottom-start" - > - {projectViewIds?.map((viewId) => { - const view = getViewById(viewId); - - if (!view) return; - - return ( - - - {view?.logo_props?.in_use ? ( - - ) : ( - - )} - {truncateText(view.name, 40)} - - - ); - })} - + } + onChange={() => {}} + /> } /> @@ -210,17 +201,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { ) : ( <> )} - -
- -
- + {!viewDetails?.is_locked ? ( <> { ) : ( <> )} +
+ +
); diff --git a/web/core/components/common/index.ts b/web/core/components/common/index.ts index 1f9a4244051..6c0aa1bf754 100644 --- a/web/core/components/common/index.ts +++ b/web/core/components/common/index.ts @@ -6,3 +6,4 @@ export * from "./logo"; export * from "./pro-icon"; export * from "./count-chip"; export * from "./activity"; +export * from "./switcher-label"; diff --git a/web/core/components/common/switcher-label.tsx b/web/core/components/common/switcher-label.tsx new file mode 100644 index 00000000000..e1426461c98 --- /dev/null +++ b/web/core/components/common/switcher-label.tsx @@ -0,0 +1,27 @@ +import { FC } from "react"; +import { TLogoProps } from "@plane/types"; +import { ISvgIcons, Logo } from "@plane/ui"; +import { getFileURL } from "@plane/utils"; +import { truncateText } from "@/helpers/string.helper"; +type TSwitcherLabelProps = { + logo_props?: TLogoProps; + logo_url?: string; + name?: string; + LabelIcon: FC; +}; + +export const SwitcherLabel: FC = (props) => { + const { logo_props, name, LabelIcon, logo_url } = props; + return ( +
+ {logo_props?.in_use ? ( + + ) : logo_url ? ( + logo + ) : ( + + )} + {truncateText(name ?? "", 40)} +
+ ); +}; diff --git a/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx b/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx index 462ad21233e..1f91d441fef 100644 --- a/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx +++ b/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx @@ -76,44 +76,6 @@ export const CycleSidebarHeader: FC = observer((props) => { const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); - const handleRestoreCycle = async () => { - if (!workspaceSlug || !projectId) return; - - await restoreCycle(workspaceSlug.toString(), projectId.toString(), cycleDetails.id) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: t("project_cycles.action.restore.success.title"), - message: t("project_cycles.action.restore.success.description"), - }); - router.push(`/${workspaceSlug.toString()}/projects/${projectId.toString()}/archives/cycles`); - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: t("project_cycles.action.restore.failed.title"), - message: t("project_cycles.action.restore.failed.description"), - }) - ); - }; - - const handleCopyText = () => { - copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: t("common.link_copied"), - message: t("common.link_copied_to_clipboard"), - }); - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: t("common.errors.default.message"), - }); - }); - }; - const submitChanges = async (data: Partial, changedProperty: string) => { if (!workspaceSlug || !projectId || !cycleDetails.id) return; @@ -225,62 +187,6 @@ export const CycleSidebarHeader: FC = observer((props) => { -
- {!isArchived && ( - - )} - {isEditingAllowed && ( - } - > - {!isArchived && ( - setArchiveCycleModal(true)} disabled={!isCompleted}> - {isCompleted ? ( -
- - {t("common.archive")} -
- ) : ( -
- -
-

{t("common.archive")}

-

- {t("project_cycles.only_completed_cycles_can_be_archived")} -

-
-
- )} -
- )} - {isArchived && ( - - - - {t("project_cycles.action.restore.title")} - - - )} - {!isCompleted && ( - { - setTrackElement("CYCLE_PAGE_SIDEBAR"); - setCycleDeleteModal(true); - }} - > - - - {t("delete")} - - - )} -
- )} -
diff --git a/web/core/components/cycles/quick-actions.tsx b/web/core/components/cycles/quick-actions.tsx index ff69d447dd8..9ab970d613e 100644 --- a/web/core/components/cycles/quick-actions.tsx +++ b/web/core/components/cycles/quick-actions.tsx @@ -24,10 +24,11 @@ type Props = { cycleId: string; projectId: string; workspaceSlug: string; + customClassName?: string; }; export const CycleQuickActions: React.FC = observer((props) => { - const { parentRef, cycleId, projectId, workspaceSlug } = props; + const { parentRef, cycleId, projectId, workspaceSlug, customClassName } = props; // router const router = useAppRouter(); // states @@ -188,7 +189,7 @@ export const CycleQuickActions: React.FC = observer((props) => {
)} - + {MENU_ITEMS.map((item) => { if (item.shouldRender === false) return null; return ( diff --git a/web/core/components/modules/analytics-sidebar/root.tsx b/web/core/components/modules/analytics-sidebar/root.tsx index cfa464d6316..76d86d81d2b 100644 --- a/web/core/components/modules/analytics-sidebar/root.tsx +++ b/web/core/components/modules/analytics-sidebar/root.tsx @@ -4,18 +4,7 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Controller, useForm } from "react-hook-form"; -import { - ArchiveRestoreIcon, - CalendarClock, - ChevronDown, - ChevronRight, - Info, - LinkIcon, - Plus, - SquareUser, - Trash2, - Users, -} from "lucide-react"; +import { CalendarClock, ChevronDown, ChevronRight, Info, Plus, SquareUser, Users } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; // plane types import { @@ -30,17 +19,7 @@ import { import { useTranslation } from "@plane/i18n"; import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; // plane ui -import { - CustomMenu, - Loader, - LayersIcon, - CustomSelect, - ModuleStatusIcon, - TOAST_TYPE, - setToast, - ArchiveIcon, - TextArea, -} from "@plane/ui"; +import { Loader, LayersIcon, CustomSelect, ModuleStatusIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui"; // components import { DateRangeDropdown, MemberDropdown } from "@/components/dropdowns"; import { @@ -53,10 +32,8 @@ import { // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks import { useModule, useEventTracker, useProjectEstimates, useUserPermissions } from "@/hooks/store"; -import { useAppRouter } from "@/hooks/use-app-router"; // plane web constants import { EEstimateSystem } from "@/plane-web/constants/estimates"; @@ -83,23 +60,18 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { const [moduleLinkModal, setModuleLinkModal] = useState(false); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); // router - const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); // store hooks const { t } = useTranslation(); const { allowPermissions } = useUserPermissions(); - const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink, restoreModule } = - useModule(); - const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker(); + const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule(); + const { captureModuleEvent, captureEvent } = useEventTracker(); const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); // derived values const moduleDetails = getModuleById(moduleId); - const moduleState = moduleDetails?.status?.toLocaleLowerCase(); - const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState); - const areEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId.toString()); const estimateType = areEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId); const isEstimatePointValid = estimateType && estimateType?.type == EEstimateSystem.POINTS ? true : false; @@ -176,24 +148,6 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { }); }; - const handleCopyText = () => { - copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link copied", - message: "Module link copied to clipboard", - }); - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Some error occurred", - }); - }); - }; - const handleDateChange = async (startDate: Date | undefined, targetDate: Date | undefined) => { submitChanges({ start_date: startDate ? renderFormattedPayloadDate(startDate) : null, @@ -206,30 +160,6 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { }); }; - const handleRestoreModule = async (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - if (!workspaceSlug || !projectId || !moduleId) return; - - await restoreModule(workspaceSlug.toString(), projectId.toString(), moduleId) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Restore success", - message: "Your module can be found in project modules.", - }); - router.push(`/${workspaceSlug}/projects/${projectId}/archives/modules`); - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Module could not be restored. Please try again.", - }) - ); - }; - useEffect(() => { if (moduleDetails) reset({ @@ -310,56 +240,6 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => {
-
- {!isArchived && ( - - )} - {isEditingAllowed && ( - - {!isArchived && ( - setArchiveModuleModal(true)} disabled={!isInArchivableGroup}> - {isInArchivableGroup ? ( -
- - {t("project_module.archive_module")} -
- ) : ( -
- -
-

Archive module

-

- {t("project_module.quick_actions.archive_module_description")} -

-
-
- )} -
- )} - {isArchived && ( - - - - {t("project_module.restore_module")} - - - )} - { - setTrackElement("Module peek-overview"); - setModuleDeleteModal(true); - }} - > - - - {t("project_module.delete_module")} - - -
- )} -
diff --git a/web/core/components/modules/quick-actions.tsx b/web/core/components/modules/quick-actions.tsx index 69b6e0eb447..e90d5bbe210 100644 --- a/web/core/components/modules/quick-actions.tsx +++ b/web/core/components/modules/quick-actions.tsx @@ -24,10 +24,11 @@ type Props = { moduleId: string; projectId: string; workspaceSlug: string; + customClassName?: string; }; export const ModuleQuickActions: React.FC = observer((props) => { - const { parentRef, moduleId, projectId, workspaceSlug } = props; + const { parentRef, moduleId, projectId, workspaceSlug, customClassName } = props; // router const router = useAppRouter(); // states @@ -167,7 +168,7 @@ export const ModuleQuickActions: React.FC = observer((props) => {
)} - + {MENU_ITEMS.map((item) => { if (item.shouldRender === false) return null; return ( diff --git a/web/core/components/pages/list/root.tsx b/web/core/components/pages/list/root.tsx index db8196b29c3..554e48855dc 100644 --- a/web/core/components/pages/list/root.tsx +++ b/web/core/components/pages/list/root.tsx @@ -17,9 +17,9 @@ type TPagesListRoot = { export const PagesListRoot: FC = observer((props) => { const { pageType, storeType } = props; // store hooks - const { getCurrentProjectFilteredPageIds } = usePageStore(storeType); + const { getCurrentProjectFilteredPageIdsByTab } = usePageStore(storeType); // derived values - const filteredPageIds = getCurrentProjectFilteredPageIds(pageType); + const filteredPageIds = getCurrentProjectFilteredPageIdsByTab(pageType); if (!filteredPageIds) return <>; return ( diff --git a/web/core/components/pages/pages-list-main-content.tsx b/web/core/components/pages/pages-list-main-content.tsx index 83435a9689c..660b0b461dc 100644 --- a/web/core/components/pages/pages-list-main-content.tsx +++ b/web/core/components/pages/pages-list-main-content.tsx @@ -26,13 +26,13 @@ export const PagesListMainContent: React.FC = observer((props) => { // plane hooks const { t } = useTranslation(); // store hooks - const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIds, getCurrentProjectPageIds, filters } = + const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIdsByTab, getCurrentProjectPageIdsByTab, filters } = usePageStore(storeType); const { toggleCreatePageModal } = useCommandPalette(); const { allowPermissions } = useUserPermissions(); // derived values - const pageIds = getCurrentProjectPageIds(pageType); - const filteredPageIds = getCurrentProjectFilteredPageIds(pageType); + const pageIds = getCurrentProjectPageIdsByTab(pageType); + const filteredPageIds = getCurrentProjectFilteredPageIdsByTab(pageType); const canPerformEmptyStateActions = allowPermissions( [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], EUserPermissionsLevel.PROJECT diff --git a/web/core/components/views/quick-actions.tsx b/web/core/components/views/quick-actions.tsx index bb7f4426ff4..c90f01f2ed5 100644 --- a/web/core/components/views/quick-actions.tsx +++ b/web/core/components/views/quick-actions.tsx @@ -22,10 +22,11 @@ type Props = { projectId: string; view: IProjectView; workspaceSlug: string; + customClassName?: string; }; export const ViewQuickActions: React.FC = observer((props) => { - const { parentRef, projectId, view, workspaceSlug } = props; + const { parentRef, projectId, view, workspaceSlug, customClassName } = props; // states const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false); const [deleteViewModal, setDeleteViewModal] = useState(false); @@ -95,7 +96,7 @@ export const ViewQuickActions: React.FC = observer((props) => { setDeleteViewModal(false)} /> setPublishModalOpen(false)} view={view} /> - + {MENU_ITEMS.map((item) => { if (item.shouldRender === false) return null; return ( diff --git a/web/core/store/pages/project-page.store.ts b/web/core/store/pages/project-page.store.ts index d5773b12629..e3ddacb5fff 100644 --- a/web/core/store/pages/project-page.store.ts +++ b/web/core/store/pages/project-page.store.ts @@ -37,8 +37,9 @@ export interface IProjectPageStore { isAnyPageAvailable: boolean; canCurrentUserCreatePage: boolean; // helper actions - getCurrentProjectPageIds: (pageType: TPageNavigationTabs) => string[] | undefined; - getCurrentProjectFilteredPageIds: (pageType: TPageNavigationTabs) => string[] | undefined; + getCurrentProjectPageIdsByTab: (pageType: TPageNavigationTabs) => string[] | undefined; + getCurrentProjectPageIds: (projectId: string) => string[]; + getCurrentProjectFilteredPageIdsByTab: (pageType: TPageNavigationTabs) => string[] | undefined; getPageById: (pageId: string) => TProjectPage | undefined; updateFilters: (filterKey: T, filterValue: TPageFilters[T]) => void; clearAllFilters: () => void; @@ -46,7 +47,7 @@ export interface IProjectPageStore { fetchPagesList: ( workspaceSlug: string, projectId: string, - pageType: TPageNavigationTabs + pageType?: TPageNavigationTabs ) => Promise; fetchPageDetails: (workspaceSlug: string, projectId: string, pageId: string) => Promise; createPage: (pageData: Partial) => Promise; @@ -125,7 +126,7 @@ export class ProjectPageStore implements IProjectPageStore { * @description get the current project page ids based on the pageType * @param {TPageNavigationTabs} pageType */ - getCurrentProjectPageIds = computedFn((pageType: TPageNavigationTabs) => { + getCurrentProjectPageIdsByTab = computedFn((pageType: TPageNavigationTabs) => { const { projectId } = this.store.router; if (!projectId) return undefined; // helps to filter pages based on the pageType @@ -137,11 +138,21 @@ export class ProjectPageStore implements IProjectPageStore { return pages ?? undefined; }); + /** + * @description get the current project page ids + * @param {string} projectId + */ + getCurrentProjectPageIds = computedFn((projectId: string) => { + if (!projectId) return []; + const pages = Object.values(this?.data || {}).filter((page) => page.project_ids?.includes(projectId)); + return pages.map((page) => page.id) as string[]; + }); + /** * @description get the current project filtered page ids based on the pageType * @param {TPageNavigationTabs} pageType */ - getCurrentProjectFilteredPageIds = computedFn((pageType: TPageNavigationTabs) => { + getCurrentProjectFilteredPageIdsByTab = computedFn((pageType: TPageNavigationTabs) => { const { projectId } = this.store.router; if (!projectId) return undefined; @@ -183,11 +194,11 @@ export class ProjectPageStore implements IProjectPageStore { /** * @description fetch all the pages */ - fetchPagesList = async (workspaceSlug: string, projectId: string, pageType: TPageNavigationTabs) => { + fetchPagesList = async (workspaceSlug: string, projectId: string, pageType?: TPageNavigationTabs) => { try { if (!workspaceSlug || !projectId) return undefined; - const currentPageIds = this.getCurrentProjectPageIds(pageType); + const currentPageIds = pageType ? this.getCurrentProjectPageIdsByTab(pageType) : undefined; runInAction(() => { this.loader = currentPageIds && currentPageIds.length > 0 ? `mutation-loader` : `init-loader`; this.error = undefined; From 953c8497e2cc8c3e484f686847c688540e526ece Mon Sep 17 00:00:00 2001 From: vamsikrishnamathala Date: Wed, 9 Apr 2025 11:47:45 +0530 Subject: [PATCH 2/2] chore: moved list fetch to layout level --- .../(detail)/[projectId]/pages/(detail)/header.tsx | 13 +++---------- .../(detail)/[projectId]/pages/(detail)/layout.tsx | 11 +++++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) 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 6ca74df9125..2062aa32dfe 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 @@ -2,12 +2,11 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useParams } from "next/navigation"; -import useSWR from "swr"; -import { Archive, Earth, FileText, Layers, Lock } from "lucide-react"; +import { FileText, Layers } from "lucide-react"; // types import { ICustomSearchSelectOption } from "@plane/types"; // ui -import { Breadcrumbs, Header, CustomSearchSelect, Tooltip } from "@plane/ui"; +import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui"; // components import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { PageEditInformationPopover } from "@/components/pages"; @@ -33,16 +32,10 @@ export const PageDetailsHeader = observer(() => { pageId: pageId?.toString() ?? "", storeType: EPageStoreType.PROJECT, }); - const { getPageById, getCurrentProjectPageIds, fetchPagesList } = usePageStore(EPageStoreType.PROJECT); - // derived values + const { getPageById, getCurrentProjectPageIds } = usePageStore(EPageStoreType.PROJECT); // derived values const projectPageIds = getCurrentProjectPageIds(projectId?.toString()); - // fetching pages list - useSWR( - workspaceSlug && projectId ? `PROJECT_PAGES_${projectId}` : null, - workspaceSlug && projectId ? () => fetchPagesList(workspaceSlug.toString(), projectId.toString()) : null - ); if (!page) return null; const switcherOptions = projectPageIds .map((id) => { diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx index 3c6f18cf107..dacd6138812 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx @@ -1,11 +1,22 @@ "use client"; // component +import { useParams } from "next/navigation"; +import useSWR from "swr"; import { AppHeader, ContentWrapper } from "@/components/core"; +// plane web hooks +import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store"; // local components import { PageDetailsHeader } from "./header"; export default function ProjectPageDetailsLayout({ children }: { children: React.ReactNode }) { + const { workspaceSlug, projectId } = useParams(); + const { fetchPagesList } = usePageStore(EPageStoreType.PROJECT); + // fetching pages list + useSWR( + workspaceSlug && projectId ? `PROJECT_PAGES_${projectId}` : null, + workspaceSlug && projectId ? () => fetchPagesList(workspaceSlug.toString(), projectId.toString()) : null + ); return ( <> } />