mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(frontend): context menu right click new builder (#11703)
## Changes 🏗️ <img width="250" height="504" alt="Screenshot 2026-01-06 at 17 53 26" src="https://github.com/user-attachments/assets/52013448-f49c-46b6-b86a-39f98270cbc3" /> <img width="300" height="544" alt="Screenshot 2026-01-06 at 17 53 29" src="https://github.com/user-attachments/assets/e6334034-68e4-4346-9092-3774ab3e8445" /> On the **New Builder**: - right-click on a node menu make it show the context menu - use the same menu for right-click and when clicking on `...` ## Checklist 📋 ### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Run locally and test the above <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a custom right-click context menu for nodes with Copy, Open agent (when available), and Delete actions; browser default menu is suppressed while preserving zoom/drag/wiring. * Introduced reusable SecondaryMenu primitives for context and dropdown menus. * **Documentation** * Added Storybook examples demonstrating the context menu and dropdown menu usage. * **Style** * Updated menu styling and icons with improved consistency and dark-mode support. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -97,6 +97,9 @@ export const Flow = () => {
|
||||
onConnect={onConnect}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onNodeContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
maxZoom={2}
|
||||
minZoom={0.1}
|
||||
onDragOver={onDragOver}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import React from "react";
|
||||
import { Node as XYNode, NodeProps } from "@xyflow/react";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { BlockUIType } from "../../../types";
|
||||
import { StickyNoteBlock } from "./components/StickyNoteBlock";
|
||||
import { BlockInfoCategoriesItem } from "@/app/api/__generated__/models/blockInfoCategoriesItem";
|
||||
import { BlockCost } from "@/app/api/__generated__/models/blockCost";
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { BlockCost } from "@/app/api/__generated__/models/blockCost";
|
||||
import { BlockInfoCategoriesItem } from "@/app/api/__generated__/models/blockInfoCategoriesItem";
|
||||
import { NodeExecutionResult } from "@/app/api/__generated__/models/nodeExecutionResult";
|
||||
import { NodeContainer } from "./components/NodeContainer";
|
||||
import { NodeHeader } from "./components/NodeHeader";
|
||||
import { FormCreator } from "../FormCreator";
|
||||
import { preprocessInputSchema } from "@/components/renderers/InputRenderer/utils/input-schema-pre-processor";
|
||||
import { OutputHandler } from "../OutputHandler";
|
||||
import { NodeAdvancedToggle } from "./components/NodeAdvancedToggle";
|
||||
import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput";
|
||||
import { NodeExecutionBadge } from "./components/NodeExecutionBadge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
|
||||
import { AyrshareConnectButton } from "./components/AyrshareConnectButton";
|
||||
import { NodeModelMetadata } from "@/app/api/__generated__/models/nodeModelMetadata";
|
||||
import { preprocessInputSchema } from "@/components/renderers/InputRenderer/utils/input-schema-pre-processor";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { NodeProps, Node as XYNode } from "@xyflow/react";
|
||||
import React from "react";
|
||||
import { BlockUIType } from "../../../types";
|
||||
import { FormCreator } from "../FormCreator";
|
||||
import { OutputHandler } from "../OutputHandler";
|
||||
import { AyrshareConnectButton } from "./components/AyrshareConnectButton";
|
||||
import { NodeAdvancedToggle } from "./components/NodeAdvancedToggle";
|
||||
import { NodeContainer } from "./components/NodeContainer";
|
||||
import { NodeExecutionBadge } from "./components/NodeExecutionBadge";
|
||||
import { NodeHeader } from "./components/NodeHeader";
|
||||
import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput";
|
||||
import { NodeRightClickMenu } from "./components/NodeRightClickMenu";
|
||||
import { StickyNoteBlock } from "./components/StickyNoteBlock";
|
||||
import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
|
||||
|
||||
export type CustomNodeData = {
|
||||
hardcodedValues: {
|
||||
@@ -88,7 +89,7 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||
|
||||
// Currently all blockTypes design are similar - that's why i am using the same component for all of them
|
||||
// If in future - if we need some drastic change in some blockTypes design - we can create separate components for them
|
||||
return (
|
||||
const node = (
|
||||
<NodeContainer selected={selected} nodeId={nodeId} hasErrors={hasErrors}>
|
||||
<div className="rounded-xlarge bg-white">
|
||||
<NodeHeader data={data} nodeId={nodeId} />
|
||||
@@ -117,6 +118,15 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
|
||||
<NodeExecutionBadge nodeId={nodeId} />
|
||||
</NodeContainer>
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeRightClickMenu
|
||||
nodeId={nodeId}
|
||||
subGraphID={data.hardcodedValues?.graph_id}
|
||||
>
|
||||
{node}
|
||||
</NodeRightClickMenu>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import { useCopyPasteStore } from "@/app/(platform)/build/stores/copyPasteStore";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/molecules/DropdownMenu/DropdownMenu";
|
||||
import { DotsThreeOutlineVerticalIcon } from "@phosphor-icons/react";
|
||||
import { Copy, Trash2, ExternalLink } from "lucide-react";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { useCopyPasteStore } from "@/app/(platform)/build/stores/copyPasteStore";
|
||||
import {
|
||||
SecondaryDropdownMenuContent,
|
||||
SecondaryDropdownMenuItem,
|
||||
SecondaryDropdownMenuSeparator,
|
||||
} from "@/components/molecules/SecondaryMenu/SecondaryMenu";
|
||||
import {
|
||||
ArrowSquareOutIcon,
|
||||
CopyIcon,
|
||||
DotsThreeOutlineVerticalIcon,
|
||||
TrashIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
|
||||
export const NodeContextMenu = ({
|
||||
nodeId,
|
||||
subGraphID,
|
||||
}: {
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
subGraphID?: string;
|
||||
}) => {
|
||||
};
|
||||
|
||||
export const NodeContextMenu = ({ nodeId, subGraphID }: Props) => {
|
||||
const { deleteElements } = useReactFlow();
|
||||
|
||||
const handleCopy = () => {
|
||||
function handleCopy() {
|
||||
useNodeStore.setState((state) => ({
|
||||
nodes: state.nodes.map((node) => ({
|
||||
...node,
|
||||
@@ -30,47 +35,47 @@ export const NodeContextMenu = ({
|
||||
|
||||
useCopyPasteStore.getState().copySelectedNodes();
|
||||
useCopyPasteStore.getState().pasteNodes();
|
||||
};
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
function handleDelete() {
|
||||
deleteElements({ nodes: [{ id: nodeId }] });
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="py-2">
|
||||
<DotsThreeOutlineVerticalIcon size={16} weight="fill" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="right"
|
||||
align="start"
|
||||
className="rounded-xlarge"
|
||||
>
|
||||
<DropdownMenuItem onClick={handleCopy} className="hover:rounded-xlarge">
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy Node
|
||||
</DropdownMenuItem>
|
||||
<SecondaryDropdownMenuContent side="right" align="start">
|
||||
<SecondaryDropdownMenuItem onClick={handleCopy}>
|
||||
<CopyIcon size={20} className="mr-2 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Copy</span>
|
||||
</SecondaryDropdownMenuItem>
|
||||
<SecondaryDropdownMenuSeparator />
|
||||
|
||||
{subGraphID && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => window.open(`/build?flowID=${subGraphID}`)}
|
||||
className="hover:rounded-xlarge"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
Open Agent
|
||||
</DropdownMenuItem>
|
||||
<>
|
||||
<SecondaryDropdownMenuItem
|
||||
onClick={() => window.open(`/build?flowID=${subGraphID}`)}
|
||||
>
|
||||
<ArrowSquareOutIcon
|
||||
size={20}
|
||||
className="mr-2 dark:text-gray-100"
|
||||
/>
|
||||
<span className="dark:text-gray-100">Open agent</span>
|
||||
</SecondaryDropdownMenuItem>
|
||||
<SecondaryDropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={handleDelete}
|
||||
className="text-red-600 hover:rounded-xlarge"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
<SecondaryDropdownMenuItem variant="destructive" onClick={handleDelete}>
|
||||
<TrashIcon
|
||||
size={20}
|
||||
className="mr-2 text-red-500 dark:text-red-400"
|
||||
/>
|
||||
<span className="dark:text-red-400">Delete</span>
|
||||
</SecondaryDropdownMenuItem>
|
||||
</SecondaryDropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { beautifyString, cn } from "@/lib/utils";
|
||||
import { NodeCost } from "./NodeCost";
|
||||
import { NodeBadges } from "./NodeBadges";
|
||||
import { NodeContextMenu } from "./NodeContextMenu";
|
||||
import { CustomNodeData } from "../CustomNode";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import { useState } from "react";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { beautifyString, cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { CustomNodeData } from "../CustomNode";
|
||||
import { NodeBadges } from "./NodeBadges";
|
||||
import { NodeContextMenu } from "./NodeContextMenu";
|
||||
import { NodeCost } from "./NodeCost";
|
||||
|
||||
export const NodeHeader = ({
|
||||
data,
|
||||
nodeId,
|
||||
}: {
|
||||
type Props = {
|
||||
data: CustomNodeData;
|
||||
nodeId: string;
|
||||
}) => {
|
||||
};
|
||||
|
||||
export const NodeHeader = ({ data, nodeId }: Props) => {
|
||||
const updateNodeData = useNodeStore((state) => state.updateNodeData);
|
||||
const title = (data.metadata?.customized_name as string) || data.title;
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useCopyPasteStore } from "@/app/(platform)/build/stores/copyPasteStore";
|
||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||
import {
|
||||
SecondaryMenuContent,
|
||||
SecondaryMenuItem,
|
||||
SecondaryMenuSeparator,
|
||||
} from "@/components/molecules/SecondaryMenu/SecondaryMenu";
|
||||
import { ArrowSquareOutIcon, CopyIcon, TrashIcon } from "@phosphor-icons/react";
|
||||
import * as ContextMenu from "@radix-ui/react-context-menu";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { CustomNode } from "../CustomNode";
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
subGraphID?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const DOUBLE_CLICK_TIMEOUT = 300;
|
||||
|
||||
export function NodeRightClickMenu({ nodeId, subGraphID, children }: Props) {
|
||||
const { deleteElements } = useReactFlow<CustomNode>();
|
||||
const lastRightClickTime = useRef<number>(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function copyNode() {
|
||||
useNodeStore.setState((state) => ({
|
||||
nodes: state.nodes.map((node) => ({
|
||||
...node,
|
||||
selected: node.id === nodeId,
|
||||
})),
|
||||
}));
|
||||
|
||||
useCopyPasteStore.getState().copySelectedNodes();
|
||||
useCopyPasteStore.getState().pasteNodes();
|
||||
}
|
||||
|
||||
function deleteNode() {
|
||||
deleteElements({ nodes: [{ id: nodeId }] });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
const now = Date.now();
|
||||
const timeSinceLastClick = now - lastRightClickTime.current;
|
||||
|
||||
if (timeSinceLastClick < DOUBLE_CLICK_TIMEOUT) {
|
||||
e.stopImmediatePropagation();
|
||||
lastRightClickTime.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
lastRightClickTime.current = now;
|
||||
}
|
||||
|
||||
container.addEventListener("contextmenu", handleContextMenu, true);
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("contextmenu", handleContextMenu, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger asChild>
|
||||
<div ref={containerRef}>{children}</div>
|
||||
</ContextMenu.Trigger>
|
||||
<SecondaryMenuContent>
|
||||
<SecondaryMenuItem onSelect={copyNode}>
|
||||
<CopyIcon size={20} className="mr-2 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Copy</span>
|
||||
</SecondaryMenuItem>
|
||||
<SecondaryMenuSeparator />
|
||||
|
||||
{subGraphID && (
|
||||
<>
|
||||
<SecondaryMenuItem
|
||||
onClick={() => window.open(`/build?flowID=${subGraphID}`)}
|
||||
>
|
||||
<ArrowSquareOutIcon
|
||||
size={20}
|
||||
className="mr-2 dark:text-gray-100"
|
||||
/>
|
||||
<span className="dark:text-gray-100">Open agent</span>
|
||||
</SecondaryMenuItem>
|
||||
<SecondaryMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<SecondaryMenuItem variant="destructive" onSelect={deleteNode}>
|
||||
<TrashIcon
|
||||
size={20}
|
||||
className="mr-2 text-red-500 dark:text-red-400"
|
||||
/>
|
||||
<span className="dark:text-red-400">Delete</span>
|
||||
</SecondaryMenuItem>
|
||||
</SecondaryMenuContent>
|
||||
</ContextMenu.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/molecules/DropdownMenu/DropdownMenu";
|
||||
import {
|
||||
ArrowSquareOutIcon,
|
||||
CopyIcon,
|
||||
DotsThreeOutlineVerticalIcon,
|
||||
TrashIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import * as ContextMenu from "@radix-ui/react-context-menu";
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import {
|
||||
SecondaryDropdownMenuContent,
|
||||
SecondaryDropdownMenuItem,
|
||||
SecondaryDropdownMenuSeparator,
|
||||
SecondaryMenuContent,
|
||||
SecondaryMenuItem,
|
||||
SecondaryMenuSeparator,
|
||||
} from "./SecondaryMenu";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Molecules/SecondaryMenu",
|
||||
component: SecondaryMenuContent,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SecondaryMenuContent>;
|
||||
|
||||
export const ContextMenuExample: Story = {
|
||||
render: () => (
|
||||
<div className="flex h-96 items-center justify-center">
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger asChild>
|
||||
<div className="flex h-32 w-64 cursor-pointer items-center justify-center rounded-lg border border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-800">
|
||||
Right-click me
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
<SecondaryMenuContent>
|
||||
<SecondaryMenuItem onSelect={() => alert("Copy")}>
|
||||
<CopyIcon size={20} className="mr-2 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Copy</span>
|
||||
</SecondaryMenuItem>
|
||||
<SecondaryMenuItem onSelect={() => alert("Open agent")}>
|
||||
<ArrowSquareOutIcon size={20} className="mr-2 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Open agent</span>
|
||||
</SecondaryMenuItem>
|
||||
<SecondaryMenuSeparator />
|
||||
<SecondaryMenuItem
|
||||
variant="destructive"
|
||||
onSelect={() => alert("Delete")}
|
||||
>
|
||||
<TrashIcon
|
||||
size={20}
|
||||
className="mr-2 text-red-500 dark:text-red-400"
|
||||
/>
|
||||
<span className="dark:text-red-400">Delete</span>
|
||||
</SecondaryMenuItem>
|
||||
</SecondaryMenuContent>
|
||||
</ContextMenu.Root>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const DropdownMenuExample: Story = {
|
||||
render: () => (
|
||||
<div className="flex h-96 items-center justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
<DotsThreeOutlineVerticalIcon size={16} weight="fill" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<SecondaryDropdownMenuContent side="right" align="start">
|
||||
<SecondaryDropdownMenuItem onClick={() => alert("Copy")}>
|
||||
<CopyIcon size={20} className="mr-2 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Copy</span>
|
||||
</SecondaryDropdownMenuItem>
|
||||
<SecondaryDropdownMenuItem onClick={() => alert("Open agent")}>
|
||||
<ArrowSquareOutIcon size={20} className="mr-2 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Open agent</span>
|
||||
</SecondaryDropdownMenuItem>
|
||||
<SecondaryDropdownMenuSeparator />
|
||||
<SecondaryDropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => alert("Delete")}
|
||||
>
|
||||
<TrashIcon
|
||||
size={20}
|
||||
className="mr-2 text-red-500 dark:text-red-400"
|
||||
/>
|
||||
<span className="dark:text-red-400">Delete</span>
|
||||
</SecondaryDropdownMenuItem>
|
||||
</SecondaryDropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as ContextMenu from "@radix-ui/react-context-menu";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import React from "react";
|
||||
|
||||
const secondaryMenuContentClassName =
|
||||
"z-10 rounded-xl border bg-white p-1 shadow-md dark:bg-gray-800";
|
||||
|
||||
const secondaryMenuItemClassName =
|
||||
"flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700";
|
||||
|
||||
const secondaryMenuSeparatorClassName =
|
||||
"my-1 h-px bg-gray-300 dark:bg-gray-600";
|
||||
|
||||
export const SecondaryMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenu.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenu.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenu.Content
|
||||
ref={ref}
|
||||
className={cn(secondaryMenuContentClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SecondaryMenuContent.displayName = "SecondaryMenuContent";
|
||||
|
||||
export const SecondaryMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenu.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenu.Item> & {
|
||||
variant?: "default" | "destructive";
|
||||
}
|
||||
>(({ className, variant = "default", ...props }, ref) => (
|
||||
<ContextMenu.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
secondaryMenuItemClassName,
|
||||
variant === "destructive" &&
|
||||
"text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SecondaryMenuItem.displayName = "SecondaryMenuItem";
|
||||
|
||||
export const SecondaryMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof ContextMenu.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof ContextMenu.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ContextMenu.Separator
|
||||
ref={ref}
|
||||
className={cn(secondaryMenuSeparatorClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SecondaryMenuSeparator.displayName = "SecondaryMenuSeparator";
|
||||
|
||||
export const SecondaryDropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(secondaryMenuContentClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
SecondaryDropdownMenuContent.displayName = "SecondaryDropdownMenuContent";
|
||||
|
||||
export const SecondaryDropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
variant?: "default" | "destructive";
|
||||
}
|
||||
>(({ className, variant = "default", ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
secondaryMenuItemClassName,
|
||||
variant === "destructive" &&
|
||||
"text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SecondaryDropdownMenuItem.displayName = "SecondaryDropdownMenuItem";
|
||||
|
||||
export const SecondaryDropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn(secondaryMenuSeparatorClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SecondaryDropdownMenuSeparator.displayName = "SecondaryDropdownMenuSeparator";
|
||||
Reference in New Issue
Block a user