diff --git a/plane-src/apps/web/core/components/issues/issue-detail-widgets/relations/quick-action-button.tsx b/plane-src/apps/web/core/components/issues/issue-detail-widgets/relations/quick-action-button.tsx index 4637588..8dfbfb0 100644 --- a/plane-src/apps/web/core/components/issues/issue-detail-widgets/relations/quick-action-button.tsx +++ b/plane-src/apps/web/core/components/issues/issue-detail-widgets/relations/quick-action-button.tsx @@ -8,10 +8,12 @@ import React from "react"; import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; +import { getIconButtonStyling } from "@plane/propel/icon-button"; import { PlusIcon } from "@plane/propel/icons"; // plane imports import type { TIssueServiceType } from "@plane/types"; -import { CustomMenu } from "@plane/ui"; +import type { TContextMenuItem } from "@plane/ui"; +import { ActionDropdown } from "@plane/ui"; // hooks import { useIssueDetail } from "@/hooks/store/use-issue-detail"; // Plane-web @@ -40,33 +42,32 @@ export const RelationActionButton = observer(function RelationActionButton(props }; // button element - const customButtonElement = customButton ? <>{customButton} : ; + const items: TContextMenuItem[] = Object.values(ISSUE_RELATION_OPTIONS).flatMap((item, index) => + item + ? [ + { + key: `${item.key}-${index}`, + title: t(item.i18n_label), + action: () => handleOnClick(item.key), + customContent: ( +
+ {item.icon(12)} + {t(item.i18n_label)} +
+ ), + }, + ] + : [] + ); return ( - } + buttonAsChild={!!customButton} + buttonClassName={!customButton ? getIconButtonStyling("ghost", "sm") : undefined} placement="bottom-start" disabled={disabled} - maxHeight="lg" - closeOnSelect - > - {Object.values(ISSUE_RELATION_OPTIONS).map((item, index) => { - if (!item) return <>; - - return ( - { - handleOnClick(item.key); - }} - > -
- {item.icon(12)} - {t(item.i18n_label)} -
-
- ); - })} -
+ /> ); }); diff --git a/plane-src/apps/web/core/components/issues/issue-detail-widgets/sub-issues/quick-action-button.tsx b/plane-src/apps/web/core/components/issues/issue-detail-widgets/sub-issues/quick-action-button.tsx index ded8cc8..abb910b 100644 --- a/plane-src/apps/web/core/components/issues/issue-detail-widgets/sub-issues/quick-action-button.tsx +++ b/plane-src/apps/web/core/components/issues/issue-detail-widgets/sub-issues/quick-action-button.tsx @@ -8,9 +8,11 @@ import React from "react"; import { observer } from "mobx-react"; // plane imports import { useTranslation } from "@plane/i18n"; +import { getIconButtonStyling } from "@plane/propel/icon-button"; import { PlusIcon, WorkItemsIcon } from "@plane/propel/icons"; import type { TIssue, TIssueServiceType } from "@plane/types"; -import { CustomMenu } from "@plane/ui"; +import type { TContextMenuItem } from "@plane/ui"; +import { ActionDropdown } from "@plane/ui"; // hooks import { useIssueDetail } from "@/hooks/store/use-issue-detail"; @@ -66,37 +68,29 @@ export const SubIssuesActionButton = observer(function SubIssuesActionButton(pro }; // options - const optionItems = [ + const optionItems: TContextMenuItem[] = [ { - i18n_label: "common.create_new", - icon: , - onClick: handleCreateNew, + key: "create-new", + title: t("common.create_new"), + icon: PlusIcon, + action: handleCreateNew, }, { - i18n_label: "common.add_existing", - icon: , - onClick: handleAddExisting, + key: "add-existing", + title: t("common.add_existing"), + icon: WorkItemsIcon, + action: handleAddExisting, }, ]; - // button element - const customButtonElement = customButton ? <>{customButton} : ; - return ( - - {optionItems.map((item, index) => ( - { - item.onClick(); - }} - > -
- {item.icon} - {t(item.i18n_label)} -
-
- ))} -
+ } + buttonAsChild={!!customButton} + buttonClassName={!customButton ? getIconButtonStyling("ghost", "sm") : undefined} + placement="bottom-start" + disabled={disabled} + /> ); }); diff --git a/plane-src/apps/web/core/components/issues/issue-detail-widgets/widget-button.tsx b/plane-src/apps/web/core/components/issues/issue-detail-widgets/widget-button.tsx index e1c2ff1..460a495 100644 --- a/plane-src/apps/web/core/components/issues/issue-detail-widgets/widget-button.tsx +++ b/plane-src/apps/web/core/components/issues/issue-detail-widgets/widget-button.tsx @@ -9,17 +9,22 @@ import React from "react"; import { Button } from "@plane/propel/button"; import { cn } from "@plane/utils"; -type Props = { +type Props = Omit, "title"> & { icon: React.ReactNode; title: string; disabled?: boolean; compactView?: boolean; }; -export function IssueDetailWidgetButton(props: Props) { - const { icon, title, disabled = false, compactView = false } = props; +export const IssueDetailWidgetButton = React.forwardRef(function IssueDetailWidgetButton( + props, + ref +) { + const { icon, title, disabled = false, compactView = false, className, ...buttonProps } = props; + return ( ); -} +}); diff --git a/plane-src/apps/web/core/components/issues/issue-detail/parent/root.tsx b/plane-src/apps/web/core/components/issues/issue-detail/parent/root.tsx index 6aa9149..86af8b9 100644 --- a/plane-src/apps/web/core/components/issues/issue-detail/parent/root.tsx +++ b/plane-src/apps/web/core/components/issues/issue-detail/parent/root.tsx @@ -8,10 +8,11 @@ import { observer } from "mobx-react"; import { useRouter } from "next/navigation"; import { MinusCircle } from "lucide-react"; import { useTranslation } from "@plane/i18n"; +import { getIconButtonStyling } from "@plane/propel/icon-button"; import type { TIssue } from "@plane/types"; // component // ui -import { ControlLink, CustomMenu } from "@plane/ui"; +import { ActionDropdown, ControlLink } from "@plane/ui"; // helpers import { generateWorkItemLink } from "@plane/utils"; // hooks @@ -92,19 +93,38 @@ export const IssueParentDetail = observer(function IssueParentDetail(props: TIss - -
{t("issue.sibling.label")}
+ ( +
+
+ {t("issue.sibling.label")} +
- + - issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null })} - className="flex items-center gap-2 py-2 text-danger-primary" - > - - {t("issue.remove.parent.label")} - - + +
+ )} + /> ); diff --git a/plane-src/apps/web/core/components/issues/issue-detail/parent/sibling-item.tsx b/plane-src/apps/web/core/components/issues/issue-detail/parent/sibling-item.tsx index aeb1925..ea3c3e3 100644 --- a/plane-src/apps/web/core/components/issues/issue-detail/parent/sibling-item.tsx +++ b/plane-src/apps/web/core/components/issues/issue-detail/parent/sibling-item.tsx @@ -5,8 +5,6 @@ */ import { observer } from "mobx-react"; -// ui -import { CustomMenu } from "@plane/ui"; // helpers import { generateWorkItemLink } from "@plane/utils"; // hooks @@ -16,12 +14,13 @@ import { useProject } from "@/hooks/store/use-project"; import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier"; type TIssueParentSiblingItem = { + closeDropdown?: () => void; workspaceSlug: string; issueId: string; }; export const IssueParentSiblingItem = observer(function IssueParentSiblingItem(props: TIssueParentSiblingItem) { - const { workspaceSlug, issueId } = props; + const { closeDropdown, workspaceSlug, issueId } = props; // hooks const { getProjectById } = useProject(); const { @@ -43,23 +42,23 @@ export const IssueParentSiblingItem = observer(function IssueParentSiblingItem(p }); return ( - <> - window.open(workItemLink, "_blank", "noopener,noreferrer")} - > -
- {issueDetail.project_id && projectDetails?.identifier && ( - - )} -
-
- + ); }); diff --git a/plane-src/apps/web/core/components/issues/issue-detail/parent/siblings.tsx b/plane-src/apps/web/core/components/issues/issue-detail/parent/siblings.tsx index 7254feb..01a1320 100644 --- a/plane-src/apps/web/core/components/issues/issue-detail/parent/siblings.tsx +++ b/plane-src/apps/web/core/components/issues/issue-detail/parent/siblings.tsx @@ -14,13 +14,14 @@ import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { IssueParentSiblingItem } from "./sibling-item"; export type TIssueParentSiblings = { + closeDropdown?: () => void; workspaceSlug: string; currentIssue: TIssue; parentIssue: TIssue; }; export const IssueParentSiblings = observer(function IssueParentSiblings(props: TIssueParentSiblings) { - const { workspaceSlug, currentIssue, parentIssue } = props; + const { closeDropdown, workspaceSlug, currentIssue, parentIssue } = props; // hooks const { fetchSubIssues, @@ -41,18 +42,23 @@ export const IssueParentSiblings = observer(function IssueParentSiblings(props: return (
{isLoading ? ( -
+
Loading
) : subIssueIds && subIssueIds.length > 0 ? ( subIssueIds.map( (issueId) => currentIssue.id != issueId && ( - + ) ) ) : ( -
+
No sibling work items
)} diff --git a/plane-src/packages/ui/src/dropdowns/action-dropdown.tsx b/plane-src/packages/ui/src/dropdowns/action-dropdown.tsx index 2c5927f..a1449ef 100644 --- a/plane-src/packages/ui/src/dropdowns/action-dropdown.tsx +++ b/plane-src/packages/ui/src/dropdowns/action-dropdown.tsx @@ -16,19 +16,42 @@ import { cn } from "../utils"; import type { TContextMenuItem } from "./context-menu"; export type TActionDropdownTrigger = React.ReactNode; +type TActionDropdownMenuContent = React.ReactNode | ((props: { closeDropdown: () => void }) => React.ReactNode); export interface IActionDropdownProps { button?: TActionDropdownTrigger; + buttonAsChild?: boolean; buttonClassName?: string; className?: string; disabled?: boolean; items: TContextMenuItem[]; + menuContent?: TActionDropdownMenuContent; menuClassName?: string; onOpenChange?: (isOpen: boolean) => void; placement?: TPlacement; portalElement?: Element | null; } +const assignRef = (ref: React.Ref | undefined, value: T) => { + if (!ref) return; + if (typeof ref === "function") { + ref(value); + return; + } + (ref as React.MutableRefObject).current = value; +}; + +const composeEventHandlers = ( + ...handlers: Array<((event: E) => void) | undefined> +) => { + return (event: E) => { + for (const handler of handlers) { + handler?.(event); + if (event.defaultPrevented) break; + } + }; +}; + const renderActionContent = (item: TContextMenuItem) => item.customContent ?? (
@@ -108,14 +131,27 @@ function ActionDropdownItem(props: TActionDropdownItemProps) { } export function ActionDropdown(props: IActionDropdownProps) { - const { button, buttonClassName, className, disabled = false, items, menuClassName, onOpenChange, placement, portalElement } = props; + const { + button, + buttonAsChild = false, + buttonClassName, + className, + disabled = false, + items, + menuContent, + 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 hasMenuContent = menuContent !== undefined && menuContent !== null; + const isDropdownDisabled = disabled || (!hasMenuContent && renderedItems.length === 0); const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: placement ?? "bottom-end", @@ -172,9 +208,13 @@ export function ActionDropdown(props: IActionDropdownProps) { useOutsideClickDetector(dropdownRef, closeDropdown); + const setReferenceNode = React.useCallback((node: HTMLElement | null) => { + setReferenceElement(node); + }, []); + const defaultButton = ( ); + const asChildButton = + buttonAsChild && React.isValidElement(button) && button.type !== React.Fragment + ? React.cloneElement(button, { + ref: (node: HTMLElement | null) => { + setReferenceNode(node); + assignRef((button as React.ReactElement & { ref?: React.Ref }).ref, node); + }, + className: cn((button.props as { className?: string }).className, buttonClassName), + disabled: isDropdownDisabled || (button.props as { disabled?: boolean }).disabled, + onClick: composeEventHandlers( + (button.props as { onClick?: (event: React.MouseEvent) => void }).onClick, + handleTriggerClick + ), + onKeyDown: composeEventHandlers( + (button.props as { onKeyDown?: (event: React.KeyboardEvent) => void }).onKeyDown, + handleKeyDown + ), + type: (button.props as { type?: string }).type ?? "button", + "aria-haspopup": "menu", + "aria-expanded": isOpen, + "aria-label": (button.props as { ["aria-label"]?: string })["aria-label"] ?? "Work item actions", + "data-action-dropdown-trigger": "true", + }) + : null; + const customButton = button ? (
, portalElement ?? document.body @@ -249,7 +322,7 @@ export function ActionDropdown(props: IActionDropdownProps) { return (
- {customButton ?? defaultButton} + {asChildButton ?? customButton ?? defaultButton} {popup}
);