From 2f45bfb7f66acf0f97fc3a4fdc11c780556a7181 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:12:15 +0530 Subject: [PATCH] [WEB-5256]chore: quick actions refactor (#8019) * chore: quick actions refactor * chore: lint fix * chore: unified factory for actions * chore: lint fix * * chore: removed redundant files * chore: updated imports * chore: updated interfaces to types * chore: updated undefined handling --- .../common/quick-actions-factory.tsx | 1 + .../ce/components/cycles/end-cycle/index.ts | 1 - .../cycles/end-cycle/use-end-cycle.tsx | 7 - apps/web/ce/components/views/helper.tsx | 65 -------- .../common/quick-actions-factory.tsx | 82 ++++++++++ .../common/quick-actions-helper.tsx | 145 ++++++++++++++++++ .../core/components/cycles/quick-actions.tsx | 99 ++---------- .../issues/layout-quick-actions.tsx | 72 +++++++++ .../core/components/modules/quick-actions.tsx | 107 ++++--------- .../core/components/views/quick-actions.tsx | 20 ++- .../workspace/views/quick-action.tsx | 16 +- 11 files changed, 365 insertions(+), 250 deletions(-) create mode 100644 apps/web/ce/components/common/quick-actions-factory.tsx delete mode 100644 apps/web/ce/components/cycles/end-cycle/use-end-cycle.tsx create mode 100644 apps/web/core/components/common/quick-actions-factory.tsx create mode 100644 apps/web/core/components/common/quick-actions-helper.tsx create mode 100644 apps/web/core/components/issues/layout-quick-actions.tsx diff --git a/apps/web/ce/components/common/quick-actions-factory.tsx b/apps/web/ce/components/common/quick-actions-factory.tsx new file mode 100644 index 0000000000..a59a61e533 --- /dev/null +++ b/apps/web/ce/components/common/quick-actions-factory.tsx @@ -0,0 +1 @@ +export { useQuickActionsFactory } from "@/components/common/quick-actions-factory"; diff --git a/apps/web/ce/components/cycles/end-cycle/index.ts b/apps/web/ce/components/cycles/end-cycle/index.ts index 2e60c45619..031608e25f 100644 --- a/apps/web/ce/components/cycles/end-cycle/index.ts +++ b/apps/web/ce/components/cycles/end-cycle/index.ts @@ -1,2 +1 @@ export * from "./modal"; -export * from "./use-end-cycle"; diff --git a/apps/web/ce/components/cycles/end-cycle/use-end-cycle.tsx b/apps/web/ce/components/cycles/end-cycle/use-end-cycle.tsx deleted file mode 100644 index c1bf626185..0000000000 --- a/apps/web/ce/components/cycles/end-cycle/use-end-cycle.tsx +++ /dev/null @@ -1,7 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const useEndCycle = (isCurrentCycle: boolean) => ({ - isEndCycleModalOpen: false, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - setEndCycleModalOpen: (value: boolean) => {}, - endCycleContextMenu: undefined, -}); diff --git a/apps/web/ce/components/views/helper.tsx b/apps/web/ce/components/views/helper.tsx index 47a7d35942..155249e2d1 100644 --- a/apps/web/ce/components/views/helper.tsx +++ b/apps/web/ce/components/views/helper.tsx @@ -1,7 +1,4 @@ -import { ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; -import { useTranslation } from "@plane/i18n"; import type { EIssueLayoutTypes, IProjectView } from "@plane/types"; -import type { TContextMenuItem } from "@plane/ui"; import type { TWorkspaceLayoutProps } from "@/components/views/helper"; export type TLayoutSelectionProps = { @@ -18,68 +15,6 @@ export function WorkspaceAdditionalLayouts(props: TWorkspaceLayoutProps) { return <>; } -export type TMenuItemsFactoryProps = { - isOwner: boolean; - isAdmin: boolean; - setDeleteViewModal: (open: boolean) => void; - setCreateUpdateViewModal: (open: boolean) => void; - handleOpenInNewTab: () => void; - handleCopyText: () => void; - isLocked: boolean; - workspaceSlug: string; - projectId?: string; - viewId: string; -}; - -export const useMenuItemsFactory = (props: TMenuItemsFactoryProps) => { - const { isOwner, isAdmin, setDeleteViewModal, setCreateUpdateViewModal, handleOpenInNewTab, handleCopyText } = props; - - const { t } = useTranslation(); - - const editMenuItem = () => ({ - key: "edit", - action: () => setCreateUpdateViewModal(true), - title: t("edit"), - icon: Pencil, - shouldRender: isOwner, - }); - - const openInNewTabMenuItem = () => ({ - key: "open-new-tab", - action: handleOpenInNewTab, - title: t("open_in_new_tab"), - icon: ExternalLink, - }); - - const copyLinkMenuItem = () => ({ - key: "copy-link", - action: handleCopyText, - title: t("copy_link"), - icon: Link, - }); - - const deleteMenuItem = () => ({ - key: "delete", - action: () => setDeleteViewModal(true), - title: t("delete"), - icon: Trash2, - shouldRender: isOwner || isAdmin, - }); - - return { - editMenuItem, - openInNewTabMenuItem, - copyLinkMenuItem, - deleteMenuItem, - }; -}; - -export const useViewMenuItems = (props: TMenuItemsFactoryProps): TContextMenuItem[] => { - const factory = useMenuItemsFactory(props); - - return [factory.editMenuItem(), factory.openInNewTabMenuItem(), factory.copyLinkMenuItem(), factory.deleteMenuItem()]; -}; - // eslint-disable-next-line @typescript-eslint/no-unused-vars export function AdditionalHeaderItems(view: IProjectView) { return <>; diff --git a/apps/web/core/components/common/quick-actions-factory.tsx b/apps/web/core/components/common/quick-actions-factory.tsx new file mode 100644 index 0000000000..52e6bfbd30 --- /dev/null +++ b/apps/web/core/components/common/quick-actions-factory.tsx @@ -0,0 +1,82 @@ +import { Pencil, ExternalLink, Link, Trash2, ArchiveRestoreIcon } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; +import { ArchiveIcon } from "@plane/propel/icons"; +import type { TContextMenuItem } from "@plane/ui"; + +/** + * Unified factory for creating menu items across all entities (cycles, modules, views, epics) + */ +export const useQuickActionsFactory = () => { + const { t } = useTranslation(); + + return { + // Common menu items + createEditMenuItem: (handler: () => void, shouldRender: boolean = true): TContextMenuItem => ({ + key: "edit", + title: t("edit"), + icon: Pencil, + action: handler, + shouldRender, + }), + + createOpenInNewTabMenuItem: (handler: () => void): TContextMenuItem => ({ + key: "open-new-tab", + title: t("open_in_new_tab"), + icon: ExternalLink, + action: handler, + }), + + createCopyLinkMenuItem: (handler: () => void): TContextMenuItem => ({ + key: "copy-link", + title: t("copy_link"), + icon: Link, + action: handler, + }), + + createArchiveMenuItem: ( + handler: () => void, + opts: { shouldRender?: boolean; disabled?: boolean; description?: string } + ): TContextMenuItem => ({ + key: "archive", + title: t("archive"), + icon: ArchiveIcon, + action: handler, + className: "items-start", + iconClassName: "mt-1", + description: opts.description, + disabled: opts.disabled, + shouldRender: opts.shouldRender, + }), + + createRestoreMenuItem: (handler: () => void, shouldRender: boolean = true): TContextMenuItem => ({ + key: "restore", + title: t("restore"), + icon: ArchiveRestoreIcon, + action: handler, + shouldRender, + }), + + createDeleteMenuItem: (handler: () => void, shouldRender: boolean = true): TContextMenuItem => ({ + key: "delete", + title: t("delete"), + icon: Trash2, + action: handler, + shouldRender, + }), + + // Layout-level actions (for work item list views) + createOpenInNewTab: (handler: () => void): TContextMenuItem => ({ + key: "open-in-new-tab", + title: "Open in new tab", + icon: ExternalLink, + action: handler, + }), + + createCopyLayoutLinkMenuItem: (handler: () => void): TContextMenuItem => ({ + key: "copy-link", + title: "Copy link", + icon: Link, + action: handler, + }), + }; +}; diff --git a/apps/web/core/components/common/quick-actions-helper.tsx b/apps/web/core/components/common/quick-actions-helper.tsx new file mode 100644 index 0000000000..eadb1a7062 --- /dev/null +++ b/apps/web/core/components/common/quick-actions-helper.tsx @@ -0,0 +1,145 @@ +// types +import type { ICycle, IModule, IProjectView, IWorkspaceView } from "@plane/types"; +import type { TContextMenuItem } from "@plane/ui"; +// hooks +import { useQuickActionsFactory } from "@/plane-web/components/common/quick-actions-factory"; + +// Types +interface UseCycleMenuItemsProps { + cycleDetails: ICycle | undefined; + isEditingAllowed: boolean; + workspaceSlug: string; + projectId: string; + cycleId: string; + handleEdit: () => void; + handleArchive: () => void; + handleRestore: () => void; + handleDelete: () => void; + handleCopyLink: () => void; + handleOpenInNewTab: () => void; +} + +interface UseModuleMenuItemsProps { + moduleDetails: IModule | undefined; + isEditingAllowed: boolean; + workspaceSlug: string; + projectId: string; + moduleId: string; + handleEdit: () => void; + handleArchive: () => void; + handleRestore: () => void; + handleDelete: () => void; + handleCopyLink: () => void; + handleOpenInNewTab: () => void; +} + +interface UseViewMenuItemsProps { + isOwner: boolean; + isAdmin: boolean; + workspaceSlug: string; + projectId?: string; + view: IProjectView | IWorkspaceView; + handleEdit: () => void; + handleDelete: () => void; + handleCopyLink: () => void; + handleOpenInNewTab: () => void; +} + +interface UseLayoutMenuItemsProps { + workspaceSlug: string; + projectId: string; + storeType: "PROJECT" | "EPIC"; + handleCopyLink: () => void; + handleOpenInNewTab: () => void; +} + +type MenuResult = { + items: TContextMenuItem[]; + modals: JSX.Element | null; +}; + +export const useCycleMenuItems = (props: UseCycleMenuItemsProps): MenuResult => { + const factory = useQuickActionsFactory(); + const { cycleDetails, isEditingAllowed, ...handlers } = props; + + const isArchived = !!cycleDetails?.archived_at; + const isCompleted = cycleDetails?.status?.toLowerCase() === "completed"; + + // Assemble final menu items - order defined here + const items = [ + factory.createEditMenuItem(handlers.handleEdit, isEditingAllowed && !isCompleted && !isArchived), + factory.createOpenInNewTabMenuItem(handlers.handleOpenInNewTab), + factory.createCopyLinkMenuItem(handlers.handleCopyLink), + factory.createArchiveMenuItem(handlers.handleArchive, { + shouldRender: isEditingAllowed && !isArchived, + disabled: !isCompleted, + description: isCompleted ? undefined : "Only completed cycles can be archived", + }), + factory.createRestoreMenuItem(handlers.handleRestore, isEditingAllowed && isArchived), + factory.createDeleteMenuItem(handlers.handleDelete, isEditingAllowed && !isCompleted && !isArchived), + ].filter((item) => item.shouldRender !== false); + + return { items, modals: null }; +}; + +export const useModuleMenuItems = (props: UseModuleMenuItemsProps): MenuResult => { + const factory = useQuickActionsFactory(); + const { moduleDetails, isEditingAllowed, ...handlers } = props; + + const isArchived = !!moduleDetails?.archived_at; + const moduleState = moduleDetails?.status?.toLocaleLowerCase(); + const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState); + + // Assemble final menu items - order defined here + const items = [ + factory.createEditMenuItem(handlers.handleEdit, isEditingAllowed && !isArchived), + factory.createOpenInNewTabMenuItem(handlers.handleOpenInNewTab), + factory.createCopyLinkMenuItem(handlers.handleCopyLink), + factory.createArchiveMenuItem(handlers.handleArchive, { + shouldRender: isEditingAllowed && !isArchived, + disabled: !isInArchivableGroup, + description: isInArchivableGroup ? undefined : "Only completed or cancelled modules can be archived", + }), + factory.createRestoreMenuItem(handlers.handleRestore, isEditingAllowed && isArchived), + factory.createDeleteMenuItem(handlers.handleDelete, isEditingAllowed && !isArchived), + ].filter((item) => item.shouldRender !== false); + + return { items, modals: null }; +}; + +export const useViewMenuItems = (props: UseViewMenuItemsProps): MenuResult => { + const factory = useQuickActionsFactory(); + const { workspaceSlug, isOwner, isAdmin, projectId, view, ...handlers } = props; + + if (!view) return { items: [], modals: null }; + + // Assemble final menu items - order defined here + const items = [ + factory.createEditMenuItem(handlers.handleEdit, isOwner), + factory.createOpenInNewTabMenuItem(handlers.handleOpenInNewTab), + factory.createCopyLinkMenuItem(handlers.handleCopyLink), + factory.createDeleteMenuItem(handlers.handleDelete, isOwner || isAdmin), + ].filter((item) => item.shouldRender !== false); + + return { items, modals: null }; +}; + +export const useLayoutMenuItems = (props: UseLayoutMenuItemsProps): MenuResult => { + const factory = useQuickActionsFactory(); + const { ...handlers } = props; + + // Assemble final menu items - order defined here + const items = [ + factory.createOpenInNewTab(handlers.handleOpenInNewTab), + factory.createCopyLayoutLinkMenuItem(handlers.handleCopyLink), + ].filter((item) => item.shouldRender !== false); + + return { items, modals: null }; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const useIntakeHeaderMenuItems = (props: { + workspaceSlug: string; + projectId: string; + handleCopyLink: () => void; +}): MenuResult => ({ items: [], modals: null }); diff --git a/apps/web/core/components/cycles/quick-actions.tsx b/apps/web/core/components/cycles/quick-actions.tsx index a7ff2298e0..d0966041b4 100644 --- a/apps/web/core/components/cycles/quick-actions.tsx +++ b/apps/web/core/components/cycles/quick-actions.tsx @@ -1,8 +1,6 @@ import { useState } from "react"; import { observer } from "mobx-react"; -// icons -import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react"; // ui import { CYCLE_TRACKER_EVENTS, @@ -11,18 +9,17 @@ import { CYCLE_TRACKER_ELEMENTS, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { ArchiveIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TContextMenuItem } from "@plane/ui"; import { ContextMenu, CustomMenu } from "@plane/ui"; import { copyUrlToClipboard, cn } from "@plane/utils"; // helpers // hooks +import { useCycleMenuItems } from "@/components/common/quick-actions-helper"; import { captureClick, captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import { useCycle } from "@/hooks/store/use-cycle"; import { useUserPermissions } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; -import { useEndCycle, EndCycleModal } from "@/plane-web/components/cycles"; // local imports import { ArchiveCycleModal } from "./archived-cycles/modal"; import { CycleDeleteModal } from "./delete-modal"; @@ -50,12 +47,6 @@ export const CycleQuickActions = observer(function CycleQuickActions(props: Prop const { t } = useTranslation(); // derived values const cycleDetails = getCycleById(cycleId); - const isArchived = !!cycleDetails?.archived_at; - const isCompleted = cycleDetails?.status?.toLowerCase() === "completed"; - const isCurrentCycle = cycleDetails?.status?.toLowerCase() === "current"; - const transferrableIssuesCount = cycleDetails - ? cycleDetails.total_issues - (cycleDetails.cancelled_issues + cycleDetails.completed_issues) - : 0; // auth const isEditingAllowed = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], @@ -64,8 +55,6 @@ export const CycleQuickActions = observer(function CycleQuickActions(props: Prop projectId ); - const { isEndCycleModalOpen, setEndCycleModalOpen, endCycleContextMenu } = useEndCycle(isCurrentCycle); - const cycleLink = `${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`; const handleCopyText = () => copyUrlToClipboard(cycleLink).then(() => { @@ -77,12 +66,6 @@ export const CycleQuickActions = observer(function CycleQuickActions(props: Prop }); const handleOpenInNewTab = () => window.open(`/${cycleLink}`, "_blank"); - const handleEditCycle = () => { - setUpdateModal(true); - }; - - const handleArchiveCycle = () => setArchiveCycleModal(true); - const handleRestoreCycle = async () => await restoreCycle(workspaceSlug, projectId, cycleId) .then(() => { @@ -113,60 +96,22 @@ export const CycleQuickActions = observer(function CycleQuickActions(props: Prop }); }); - const handleDeleteCycle = () => { - setDeleteModal(true); - }; + const menuResult = useCycleMenuItems({ + cycleDetails: cycleDetails ?? undefined, + workspaceSlug, + projectId, + cycleId, + isEditingAllowed, + handleEdit: () => setUpdateModal(true), + handleArchive: () => setArchiveCycleModal(true), + handleRestore: handleRestoreCycle, + handleDelete: () => setDeleteModal(true), + handleCopyLink: handleCopyText, + handleOpenInNewTab, + }); - const MENU_ITEMS: TContextMenuItem[] = [ - { - key: "edit", - title: t("edit"), - icon: Pencil, - action: handleEditCycle, - shouldRender: isEditingAllowed && !isCompleted && !isArchived, - }, - { - key: "open-new-tab", - action: handleOpenInNewTab, - title: t("open_in_new_tab"), - icon: ExternalLink, - shouldRender: !isArchived, - }, - { - key: "copy-link", - action: handleCopyText, - title: t("copy_link"), - icon: LinkIcon, - shouldRender: !isArchived, - }, - { - key: "archive", - action: handleArchiveCycle, - title: t("archive"), - description: isCompleted ? undefined : t("project_cycles.only_completed_cycles_can_be_archived"), - icon: ArchiveIcon, - className: "items-start", - iconClassName: "mt-1", - shouldRender: isEditingAllowed && !isArchived, - disabled: !isCompleted, - }, - { - key: "restore", - action: handleRestoreCycle, - title: t("restore"), - icon: ArchiveRestoreIcon, - shouldRender: isEditingAllowed && isArchived, - }, - { - key: "delete", - action: handleDeleteCycle, - title: t("delete"), - icon: Trash2, - shouldRender: isEditingAllowed && !isCompleted && !isArchived, - }, - ]; - - if (endCycleContextMenu) MENU_ITEMS.splice(3, 0, endCycleContextMenu); + const MENU_ITEMS: TContextMenuItem[] = Array.isArray(menuResult) ? menuResult : menuResult.items; + const additionalModals = Array.isArray(menuResult) ? null : menuResult.modals; const CONTEXT_MENU_ITEMS = MENU_ITEMS.map(function CONTEXT_MENU_ITEMS(item) { return { @@ -206,17 +151,7 @@ export const CycleQuickActions = observer(function CycleQuickActions(props: Prop workspaceSlug={workspaceSlug} projectId={projectId} /> - {isCurrentCycle && ( - setEndCycleModalOpen(false)} - cycleId={cycleId} - projectId={projectId} - workspaceSlug={workspaceSlug} - transferrableIssuesCount={transferrableIssuesCount} - cycleName={cycleDetails.name} - /> - )} + {additionalModals} )} diff --git a/apps/web/core/components/issues/layout-quick-actions.tsx b/apps/web/core/components/issues/layout-quick-actions.tsx new file mode 100644 index 0000000000..ab3d198dc6 --- /dev/null +++ b/apps/web/core/components/issues/layout-quick-actions.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { observer } from "mobx-react"; +import { TOAST_TYPE, setToast } from "@plane/propel/toast"; +import type { TContextMenuItem } from "@plane/ui"; +import { CustomMenu } from "@plane/ui"; +import { copyUrlToClipboard, cn } from "@plane/utils"; +import { useLayoutMenuItems } from "@/components/common/quick-actions-helper"; + +type Props = { + workspaceSlug: string; + projectId: string; + storeType: "PROJECT" | "EPIC"; +}; + +export const LayoutQuickActions: React.FC = observer((props) => { + const { workspaceSlug, projectId, storeType } = props; + + const layoutLink = `${workspaceSlug}/projects/${projectId}/${storeType === "EPIC" ? "epics" : "issues"}`; + + const handleCopyLink = () => + copyUrlToClipboard(layoutLink).then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Link copied", + message: `${storeType === "EPIC" ? "Epics" : "Work items"} link copied to clipboard.`, + }); + }); + + const handleOpenInNewTab = () => window.open(`/${layoutLink}`, "_blank"); + + const menuResult = useLayoutMenuItems({ + workspaceSlug, + projectId, + storeType, + handleCopyLink, + handleOpenInNewTab, + }); + + const MENU_ITEMS: TContextMenuItem[] = Array.isArray(menuResult) ? menuResult : menuResult.items; + const additionalModals = Array.isArray(menuResult) ? null : menuResult.modals; + + return ( + <> + {additionalModals} + + {MENU_ITEMS.map((item) => { + if (item.shouldRender === false) return null; + return ( + + {item.icon && } + {item.title} + + ); + })} + + + ); +}); diff --git a/apps/web/core/components/modules/quick-actions.tsx b/apps/web/core/components/modules/quick-actions.tsx index 264619110a..bab24e50a6 100644 --- a/apps/web/core/components/modules/quick-actions.tsx +++ b/apps/web/core/components/modules/quick-actions.tsx @@ -1,8 +1,6 @@ import { useState } from "react"; import { observer } from "mobx-react"; -// icons -import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react"; // plane imports import { EUserPermissions, @@ -11,13 +9,12 @@ import { MODULE_TRACKER_EVENTS, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -// ui -import { ArchiveIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TContextMenuItem } from "@plane/ui"; import { ContextMenu, CustomMenu } from "@plane/ui"; import { copyUrlToClipboard, cn } from "@plane/utils"; // components +import { useModuleMenuItems } from "@/components/common/quick-actions-helper"; import { ArchiveModuleModal, CreateUpdateModuleModal, DeleteModuleModal } from "@/components/modules"; // helpers import { captureClick, captureSuccess, captureError } from "@/helpers/event-tracker.helper"; @@ -50,7 +47,6 @@ export const ModuleQuickActions = observer(function ModuleQuickActions(props: Pr const { t } = useTranslation(); // derived values const moduleDetails = getModuleById(moduleId); - const isArchived = !!moduleDetails?.archived_at; // auth const isEditingAllowed = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], @@ -59,9 +55,6 @@ export const ModuleQuickActions = observer(function ModuleQuickActions(props: Pr projectId ); - const moduleState = moduleDetails?.status?.toLocaleLowerCase(); - const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState); - const moduleLink = `${workspaceSlug}/projects/${projectId}/modules/${moduleId}`; const handleCopyText = () => copyUrlToClipboard(moduleLink).then(() => { @@ -73,12 +66,6 @@ export const ModuleQuickActions = observer(function ModuleQuickActions(props: Pr }); const handleOpenInNewTab = () => window.open(`/${moduleLink}`, "_blank"); - const handleEditModule = () => { - setEditModal(true); - }; - - const handleArchiveModule = () => setArchiveModuleModal(true); - const handleRestoreModule = async () => await restoreModule(workspaceSlug, projectId, moduleId) .then(() => { @@ -106,72 +93,35 @@ export const ModuleQuickActions = observer(function ModuleQuickActions(props: Pr }); }); - const handleDeleteModule = () => { - setDeleteModal(true); - }; - - const MENU_ITEMS: TContextMenuItem[] = [ - { - key: "edit", - title: t("edit"), - icon: Pencil, - action: handleEditModule, - shouldRender: isEditingAllowed && !isArchived, - }, - { - key: "open-new-tab", - action: handleOpenInNewTab, - title: t("open_in_new_tab"), - icon: ExternalLink, - shouldRender: !isArchived, - }, - { - key: "copy-link", - action: handleCopyText, - title: t("copy_link"), - icon: LinkIcon, - shouldRender: !isArchived, - }, - { - key: "archive", - action: handleArchiveModule, - title: t("archive"), - description: isInArchivableGroup ? undefined : t("project_module.quick_actions.archive_module_description"), - icon: ArchiveIcon, - className: "items-start", - iconClassName: "mt-1", - shouldRender: isEditingAllowed && !isArchived, - disabled: !isInArchivableGroup, - }, - { - key: "restore", - action: handleRestoreModule, - title: t("restore"), - icon: ArchiveRestoreIcon, - shouldRender: isEditingAllowed && isArchived, - }, - { - key: "delete", - action: handleDeleteModule, - title: t("delete"), - icon: Trash2, - shouldRender: isEditingAllowed, - }, - ]; - - const CONTEXT_MENU_ITEMS = MENU_ITEMS.map(function CONTEXT_MENU_ITEMS(item) { - return { - ...item, - - onClick: () => { - captureClick({ - elementName: MODULE_TRACKER_ELEMENTS.CONTEXT_MENU, - }); - item.action(); - }, - }; + // Use unified menu hook from plane-web (resolves to CE or EE) + const menuResult = useModuleMenuItems({ + moduleDetails: moduleDetails ?? undefined, + workspaceSlug, + projectId, + moduleId, + isEditingAllowed, + handleEdit: () => setEditModal(true), + handleArchive: () => setArchiveModuleModal(true), + handleRestore: handleRestoreModule, + handleDelete: () => setDeleteModal(true), + handleCopyLink: handleCopyText, + handleOpenInNewTab, }); + // Handle both CE (array) and EE (object) return types + const MENU_ITEMS: TContextMenuItem[] = Array.isArray(menuResult) ? menuResult : menuResult.items; + const additionalModals = Array.isArray(menuResult) ? null : menuResult.modals; + + const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({ + ...item, + action: () => { + captureClick({ + elementName: MODULE_TRACKER_ELEMENTS.CONTEXT_MENU, + }); + item.action(); + }, + })); + return ( <> {moduleDetails && ( @@ -191,6 +141,7 @@ export const ModuleQuickActions = observer(function ModuleQuickActions(props: Pr handleClose={() => setArchiveModuleModal(false)} /> setDeleteModal(false)} /> + {additionalModals} )} diff --git a/apps/web/core/components/views/quick-actions.tsx b/apps/web/core/components/views/quick-actions.tsx index 4fcaab65db..8d83eaaf38 100644 --- a/apps/web/core/components/views/quick-actions.tsx +++ b/apps/web/core/components/views/quick-actions.tsx @@ -9,10 +9,10 @@ import type { TContextMenuItem } from "@plane/ui"; import { ContextMenu, CustomMenu } from "@plane/ui"; import { copyUrlToClipboard, cn } from "@plane/utils"; // helpers +import { useViewMenuItems } from "@/components/common/quick-actions-helper"; import { captureClick } from "@/helpers/event-tracker.helper"; // hooks import { useUser, useUserPermissions } from "@/hooks/store/user"; -import { useViewMenuItems } from "@/plane-web/components/views/helper"; import { PublishViewModal, useViewPublish } from "@/plane-web/components/views/publish"; // local imports import { DeleteProjectViewModal } from "./delete-view-modal"; @@ -54,19 +54,22 @@ export const ViewQuickActions = observer(function ViewQuickActions(props: Props) }); const handleOpenInNewTab = () => window.open(`/${viewLink}`, "_blank"); - const MENU_ITEMS: TContextMenuItem[] = useViewMenuItems({ + const menuResult = useViewMenuItems({ isOwner, isAdmin, - setDeleteViewModal, - setCreateUpdateViewModal, - handleOpenInNewTab, - handleCopyText, - isLocked: view.is_locked, workspaceSlug, projectId, - viewId: view.id, + view, + handleEdit: () => setCreateUpdateViewModal(true), + handleDelete: () => setDeleteViewModal(true), + handleCopyLink: handleCopyText, + handleOpenInNewTab, }); + // Handle both CE (array) and EE (object) return types + const MENU_ITEMS: TContextMenuItem[] = Array.isArray(menuResult) ? menuResult : menuResult.items; + const additionalModals = Array.isArray(menuResult) ? null : menuResult.modals; + if (publishContextMenu) MENU_ITEMS.splice(2, 0, publishContextMenu); const CONTEXT_MENU_ITEMS = MENU_ITEMS.map(function CONTEXT_MENU_ITEMS(item) { @@ -91,6 +94,7 @@ export const ViewQuickActions = observer(function ViewQuickActions(props: Props) /> setDeleteViewModal(false)} /> setPublishModalOpen(false)} view={view} /> + {additionalModals} {MENU_ITEMS.map((item) => { diff --git a/apps/web/core/components/workspace/views/quick-action.tsx b/apps/web/core/components/workspace/views/quick-action.tsx index b5adac064d..5b9f7a07f8 100644 --- a/apps/web/core/components/workspace/views/quick-action.tsx +++ b/apps/web/core/components/workspace/views/quick-action.tsx @@ -4,14 +4,13 @@ import { observer } from "mobx-react"; import { EUserPermissions, EUserPermissionsLevel, GLOBAL_VIEW_TRACKER_ELEMENTS } from "@plane/constants"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { IWorkspaceView } from "@plane/types"; -import type { TContextMenuItem } from "@plane/ui"; import { CustomMenu } from "@plane/ui"; import { copyUrlToClipboard, cn } from "@plane/utils"; // helpers +import { useViewMenuItems } from "@/components/common/quick-actions-helper"; import { captureClick } from "@/helpers/event-tracker.helper"; // hooks import { useUser, useUserPermissions } from "@/hooks/store/user"; -import { useViewMenuItems } from "@/plane-web/components/views/helper"; // local imports import { DeleteGlobalViewModal } from "./delete-view-modal"; import { CreateUpdateWorkspaceViewModal } from "./modal"; @@ -44,16 +43,15 @@ export const WorkspaceViewQuickActions = observer(function WorkspaceViewQuickAct }); const handleOpenInNewTab = () => window.open(`/${viewLink}`, "_blank"); - const MENU_ITEMS: TContextMenuItem[] = useViewMenuItems({ + const MENU_ITEMS = useViewMenuItems({ isOwner, isAdmin, - setDeleteViewModal, - setCreateUpdateViewModal: setUpdateViewModal, + handleDelete: () => setDeleteViewModal(true), + handleEdit: () => setUpdateViewModal(true), handleOpenInNewTab, - handleCopyText, - isLocked: view.is_locked, + handleCopyLink: handleCopyText, workspaceSlug, - viewId: view.id, + view, }); return ( @@ -66,7 +64,7 @@ export const WorkspaceViewQuickActions = observer(function WorkspaceViewQuickAct closeOnSelect buttonClassName="flex-shrink-0 flex items-center justify-center size-[26px] bg-custom-background-80/70 rounded" > - {MENU_ITEMS.map((item) => { + {MENU_ITEMS.items.map((item) => { if (item.shouldRender === false) return null; return (