diff --git a/plane-src/apps/web/core/components/comments/quick-actions.tsx b/plane-src/apps/web/core/components/comments/quick-actions.tsx index 8facbe0..d1d3277 100644 --- a/plane-src/apps/web/core/components/comments/quick-actions.tsx +++ b/plane-src/apps/web/core/components/comments/quick-actions.tsx @@ -10,12 +10,11 @@ import { MoreHorizontal } from "lucide-react"; // plane imports import { EIssueCommentAccessSpecifier } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { IconButton } from "@plane/propel/icon-button"; +import { getIconButtonStyling } from "@plane/propel/icon-button"; import { LinkIcon, GlobeIcon, LockIcon, EditIcon, TrashIcon } from "@plane/propel/icons"; import type { TIssueComment, TCommentsOperations } from "@plane/types"; import type { TContextMenuItem } from "@plane/ui"; -import { CustomMenu } from "@plane/ui"; -import { cn } from "@plane/utils"; +import { ActionDropdown } from "@plane/ui"; // hooks import { useUser } from "@/hooks/store/user"; @@ -84,39 +83,11 @@ export const CommentQuickActions = observer(function CommentQuickActions(props: ); return ( - } closeOnSelect> - {MENU_ITEMS.map((item) => { - if (item.shouldRender === false) return null; - - return ( - item.action()} - className={cn( - "flex items-center gap-2", - { - "text-placeholder": item.disabled, - }, - item.className - )} - disabled={item.disabled} - > - {item.icon && } -
-
{item.title}
- {item.description && ( -

- {item.description} -

- )} -
-
- ); - })} -
+ } + buttonClassName={getIconButtonStyling("ghost", "sm")} + placement="bottom-end" + /> ); }); diff --git a/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx b/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx index 6c8135d..ca396aa 100644 --- a/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx +++ b/plane-src/apps/web/core/components/issues/attachment/attachment-list-item.tsx @@ -7,12 +7,14 @@ import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; +import { getIconButtonStyling } from "@plane/propel/icon-button"; import { TrashIcon } from "@plane/propel/icons"; import { Tooltip } from "@plane/propel/tooltip"; import type { TIssueServiceType } from "@plane/types"; import { EIssueServiceType } from "@plane/types"; // ui -import { CustomMenu } from "@plane/ui"; +import type { TContextMenuItem } from "@plane/ui"; +import { ActionDropdown } from "@plane/ui"; import { convertBytesToSize, getFileExtension, getFileName, getFileURL, renderFormattedDate } from "@plane/utils"; // components // @@ -46,21 +48,39 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt const fileExtension = getFileExtension(attachment?.attributes.name ?? ""); const fileIcon = getFileIcon(fileExtension, 18); const fileURL = getFileURL(attachment?.asset_url ?? ""); + const menuItems: TContextMenuItem[] = [ + { + key: "delete", + action: () => { + toggleDeleteAttachmentModal(attachmentId); + }, + title: t("common.actions.delete"), + icon: TrashIcon, + }, + ]; // hooks const { isMobile } = usePlatformOS(); if (!attachment) return <>; return ( - <> - - + + ); }); diff --git a/plane-src/apps/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx b/plane-src/apps/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx index efe3c4b..5f5dac4 100644 --- a/plane-src/apps/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx +++ b/plane-src/apps/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx @@ -7,12 +7,14 @@ import { observer } from "mobx-react"; import { Link as Loader } from "lucide-react"; import { useTranslation } from "@plane/i18n"; +import { getIconButtonStyling } from "@plane/propel/icon-button"; import { LinkIcon, EditIcon, TrashIcon, CloseIcon, ChevronRightIcon } from "@plane/propel/icons"; // plane imports import { Tooltip } from "@plane/propel/tooltip"; import type { TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types"; import { EIssueServiceType, EIssuesStoreType } from "@plane/types"; -import { ControlLink, CustomMenu } from "@plane/ui"; +import type { TContextMenuItem } from "@plane/ui"; +import { ActionDropdown, ControlLink } from "@plane/ui"; import { cn, generateWorkItemLink } from "@plane/utils"; // helpers import { useSubIssueOperations } from "@/components/issues/issue-detail-widgets/sub-issues/helper"; @@ -102,6 +104,45 @@ export const SubIssuesListItem = observer(function SubIssuesListItem(props: Prop projectIdentifier: projectDetail?.identifier, sequenceId: issue?.sequence_id, }); + const menuItems: TContextMenuItem[] = [ + { + key: "edit", + action: () => { + handleIssueCrudState("update", parentIssueId, { ...issue }); + toggleCreateIssueModal(true); + }, + title: t("issue.edit"), + icon: EditIcon, + shouldRender: canEdit, + }, + { + key: "copy-link", + action: () => { + subIssueOperations.copyLink(workItemLink); + }, + title: t("issue.copy_link"), + icon: LinkIcon, + }, + { + key: "remove-parent", + action: () => { + if (issue.project_id) subIssueOperations.removeSubIssue(workspaceSlug, issue.project_id, parentIssueId, issue.id); + }, + title: issueServiceType === EIssueServiceType.ISSUES ? t("issue.remove.parent.label") : t("issue.remove.label"), + icon: CloseIcon, + shouldRender: canEdit, + }, + { + key: "delete", + action: () => { + handleIssueCrudState("delete", parentIssueId, issue); + toggleDeleteIssueModal(issue.id); + }, + title: t("issue.delete.label"), + icon: TrashIcon, + shouldRender: canEdit, + }, + ]; return (
@@ -189,62 +230,11 @@ export const SubIssuesListItem = observer(function SubIssuesListItem(props: Prop
- - {canEdit && ( - { - handleIssueCrudState("update", parentIssueId, { ...issue }); - toggleCreateIssueModal(true); - }} - > -
- - {t("issue.edit")} -
-
- )} - - { - subIssueOperations.copyLink(workItemLink); - }} - > -
- - {t("issue.copy_link")} -
-
- - {canEdit && ( - { - if (issue.project_id) - subIssueOperations.removeSubIssue(workspaceSlug, issue.project_id, parentIssueId, issue.id); - }} - > -
- - {issueServiceType === EIssueServiceType.ISSUES - ? t("issue.remove.parent.label") - : t("issue.remove.label")} -
-
- )} - - {canEdit && ( - { - handleIssueCrudState("delete", parentIssueId, issue); - toggleDeleteIssueModal(issue.id); - }} - > -
- - {t("issue.delete.label")} -
-
- )} -
+
)} diff --git a/plane-src/apps/web/core/components/issues/issue-detail/links/link-item.tsx b/plane-src/apps/web/core/components/issues/issue-detail/links/link-item.tsx index 765c349..aa16e79 100644 --- a/plane-src/apps/web/core/components/issues/issue-detail/links/link-item.tsx +++ b/plane-src/apps/web/core/components/issues/issue-detail/links/link-item.tsx @@ -5,15 +5,18 @@ */ import { observer } from "mobx-react"; +import { MoreHorizontal } from "lucide-react"; import { useTranslation } from "@plane/i18n"; +import { getIconButtonStyling } from "@plane/propel/icon-button"; import { LinkIcon, CopyIcon, EditIcon, TrashIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { Tooltip } from "@plane/propel/tooltip"; import type { TIssueServiceType } from "@plane/types"; import { EIssueServiceType } from "@plane/types"; // ui -import { CustomMenu } from "@plane/ui"; +import type { TContextMenuItem } from "@plane/ui"; +import { ActionDropdown } from "@plane/ui"; import { calculateTimeAgo, copyTextToClipboard } from "@plane/utils"; // helpers // hooks @@ -45,6 +48,26 @@ export const IssueLinkItem = observer(function IssueLinkItem(props: TIssueLinkIt // const Icon = getIconForLink(linkDetail.url); const faviconUrl: string | undefined = linkDetail.metadata?.favicon; const linkTitle: string | undefined = linkDetail.metadata?.title; + const menuItems: TContextMenuItem[] = [ + { + key: "edit", + action: () => { + toggleIssueLinkModal(true); + }, + title: t("common.actions.edit"), + icon: EditIcon, + shouldRender: !isNotAllowed, + }, + { + key: "delete", + action: () => { + linkOperations.remove(linkDetail.id); + }, + title: t("common.actions.delete"), + icon: TrashIcon, + shouldRender: !isNotAllowed, + }, + ]; const toggleIssueLinkModal = (modalToggle: boolean) => { toggleIssueLinkModalStore(modalToggle); @@ -95,32 +118,13 @@ export const IssueLinkItem = observer(function IssueLinkItem(props: TIssueLinkIt > - } + buttonClassName={getIconButtonStyling("ghost", "sm") + " text-placeholder group-hover:text-secondary"} placement="bottom-end" - closeOnSelect disabled={isNotAllowed} - > - { - toggleIssueLinkModal(true); - }} - > - - {t("common.actions.edit")} - - { - linkOperations.remove(linkDetail.id); - }} - > - - {t("common.actions.delete")} - - + /> diff --git a/plane-src/apps/web/core/components/issues/relations/issue-list-item.tsx b/plane-src/apps/web/core/components/issues/relations/issue-list-item.tsx index 573a4b7..b69ddeb 100644 --- a/plane-src/apps/web/core/components/issues/relations/issue-list-item.tsx +++ b/plane-src/apps/web/core/components/issues/relations/issue-list-item.tsx @@ -4,15 +4,16 @@ * See the LICENSE file for details. */ -import React from "react"; import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; +import { getIconButtonStyling } from "@plane/propel/icon-button"; import { LinkIcon, EditIcon, TrashIcon, CloseIcon } from "@plane/propel/icons"; // plane imports import { Tooltip } from "@plane/propel/tooltip"; import type { TIssue, TIssueServiceType } from "@plane/types"; import { EIssueServiceType } from "@plane/types"; -import { ControlLink, CustomMenu } from "@plane/ui"; +import type { TContextMenuItem } from "@plane/ui"; +import { ActionDropdown, ControlLink } from "@plane/ui"; import { generateWorkItemLink } from "@plane/utils"; // hooks import { useIssueDetail } from "@/hooks/store/use-issue-detail"; @@ -59,8 +60,8 @@ export const RelationIssueListItem = observer(function RelationIssueListItem(pro const { issue: { getIssueById }, removeRelation, - toggleCreateIssueModal, toggleDeleteIssueModal, + toggleCreateIssueModal, } = useIssueDetail(issueServiceType); const project = useProject(); const { isMobile } = usePlatformOS(); @@ -92,32 +93,42 @@ export const RelationIssueListItem = observer(function RelationIssueListItem(pro handleRedirection(workspaceSlug, issue, isMobile); }; - const handleEditIssue = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - handleIssueCrudState("update", relationIssueId, { ...issue }); - toggleCreateIssueModal(true); - }; - - const handleDeleteIssue = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - handleIssueCrudState("delete", relationIssueId, issue); - toggleDeleteIssueModal(relationIssueId); - handleIssueCrudState("removeRelation", issueId, issue, relationKey, relationIssueId); - }; - - const handleCopyIssueLink = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - issueOperations.copyLink(workItemLink); - }; - - const handleRemoveRelation = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - removeRelation(workspaceSlug, projectId, issueId, relationKey, relationIssueId); - }; + const menuItems: TContextMenuItem[] = [ + { + key: "edit", + action: () => { + handleIssueCrudState("update", relationIssueId, { ...issue }); + toggleCreateIssueModal(true); + }, + title: t("common.actions.edit"), + icon: EditIcon, + shouldRender: !disabled, + }, + { + key: "copy-link", + action: () => issueOperations.copyLink(workItemLink), + title: t("common.actions.copy_link"), + icon: LinkIcon, + }, + { + key: "remove-relation", + action: () => removeRelation(workspaceSlug, projectId, issueId, relationKey, relationIssueId), + title: t("common.actions.remove_relation"), + icon: CloseIcon, + shouldRender: !disabled, + }, + { + key: "delete", + action: () => { + handleIssueCrudState("delete", relationIssueId, issue); + toggleDeleteIssueModal(relationIssueId); + handleIssueCrudState("removeRelation", issueId, issue, relationKey, relationIssueId); + }, + title: t("common.actions.delete"), + icon: TrashIcon, + shouldRender: !disabled, + }, + ]; return (
@@ -164,41 +175,11 @@ export const RelationIssueListItem = observer(function RelationIssueListItem(pro />
- - {!disabled && ( - -
- - {t("common.actions.edit")} -
-
- )} - - -
- - {t("common.actions.copy_link")} -
-
- - {!disabled && ( - -
- - {t("common.actions.remove_relation")} -
-
- )} - - {!disabled && ( - -
- - {t("common.actions.delete")} -
-
- )} -
+
)} diff --git a/plane-src/packages/ui/src/dropdowns/action-dropdown.tsx b/plane-src/packages/ui/src/dropdowns/action-dropdown.tsx index cdf12b8..2c5927f 100644 --- a/plane-src/packages/ui/src/dropdowns/action-dropdown.tsx +++ b/plane-src/packages/ui/src/dropdowns/action-dropdown.tsx @@ -21,6 +21,7 @@ export interface IActionDropdownProps { button?: TActionDropdownTrigger; buttonClassName?: string; className?: string; + disabled?: boolean; items: TContextMenuItem[]; menuClassName?: string; onOpenChange?: (isOpen: boolean) => void; @@ -107,13 +108,14 @@ function ActionDropdownItem(props: TActionDropdownItemProps) { } export function ActionDropdown(props: IActionDropdownProps) { - const { button, buttonClassName, className, items, menuClassName, onOpenChange, placement, portalElement } = props; + const { button, buttonClassName, className, disabled = false, items, menuClassName, onOpenChange, placement, portalElement } = props; const dropdownRef = React.useRef(null); const [referenceElement, setReferenceElement] = React.useState(null); const [popperElement, setPopperElement] = React.useState(null); const [isOpen, setIsOpen] = React.useState(false); const renderedItems = items.filter((item) => item.shouldRender !== false); + const isDropdownDisabled = disabled || renderedItems.length === 0; const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: placement ?? "bottom-end", @@ -141,10 +143,10 @@ export function ActionDropdown(props: IActionDropdownProps) { }); const openDropdown = React.useCallback(() => { - if (renderedItems.length === 0) return; + if (isDropdownDisabled) return; setIsOpen(true); onOpenChange?.(true); - }, [onOpenChange, renderedItems.length]); + }, [isDropdownDisabled, onOpenChange]); const closeDropdown = React.useCallback(() => { if (!isOpen) return; @@ -156,7 +158,7 @@ export function ActionDropdown(props: IActionDropdownProps) { event.preventDefault(); event.stopPropagation(); - if (renderedItems.length === 0) return; + if (isDropdownDisabled) return; if (isOpen) { closeDropdown(); @@ -177,14 +179,14 @@ export function ActionDropdown(props: IActionDropdownProps) { className={cn( "clickable relative grid place-items-center rounded-sm border-0 bg-transparent p-1 text-secondary shadow-none outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 hover:text-primary", { - "cursor-not-allowed": renderedItems.length === 0, - "cursor-pointer": renderedItems.length > 0, + "cursor-not-allowed": isDropdownDisabled, + "cursor-pointer": !isDropdownDisabled, }, buttonClassName )} onClick={handleTriggerClick} onKeyDown={handleKeyDown} - disabled={renderedItems.length === 0} + disabled={isDropdownDisabled} aria-haspopup="menu" aria-expanded={isOpen} aria-label="Work item actions" @@ -201,14 +203,14 @@ export function ActionDropdown(props: IActionDropdownProps) { className={cn( "clickable block h-full rounded-full border-0 bg-transparent p-0 shadow-none outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0", { - "cursor-not-allowed": renderedItems.length === 0, - "cursor-pointer": renderedItems.length > 0, + "cursor-not-allowed": isDropdownDisabled, + "cursor-pointer": !isDropdownDisabled, }, buttonClassName )} onClick={handleTriggerClick} onKeyDown={handleKeyDown} - disabled={renderedItems.length === 0} + disabled={isDropdownDisabled} aria-haspopup="menu" aria-expanded={isOpen} aria-label="Work item actions"