АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: канон составных detail-menu и customButton-trigger
This commit is contained in:
+26
-25
@@ -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}</> : <PlusIcon className="h-4 w-4" />;
|
||||
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: (
|
||||
<div className="flex items-center gap-2">
|
||||
{item.icon(12)}
|
||||
<span>{t(item.i18n_label)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
customButton={customButtonElement}
|
||||
<ActionDropdown
|
||||
items={items}
|
||||
button={customButton ?? <PlusIcon className="h-4 w-4" />}
|
||||
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 (
|
||||
<CustomMenu.MenuItem
|
||||
key={index}
|
||||
onClick={() => {
|
||||
handleOnClick(item.key);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.icon(12)}
|
||||
<span>{t(item.i18n_label)}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
+20
-26
@@ -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: <PlusIcon className="h-3 w-3" />,
|
||||
onClick: handleCreateNew,
|
||||
key: "create-new",
|
||||
title: t("common.create_new"),
|
||||
icon: PlusIcon,
|
||||
action: handleCreateNew,
|
||||
},
|
||||
{
|
||||
i18n_label: "common.add_existing",
|
||||
icon: <WorkItemsIcon className="h-3 w-3" />,
|
||||
onClick: handleAddExisting,
|
||||
key: "add-existing",
|
||||
title: t("common.add_existing"),
|
||||
icon: WorkItemsIcon,
|
||||
action: handleAddExisting,
|
||||
},
|
||||
];
|
||||
|
||||
// button element
|
||||
const customButtonElement = customButton ? <>{customButton}</> : <PlusIcon className="h-4 w-4" />;
|
||||
|
||||
return (
|
||||
<CustomMenu customButton={customButtonElement} placement="bottom-start" disabled={disabled} closeOnSelect>
|
||||
{optionItems.map((item, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={index}
|
||||
onClick={() => {
|
||||
item.onClick();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.icon}
|
||||
<span>{t(item.i18n_label)}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<ActionDropdown
|
||||
items={optionItems}
|
||||
button={customButton ?? <PlusIcon className="h-4 w-4" />}
|
||||
buttonAsChild={!!customButton}
|
||||
buttonClassName={!customButton ? getIconButtonStyling("ghost", "sm") : undefined}
|
||||
placement="bottom-start"
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,17 +9,22 @@ import React from "react";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
type Props = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "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<HTMLButtonElement, Props>(function IssueDetailWidgetButton(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
const { icon, title, disabled = false, compactView = false, className, ...buttonProps } = props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={"secondary"}
|
||||
disabled={disabled}
|
||||
size="xl"
|
||||
@@ -27,11 +32,13 @@ export function IssueDetailWidgetButton(props: Props) {
|
||||
"border-transparent shadow-none focus-visible:outline-none",
|
||||
compactView
|
||||
? "h-10 rounded-[18px] bg-layer-2/80 px-4 backdrop-blur-xl hover:bg-layer-2-active"
|
||||
: "rounded-md"
|
||||
: "rounded-md",
|
||||
className
|
||||
)}
|
||||
{...buttonProps}
|
||||
>
|
||||
{icon && icon}
|
||||
<span className="text-body-xs-medium">{title}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
</div>
|
||||
</ControlLink>
|
||||
|
||||
<CustomMenu ellipsis optionsClassName="p-1.5">
|
||||
<div className="border-b border-strong text-11 font-medium text-secondary">{t("issue.sibling.label")}</div>
|
||||
<ActionDropdown
|
||||
items={[]}
|
||||
buttonClassName={getIconButtonStyling("ghost", "sm")}
|
||||
placement="bottom-end"
|
||||
menuClassName="p-1.5"
|
||||
menuContent={({ closeDropdown }) => (
|
||||
<div className="min-w-[12rem]">
|
||||
<div className="border-b border-strong px-2 pb-2 text-11 font-medium text-secondary">
|
||||
{t("issue.sibling.label")}
|
||||
</div>
|
||||
|
||||
<IssueParentSiblings workspaceSlug={workspaceSlug} currentIssue={issue} parentIssue={parentIssue} />
|
||||
<IssueParentSiblings
|
||||
closeDropdown={closeDropdown}
|
||||
workspaceSlug={workspaceSlug}
|
||||
currentIssue={issue}
|
||||
parentIssue={parentIssue}
|
||||
/>
|
||||
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null })}
|
||||
className="flex items-center gap-2 py-2 text-danger-primary"
|
||||
>
|
||||
<MinusCircle className="h-4 w-4" />
|
||||
<span>{t("issue.remove.parent.label")}</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null });
|
||||
closeDropdown();
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-[0.9rem] px-2 py-2 text-left text-danger-primary transition-colors hover:bg-white/6"
|
||||
>
|
||||
<MinusCircle className="h-4 w-4" />
|
||||
<span>{t("issue.remove.parent.label")}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<CustomMenu.MenuItem
|
||||
key={issueDetail.id}
|
||||
onClick={() => window.open(workItemLink, "_blank", "noopener,noreferrer")}
|
||||
>
|
||||
<div className="flex items-center gap-2 py-0.5">
|
||||
{issueDetail.project_id && projectDetails?.identifier && (
|
||||
<IssueIdentifier
|
||||
projectId={issueDetail.project_id}
|
||||
issueTypeId={issueDetail.type_id}
|
||||
projectIdentifier={projectDetails?.identifier}
|
||||
issueSequenceId={issueDetail.sequence_id}
|
||||
size="xs"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded-[0.9rem] px-2 py-2 text-left text-secondary transition-colors hover:bg-white/6"
|
||||
onClick={() => {
|
||||
closeDropdown?.();
|
||||
window.open(workItemLink, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
>
|
||||
{issueDetail.project_id && projectDetails?.identifier && (
|
||||
<IssueIdentifier
|
||||
projectId={issueDetail.project_id}
|
||||
issueTypeId={issueDetail.type_id}
|
||||
projectIdentifier={projectDetails?.identifier}
|
||||
issueSequenceId={issueDetail.sequence_id}
|
||||
size="xs"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<div className="my-1">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2 px-1 py-1 text-left text-11 whitespace-nowrap text-secondary">
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 text-left text-11 whitespace-nowrap text-secondary">
|
||||
Loading
|
||||
</div>
|
||||
) : subIssueIds && subIssueIds.length > 0 ? (
|
||||
subIssueIds.map(
|
||||
(issueId) =>
|
||||
currentIssue.id != issueId && (
|
||||
<IssueParentSiblingItem key={issueId} workspaceSlug={workspaceSlug} issueId={issueId} />
|
||||
<IssueParentSiblingItem
|
||||
key={issueId}
|
||||
closeDropdown={closeDropdown}
|
||||
workspaceSlug={workspaceSlug}
|
||||
issueId={issueId}
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center gap-2 px-1 py-1 text-left text-11 whitespace-nowrap text-secondary">
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 text-left text-11 whitespace-nowrap text-secondary">
|
||||
No sibling work items
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user