UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: миграция secondary detail action-menu на ActionDropdown
This commit is contained in:
parent
b0173c82e6
commit
c86fc16cdf
|
|
@ -10,12 +10,11 @@ import { MoreHorizontal } from "lucide-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
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 { LinkIcon, GlobeIcon, LockIcon, EditIcon, TrashIcon } from "@plane/propel/icons";
|
||||||
import type { TIssueComment, TCommentsOperations } from "@plane/types";
|
import type { TIssueComment, TCommentsOperations } from "@plane/types";
|
||||||
import type { TContextMenuItem } from "@plane/ui";
|
import type { TContextMenuItem } from "@plane/ui";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { ActionDropdown } from "@plane/ui";
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "@/hooks/store/user";
|
import { useUser } from "@/hooks/store/user";
|
||||||
|
|
||||||
|
|
@ -84,39 +83,11 @@ export const CommentQuickActions = observer(function CommentQuickActions(props:
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu customButton={<IconButton icon={MoreHorizontal} variant="ghost" size="sm" />} closeOnSelect>
|
<ActionDropdown
|
||||||
{MENU_ITEMS.map((item) => {
|
items={MENU_ITEMS}
|
||||||
if (item.shouldRender === false) return null;
|
button={<MoreHorizontal className="size-3.5" />}
|
||||||
|
buttonClassName={getIconButtonStyling("ghost", "sm")}
|
||||||
return (
|
placement="bottom-end"
|
||||||
<CustomMenu.MenuItem
|
/>
|
||||||
key={item.key}
|
|
||||||
onClick={() => item.action()}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2",
|
|
||||||
{
|
|
||||||
"text-placeholder": item.disabled,
|
|
||||||
},
|
|
||||||
item.className
|
|
||||||
)}
|
|
||||||
disabled={item.disabled}
|
|
||||||
>
|
|
||||||
{item.icon && <item.icon className={cn("size-3 shrink-0", item.iconClassName)} />}
|
|
||||||
<div>
|
|
||||||
<h5>{item.title}</h5>
|
|
||||||
{item.description && (
|
|
||||||
<p
|
|
||||||
className={cn("whitespace-pre-line text-tertiary", {
|
|
||||||
"text-placeholder": item.disabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CustomMenu>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
import { TrashIcon } from "@plane/propel/icons";
|
import { TrashIcon } from "@plane/propel/icons";
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import type { TIssueServiceType } from "@plane/types";
|
import type { TIssueServiceType } from "@plane/types";
|
||||||
import { EIssueServiceType } from "@plane/types";
|
import { EIssueServiceType } from "@plane/types";
|
||||||
// ui
|
// 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";
|
import { convertBytesToSize, getFileExtension, getFileName, getFileURL, renderFormattedDate } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
//
|
//
|
||||||
|
|
@ -46,19 +48,37 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
const fileExtension = getFileExtension(attachment?.attributes.name ?? "");
|
const fileExtension = getFileExtension(attachment?.attributes.name ?? "");
|
||||||
const fileIcon = getFileIcon(fileExtension, 18);
|
const fileIcon = getFileIcon(fileExtension, 18);
|
||||||
const fileURL = getFileURL(attachment?.asset_url ?? "");
|
const fileURL = getFileURL(attachment?.asset_url ?? "");
|
||||||
|
const menuItems: TContextMenuItem[] = [
|
||||||
|
{
|
||||||
|
key: "delete",
|
||||||
|
action: () => {
|
||||||
|
toggleDeleteAttachmentModal(attachmentId);
|
||||||
|
},
|
||||||
|
title: t("common.actions.delete"),
|
||||||
|
icon: TrashIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
// hooks
|
// hooks
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
if (!attachment) return <></>;
|
if (!attachment) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<button
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
window.open(fileURL, "_blank");
|
window.open(fileURL, "_blank");
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
window.open(fileURL, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="group flex h-11 items-center justify-between gap-3 pr-2 pl-9 hover:bg-surface-2">
|
<div className="group flex h-11 items-center justify-between gap-3 pr-2 pl-9 hover:bg-surface-2">
|
||||||
<div className="flex items-center gap-3 truncate text-13">
|
<div className="flex items-center gap-3 truncate text-13">
|
||||||
|
|
@ -86,21 +106,14 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CustomMenu ellipsis closeOnSelect placement="bottom-end" disabled={disabled}>
|
<ActionDropdown
|
||||||
<CustomMenu.MenuItem
|
items={menuItems}
|
||||||
onClick={() => {
|
buttonClassName={getIconButtonStyling("ghost", "sm")}
|
||||||
toggleDeleteAttachmentModal(attachmentId);
|
placement="bottom-end"
|
||||||
}}
|
disabled={!!disabled}
|
||||||
>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
</div>
|
||||||
<TrashIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
|
||||||
<span>{t("common.actions.delete")}</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Link as Loader } from "lucide-react";
|
import { Link as Loader } from "lucide-react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
import { LinkIcon, EditIcon, TrashIcon, CloseIcon, ChevronRightIcon } from "@plane/propel/icons";
|
import { LinkIcon, EditIcon, TrashIcon, CloseIcon, ChevronRightIcon } from "@plane/propel/icons";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import type { TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types";
|
import type { TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types";
|
||||||
import { EIssueServiceType, EIssuesStoreType } 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";
|
import { cn, generateWorkItemLink } from "@plane/utils";
|
||||||
// helpers
|
// helpers
|
||||||
import { useSubIssueOperations } from "@/components/issues/issue-detail-widgets/sub-issues/helper";
|
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,
|
projectIdentifier: projectDetail?.identifier,
|
||||||
sequenceId: issue?.sequence_id,
|
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 (
|
return (
|
||||||
<div key={issueId}>
|
<div key={issueId}>
|
||||||
|
|
@ -189,62 +230,11 @@ export const SubIssuesListItem = observer(function SubIssuesListItem(props: Prop
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0 text-13">
|
<div className="flex-shrink-0 text-13">
|
||||||
<CustomMenu placement="bottom-end" ellipsis>
|
<ActionDropdown
|
||||||
{canEdit && (
|
items={menuItems}
|
||||||
<CustomMenu.MenuItem
|
buttonClassName={getIconButtonStyling("ghost", "sm")}
|
||||||
onClick={() => {
|
placement="bottom-end"
|
||||||
handleIssueCrudState("update", parentIssueId, { ...issue });
|
/>
|
||||||
toggleCreateIssueModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<EditIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
|
||||||
<span>{t("issue.edit")}</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
subIssueOperations.copyLink(workItemLink);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<LinkIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
|
||||||
<span>{t("issue.copy_link")}</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
|
|
||||||
{canEdit && (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
if (issue.project_id)
|
|
||||||
subIssueOperations.removeSubIssue(workspaceSlug, issue.project_id, parentIssueId, issue.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CloseIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
|
||||||
{issueServiceType === EIssueServiceType.ISSUES
|
|
||||||
? t("issue.remove.parent.label")
|
|
||||||
: t("issue.remove.label")}
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canEdit && (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
handleIssueCrudState("delete", parentIssueId, issue);
|
|
||||||
toggleDeleteIssueModal(issue.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<TrashIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
|
||||||
<span>{t("issue.delete.label")}</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { MoreHorizontal } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
import { LinkIcon, CopyIcon, EditIcon, TrashIcon } from "@plane/propel/icons";
|
import { LinkIcon, CopyIcon, EditIcon, TrashIcon } from "@plane/propel/icons";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import type { TIssueServiceType } from "@plane/types";
|
import type { TIssueServiceType } from "@plane/types";
|
||||||
import { EIssueServiceType } from "@plane/types";
|
import { EIssueServiceType } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "@plane/ui";
|
import type { TContextMenuItem } from "@plane/ui";
|
||||||
|
import { ActionDropdown } from "@plane/ui";
|
||||||
import { calculateTimeAgo, copyTextToClipboard } from "@plane/utils";
|
import { calculateTimeAgo, copyTextToClipboard } from "@plane/utils";
|
||||||
// helpers
|
// helpers
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -45,6 +48,26 @@ export const IssueLinkItem = observer(function IssueLinkItem(props: TIssueLinkIt
|
||||||
// const Icon = getIconForLink(linkDetail.url);
|
// const Icon = getIconForLink(linkDetail.url);
|
||||||
const faviconUrl: string | undefined = linkDetail.metadata?.favicon;
|
const faviconUrl: string | undefined = linkDetail.metadata?.favicon;
|
||||||
const linkTitle: string | undefined = linkDetail.metadata?.title;
|
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) => {
|
const toggleIssueLinkModal = (modalToggle: boolean) => {
|
||||||
toggleIssueLinkModalStore(modalToggle);
|
toggleIssueLinkModalStore(modalToggle);
|
||||||
|
|
@ -95,32 +118,13 @@ export const IssueLinkItem = observer(function IssueLinkItem(props: TIssueLinkIt
|
||||||
>
|
>
|
||||||
<CopyIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
<CopyIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||||
</span>
|
</span>
|
||||||
<CustomMenu
|
<ActionDropdown
|
||||||
ellipsis
|
items={menuItems}
|
||||||
buttonClassName="text-placeholder group-hover:text-secondary"
|
button={<MoreHorizontal className="size-3.5" />}
|
||||||
|
buttonClassName={getIconButtonStyling("ghost", "sm") + " text-placeholder group-hover:text-secondary"}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
closeOnSelect
|
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
>
|
/>
|
||||||
<CustomMenu.MenuItem
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => {
|
|
||||||
toggleIssueLinkModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditIcon className="h-3 w-3 stroke-[1.5] text-secondary" />
|
|
||||||
{t("common.actions.edit")}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => {
|
|
||||||
linkOperations.remove(linkDetail.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrashIcon className="h-3 w-3" />
|
|
||||||
{t("common.actions.delete")}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,16 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
import { LinkIcon, EditIcon, TrashIcon, CloseIcon } from "@plane/propel/icons";
|
import { LinkIcon, EditIcon, TrashIcon, CloseIcon } from "@plane/propel/icons";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import type { TIssue, TIssueServiceType } from "@plane/types";
|
import type { TIssue, TIssueServiceType } from "@plane/types";
|
||||||
import { EIssueServiceType } 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";
|
import { generateWorkItemLink } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||||
|
|
@ -59,8 +60,8 @@ export const RelationIssueListItem = observer(function RelationIssueListItem(pro
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
removeRelation,
|
removeRelation,
|
||||||
toggleCreateIssueModal,
|
|
||||||
toggleDeleteIssueModal,
|
toggleDeleteIssueModal,
|
||||||
|
toggleCreateIssueModal,
|
||||||
} = useIssueDetail(issueServiceType);
|
} = useIssueDetail(issueServiceType);
|
||||||
const project = useProject();
|
const project = useProject();
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
@ -92,32 +93,42 @@ export const RelationIssueListItem = observer(function RelationIssueListItem(pro
|
||||||
handleRedirection(workspaceSlug, issue, isMobile);
|
handleRedirection(workspaceSlug, issue, isMobile);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditIssue = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
const menuItems: TContextMenuItem[] = [
|
||||||
e.stopPropagation();
|
{
|
||||||
e.preventDefault();
|
key: "edit",
|
||||||
|
action: () => {
|
||||||
handleIssueCrudState("update", relationIssueId, { ...issue });
|
handleIssueCrudState("update", relationIssueId, { ...issue });
|
||||||
toggleCreateIssueModal(true);
|
toggleCreateIssueModal(true);
|
||||||
};
|
},
|
||||||
|
title: t("common.actions.edit"),
|
||||||
const handleDeleteIssue = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
icon: EditIcon,
|
||||||
e.stopPropagation();
|
shouldRender: !disabled,
|
||||||
e.preventDefault();
|
},
|
||||||
|
{
|
||||||
|
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);
|
handleIssueCrudState("delete", relationIssueId, issue);
|
||||||
toggleDeleteIssueModal(relationIssueId);
|
toggleDeleteIssueModal(relationIssueId);
|
||||||
handleIssueCrudState("removeRelation", issueId, issue, relationKey, relationIssueId);
|
handleIssueCrudState("removeRelation", issueId, issue, relationKey, relationIssueId);
|
||||||
};
|
},
|
||||||
|
title: t("common.actions.delete"),
|
||||||
const handleCopyIssueLink = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
icon: TrashIcon,
|
||||||
e.stopPropagation();
|
shouldRender: !disabled,
|
||||||
e.preventDefault();
|
},
|
||||||
issueOperations.copyLink(workItemLink);
|
];
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveRelation = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
removeRelation(workspaceSlug, projectId, issueId, relationKey, relationIssueId);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={relationIssueId}>
|
<div key={relationIssueId}>
|
||||||
|
|
@ -164,41 +175,11 @@ export const RelationIssueListItem = observer(function RelationIssueListItem(pro
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 pl-2 text-13">
|
<div className="flex-shrink-0 pl-2 text-13">
|
||||||
<CustomMenu placement="bottom-end" ellipsis>
|
<ActionDropdown
|
||||||
{!disabled && (
|
items={menuItems}
|
||||||
<CustomMenu.MenuItem onClick={handleEditIssue}>
|
buttonClassName={getIconButtonStyling("ghost", "sm")}
|
||||||
<div className="flex items-center gap-2">
|
placement="bottom-end"
|
||||||
<EditIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
/>
|
||||||
<span>{t("common.actions.edit")}</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CustomMenu.MenuItem onClick={handleCopyIssueLink}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<LinkIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
|
||||||
<span>{t("common.actions.copy_link")}</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
|
|
||||||
{!disabled && (
|
|
||||||
<CustomMenu.MenuItem onClick={handleRemoveRelation}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CloseIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
|
||||||
<span>{t("common.actions.remove_relation")}</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!disabled && (
|
|
||||||
<CustomMenu.MenuItem onClick={handleDeleteIssue}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<TrashIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
|
||||||
<span>{t("common.actions.delete")}</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export interface IActionDropdownProps {
|
||||||
button?: TActionDropdownTrigger;
|
button?: TActionDropdownTrigger;
|
||||||
buttonClassName?: string;
|
buttonClassName?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
disabled?: boolean;
|
||||||
items: TContextMenuItem[];
|
items: TContextMenuItem[];
|
||||||
menuClassName?: string;
|
menuClassName?: string;
|
||||||
onOpenChange?: (isOpen: boolean) => void;
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
|
|
@ -107,13 +108,14 @@ function ActionDropdownItem(props: TActionDropdownItemProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActionDropdown(props: IActionDropdownProps) {
|
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<HTMLDivElement | null>(null);
|
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const [referenceElement, setReferenceElement] = React.useState<HTMLElement | null>(null);
|
const [referenceElement, setReferenceElement] = React.useState<HTMLElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = React.useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = React.useState<HTMLDivElement | null>(null);
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
|
||||||
const renderedItems = items.filter((item) => item.shouldRender !== false);
|
const renderedItems = items.filter((item) => item.shouldRender !== false);
|
||||||
|
const isDropdownDisabled = disabled || renderedItems.length === 0;
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: placement ?? "bottom-end",
|
placement: placement ?? "bottom-end",
|
||||||
|
|
@ -141,10 +143,10 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const openDropdown = React.useCallback(() => {
|
const openDropdown = React.useCallback(() => {
|
||||||
if (renderedItems.length === 0) return;
|
if (isDropdownDisabled) return;
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
onOpenChange?.(true);
|
onOpenChange?.(true);
|
||||||
}, [onOpenChange, renderedItems.length]);
|
}, [isDropdownDisabled, onOpenChange]);
|
||||||
|
|
||||||
const closeDropdown = React.useCallback(() => {
|
const closeDropdown = React.useCallback(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
|
|
@ -156,7 +158,7 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (renderedItems.length === 0) return;
|
if (isDropdownDisabled) return;
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
|
|
@ -177,14 +179,14 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||||
className={cn(
|
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",
|
"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-not-allowed": isDropdownDisabled,
|
||||||
"cursor-pointer": renderedItems.length > 0,
|
"cursor-pointer": !isDropdownDisabled,
|
||||||
},
|
},
|
||||||
buttonClassName
|
buttonClassName
|
||||||
)}
|
)}
|
||||||
onClick={handleTriggerClick}
|
onClick={handleTriggerClick}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
disabled={renderedItems.length === 0}
|
disabled={isDropdownDisabled}
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
aria-label="Work item actions"
|
aria-label="Work item actions"
|
||||||
|
|
@ -201,14 +203,14 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||||
className={cn(
|
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",
|
"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-not-allowed": isDropdownDisabled,
|
||||||
"cursor-pointer": renderedItems.length > 0,
|
"cursor-pointer": !isDropdownDisabled,
|
||||||
},
|
},
|
||||||
buttonClassName
|
buttonClassName
|
||||||
)}
|
)}
|
||||||
onClick={handleTriggerClick}
|
onClick={handleTriggerClick}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
disabled={renderedItems.length === 0}
|
disabled={isDropdownDisabled}
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
aria-label="Work item actions"
|
aria-label="Work item actions"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue