Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/ce/components/common/quick-actions-factory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useQuickActionsFactory } from "@/components/common/quick-actions-factory";
1 change: 0 additions & 1 deletion apps/web/ce/components/cycles/end-cycle/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./modal";
export * from "./use-end-cycle";
7 changes: 0 additions & 7 deletions apps/web/ce/components/cycles/end-cycle/use-end-cycle.tsx

This file was deleted.

65 changes: 0 additions & 65 deletions apps/web/ce/components/views/helper.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -14,67 +11,5 @@ export const GlobalViewLayoutSelection = (props: TLayoutSelectionProps) => <></>

export const WorkspaceAdditionalLayouts = (props: TWorkspaceLayoutProps) => <></>;

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 const AdditionalHeaderItems = (view: IProjectView) => <></>;
82 changes: 82 additions & 0 deletions apps/web/core/components/common/quick-actions-factory.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 145 additions & 0 deletions apps/web/core/components/common/quick-actions-helper.tsx
Original file line number Diff line number Diff line change
@@ -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
export interface UseCycleMenuItemsProps {
cycleDetails: ICycle;
isEditingAllowed: boolean;
workspaceSlug: string;
projectId: string;
cycleId: string;
handleEdit: () => void;
handleArchive: () => void;
handleRestore: () => void;
handleDelete: () => void;
handleCopyLink: () => void;
handleOpenInNewTab: () => void;
}

export interface UseModuleMenuItemsProps {
moduleDetails: IModule;
isEditingAllowed: boolean;
workspaceSlug: string;
projectId: string;
moduleId: string;
handleEdit: () => void;
handleArchive: () => void;
handleRestore: () => void;
handleDelete: () => void;
handleCopyLink: () => void;
handleOpenInNewTab: () => void;
}

export interface UseViewMenuItemsProps {
isOwner: boolean;
isAdmin: boolean;
workspaceSlug: string;
projectId?: string;
view: IProjectView | IWorkspaceView;
handleEdit: () => void;
handleDelete: () => void;
handleCopyLink: () => void;
handleOpenInNewTab: () => void;
}

export interface UseLayoutMenuItemsProps {
workspaceSlug: string;
projectId: string;
storeType: "PROJECT" | "EPIC";
handleCopyLink: () => void;
handleOpenInNewTab: () => void;
}

export 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 { isOwner, isAdmin, 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 });
Loading
Loading