mirror of
https://github.com/makeplane/plane.git
synced 2025-12-30 10:20:46 -06:00
[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
This commit is contained in:
1
apps/web/ce/components/common/quick-actions-factory.tsx
Normal file
1
apps/web/ce/components/common/quick-actions-factory.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { useQuickActionsFactory } from "@/components/common/quick-actions-factory";
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./modal";
|
||||
export * from "./use-end-cycle";
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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 <></>;
|
||||
|
||||
82
apps/web/core/components/common/quick-actions-factory.tsx
Normal file
82
apps/web/core/components/common/quick-actions-factory.tsx
Normal file
@@ -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,
|
||||
}),
|
||||
};
|
||||
};
|
||||
145
apps/web/core/components/common/quick-actions-helper.tsx
Normal file
145
apps/web/core/components/common/quick-actions-helper.tsx
Normal file
@@ -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 });
|
||||
@@ -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 && (
|
||||
<EndCycleModal
|
||||
isOpen={isEndCycleModalOpen}
|
||||
handleClose={() => setEndCycleModalOpen(false)}
|
||||
cycleId={cycleId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
transferrableIssuesCount={transferrableIssuesCount}
|
||||
cycleName={cycleDetails.name}
|
||||
/>
|
||||
)}
|
||||
{additionalModals}
|
||||
</div>
|
||||
)}
|
||||
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
|
||||
|
||||
72
apps/web/core/components/issues/layout-quick-actions.tsx
Normal file
72
apps/web/core/components/issues/layout-quick-actions.tsx
Normal file
@@ -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<Props> = 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}
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
placement="bottom-end"
|
||||
closeOnSelect
|
||||
maxHeight="lg"
|
||||
className="flex-shrink-0 flex items-center justify-center size-[26px] bg-custom-background-80/70 rounded"
|
||||
>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
if (item.shouldRender === false) return null;
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={item.action}
|
||||
className={cn("flex items-center gap-2", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className="h-3 w-3" />}
|
||||
<span>{item.title}</span>
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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)}
|
||||
/>
|
||||
<DeleteModuleModal data={moduleDetails} isOpen={deleteModal} onClose={() => setDeleteModal(false)} />
|
||||
{additionalModals}
|
||||
</div>
|
||||
)}
|
||||
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
|
||||
|
||||
@@ -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)
|
||||
/>
|
||||
<DeleteProjectViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
|
||||
<PublishViewModal isOpen={isPublishModalOpen} onClose={() => setPublishModalOpen(false)} view={view} />
|
||||
{additionalModals}
|
||||
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
|
||||
<CustomMenu ellipsis placement="bottom-end" closeOnSelect buttonClassName={customClassName}>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
|
||||
@@ -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 (
|
||||
<CustomMenu.MenuItem
|
||||
|
||||
Reference in New Issue
Block a user