mirror of
https://github.com/reconurge/flowsint.git
synced 2026-03-11 17:34:31 -05:00
feat: social accounts
This commit is contained in:
@@ -7,7 +7,6 @@ const DashboardPage = async ({
|
||||
params: Promise<{ investigation_id: string }>
|
||||
}) => {
|
||||
const { investigation_id } = await (params)
|
||||
console.log(investigation_id)
|
||||
const { nodes, edges } = await getInvestigationData(investigation_id)
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function CustomEdge({ id, label, confidence_level, sourceX, sourc
|
||||
<BaseEdge id={id} path={edgePath} />
|
||||
<EdgeLabelRenderer>
|
||||
{settings.showEdgeLabel &&
|
||||
<Badge color={label === "relation" ? 'orange' : "blue"} style={{
|
||||
<Badge size={"1"} color={label === "relation" ? 'orange' : "blue"} style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
pointerEvents: 'all',
|
||||
|
||||
@@ -20,12 +20,13 @@ import PhoneNode from './nodes/phone';
|
||||
import CustomEdge from './custom-edge';
|
||||
import IpNode from './nodes/ip_address';
|
||||
import EmailNode from './nodes/email';
|
||||
import { AlignCenterHorizontal, AlignCenterVertical, MaximizeIcon, ZoomInIcon, ZoomOutIcon } from 'lucide-react';
|
||||
import SocialNode from './nodes/social'
|
||||
import { AlignCenterHorizontal, AlignCenterVertical, MaximizeIcon, RotateCcwIcon, ZoomInIcon, ZoomOutIcon } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import NewActions from './new-actions';
|
||||
import { IconButton, Tooltip, Spinner } from '@radix-ui/themes';
|
||||
|
||||
const nodeTypes = { individual: IndividualNode, phone: PhoneNode, ip: IpNode, email: EmailNode };
|
||||
const nodeTypes = { individual: IndividualNode, phone: PhoneNode, ip: IpNode, email: EmailNode, social: SocialNode };
|
||||
const edgeTypes = {
|
||||
'custom': CustomEdge,
|
||||
};
|
||||
@@ -100,6 +101,9 @@ const LayoutFlow = ({ initialNodes, initialEdges, theme }: { initialNodes: any,
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
proOptions={{
|
||||
hideAttribution: true
|
||||
}}
|
||||
nodeTypes={nodeTypes}
|
||||
// @ts-ignore
|
||||
edgeTypes={edgeTypes}
|
||||
@@ -117,6 +121,11 @@ const LayoutFlow = ({ initialNodes, initialEdges, theme }: { initialNodes: any,
|
||||
</Tooltip>
|
||||
</Panel>
|
||||
<Panel position="top-right" className='flex items-center gap-1'>
|
||||
<Tooltip content="Reload schema">
|
||||
<IconButton onClick={() => window.location.reload()} variant="soft">
|
||||
<RotateCcwIcon className='h-4 w-4' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<NewActions addNodes={addNodes} />
|
||||
</Panel>
|
||||
<Panel position="bottom-left" className='flex flex-col items-center gap-1'>
|
||||
|
||||
@@ -1,31 +1,47 @@
|
||||
"use client"
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Card, Box, Text, ContextMenu, Badge, Flex, Inset } from '@radix-ui/themes';
|
||||
import { Card, Box, Text, ContextMenu, Badge, Flex, Inset, Tooltip, Avatar } from '@radix-ui/themes';
|
||||
import { NodeProvider, useNodeContext } from './node-context';
|
||||
import { AtSignIcon } from 'lucide-react';
|
||||
import { cn } from '@/utils';
|
||||
import { useInvestigationContext } from '../investigation-provider';
|
||||
|
||||
function EmailNode({ data }: any) {
|
||||
const { handleDeleteNode } = useNodeContext()
|
||||
const { handleDeleteNode, loading } = useNodeContext()
|
||||
const { settings } = useInvestigationContext()
|
||||
|
||||
return (
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<Box>
|
||||
<Card>
|
||||
<Inset>
|
||||
<Flex className='items-center p-0'>
|
||||
<Badge className='!h-[24px] !rounded-r-none'>
|
||||
<AtSignIcon className='h-3 w-3' />
|
||||
</Badge>
|
||||
<Box className='p-1'>
|
||||
<Text as="div" size="1" weight="regular">
|
||||
{data.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Inset>
|
||||
</Card>
|
||||
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
|
||||
{settings.showNodeLabel ?
|
||||
<Card>
|
||||
<Inset>
|
||||
<Flex className='items-center p-0'>
|
||||
<Badge className='!h-[24px] !rounded-r-none'>
|
||||
<AtSignIcon className='h-3 w-3' />
|
||||
</Badge>
|
||||
<Box className='p-1'>
|
||||
<Text as="div" size="1" weight="regular">
|
||||
{data.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Inset>
|
||||
</Card>
|
||||
:
|
||||
<Tooltip content={data.username || data.profile_url}>
|
||||
<button className='!rounded-full border-transparent'>
|
||||
<Avatar
|
||||
size="1"
|
||||
src={data?.image_url}
|
||||
radius="full"
|
||||
/* @ts-ignore */
|
||||
fallback={<AtSignIcon className='h-3 w-3' />}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
@@ -33,7 +49,6 @@ function EmailNode({ data }: any) {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { useInvestigationContext } from '../investigation-provider';
|
||||
import { Avatar, Card, Box, Flex, Text, ContextMenu, Dialog, TextField, Button } from '@radix-ui/themes';
|
||||
import { AtSignIcon, CameraIcon, FacebookIcon, InstagramIcon, LocateIcon, PhoneIcon, UserIcon } from 'lucide-react';
|
||||
import { Avatar, Card, Box, Flex, Text, ContextMenu, Dialog, TextField, Button, Spinner, Badge, Tooltip, Inset } from '@radix-ui/themes';
|
||||
import { AtSignIcon, CameraIcon, FacebookIcon, InstagramIcon, LocateIcon, MessageCircleDashedIcon, PhoneIcon, SendIcon, UserIcon } from 'lucide-react';
|
||||
import { NodeProvider, useNodeContext } from './node-context';
|
||||
import { cn } from '@/utils';
|
||||
|
||||
function Custom({ data }: any) {
|
||||
console.log(data)
|
||||
const { settings } = useInvestigationContext()
|
||||
const { setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode } = useNodeContext()
|
||||
const { setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode, loading } = useNodeContext()
|
||||
const [open, setOpen] = useState(false);
|
||||
if (!data) return
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<Box>{settings.showNodeLabel ?
|
||||
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>{settings.showNodeLabel ?
|
||||
<Card className='!py-1' onClick={() => setOpen(true)}>
|
||||
<Flex gap="2" align="center">
|
||||
<Avatar
|
||||
size="2"
|
||||
src={data?.image_url}
|
||||
radius="full"
|
||||
fallback={data.full_name?.[0]}
|
||||
fallback={loading ? <Spinner /> : data.full_name[0]}
|
||||
/>
|
||||
{settings.showNodeLabel &&
|
||||
<Box>
|
||||
@@ -35,12 +35,17 @@ function Custom({ data }: any) {
|
||||
</Text>
|
||||
</Box>}
|
||||
</Flex>
|
||||
</Card> : <Avatar
|
||||
size="3"
|
||||
src={data?.image_url}
|
||||
radius="full"
|
||||
fallback={data.full_name?.[0]}
|
||||
/>}
|
||||
</Card> :
|
||||
<Tooltip content={data.full_name}>
|
||||
<button className='!rounded-full border-transparent' onClick={() => setOpen(true)}>
|
||||
<Avatar
|
||||
size="3"
|
||||
src={data?.image_url}
|
||||
radius="full"
|
||||
fallback={<UserIcon className='h-4 w-4' />}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
@@ -64,10 +69,12 @@ function Custom({ data }: any) {
|
||||
<ContextMenu.Sub>
|
||||
<ContextMenu.SubTrigger >Social account</ContextMenu.SubTrigger>
|
||||
<ContextMenu.SubContent>
|
||||
<ContextMenu.Item><FacebookIcon className='h-4 w-4' />Facebook</ContextMenu.Item>
|
||||
<ContextMenu.Item><InstagramIcon className='h-4 w-4' />Instagram</ContextMenu.Item>
|
||||
<ContextMenu.Item><CameraIcon className='h-4 w-4' />Snapchat</ContextMenu.Item>
|
||||
<ContextMenu.Item>Coco</ContextMenu.Item>
|
||||
<ContextMenu.Item onClick={() => setOpenAddNodeModal("social_accounts_facebook", data.id)}><FacebookIcon className='h-4 w-4' />Facebook</ContextMenu.Item>
|
||||
<ContextMenu.Item onClick={() => setOpenAddNodeModal("social_accounts_instagram", data.id)}><InstagramIcon className='h-4 w-4' />Instagram</ContextMenu.Item>
|
||||
<ContextMenu.Item onClick={() => setOpenAddNodeModal("social_accounts_telegram", data.id)}><SendIcon className='h-4 w-4' />Telegram</ContextMenu.Item>
|
||||
<ContextMenu.Item onClick={() => setOpenAddNodeModal("social_accounts_signal", data.id)}><MessageCircleDashedIcon className='h-4 w-4' />Signal</ContextMenu.Item>
|
||||
<ContextMenu.Item onClick={() => setOpenAddNodeModal("social_accounts_snapchat", data.id)}><CameraIcon className='h-4 w-4' />Snapchat</ContextMenu.Item>
|
||||
<ContextMenu.Item disabled onClick={() => setOpenAddNodeModal("social_accounts_coco", data.id)}>Coco <Badge radius='full'>soon</Badge></ContextMenu.Item>
|
||||
</ContextMenu.SubContent>
|
||||
</ContextMenu.Sub>
|
||||
</ContextMenu.SubContent>
|
||||
|
||||
@@ -4,15 +4,16 @@ import { Handle, Position } from '@xyflow/react';
|
||||
import { Card, Box, Text, ContextMenu, Flex, Inset, Badge } from '@radix-ui/themes';
|
||||
import { NodeProvider, useNodeContext } from './node-context';
|
||||
import { LocateIcon } from 'lucide-react';
|
||||
import { cn } from '@/utils';
|
||||
|
||||
function Custom({ data }: any) {
|
||||
const { handleDeleteNode } = useNodeContext()
|
||||
const { handleDeleteNode, loading } = useNodeContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<Box>
|
||||
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
|
||||
<Card>
|
||||
<Inset>
|
||||
<Flex className='items-center p-0'>
|
||||
|
||||
@@ -8,15 +8,20 @@ import { useNodeId, useReactFlow } from "@xyflow/react";
|
||||
interface NodeContextType {
|
||||
setOpenAddNodeModal: any,
|
||||
handleDuplicateNode: any,
|
||||
handleDeleteNode: any
|
||||
handleDeleteNode: any,
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const nodesTypes = {
|
||||
"emails": { table: "emails", type: "email", field: "email" },
|
||||
"individuals": { table: "individuals", type: "individual", field: "full_name" },
|
||||
"phone_numbers": { table: "phone_numbers", type: "phone", field: "phone_number" },
|
||||
"ip_addresses": { table: "ip_addresses", type: "ip", field: "ip_address" },
|
||||
"social_accounts": { table: "social_accounts", type: "social", field: "username" },
|
||||
"emails": { table: "emails", type: "email", fields: ["email"] },
|
||||
"individuals": { table: "individuals", type: "individual", fields: ["full_name"] },
|
||||
"phone_numbers": { table: "phone_numbers", type: "phone", fields: ["phone_number"] },
|
||||
"ip_addresses": { table: "ip_addresses", type: "ip", fields: ["ip_address"] },
|
||||
"social_accounts_facebook": { table: "social_accounts", type: "social", fields: ["profile_url", "username", "platform:facebook"] },
|
||||
"social_accounts_instagram": { table: "social_accounts", type: "social", fields: ["profile_url", "username", "platform:instagram"] },
|
||||
"social_accounts_telegram": { table: "social_accounts", type: "social", fields: ["profile_url", "username", "platform:telegram"] },
|
||||
"social_accounts_snapchat": { table: "social_accounts", type: "social", fields: ["profile_url", "username", "platform:snapchat"] },
|
||||
"social_accounts_signal": { table: "social_accounts", type: "social", fields: ["profile_url", "username", "platform:signal"] },
|
||||
}
|
||||
|
||||
const NodeContext = createContext<NodeContextType | undefined>(undefined);
|
||||
@@ -41,10 +46,25 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
|
||||
setOpenNodeModal(true)
|
||||
}
|
||||
|
||||
const onSubmitNewNodeModal = (e: { preventDefault: () => void; currentTarget: HTMLFormElement | undefined; }) => {
|
||||
const onSubmitNewNodeModal = async (e: { preventDefault: () => void; currentTarget: HTMLFormElement | undefined; }) => {
|
||||
e.preventDefault();
|
||||
const data = Object.fromEntries(new FormData(e.currentTarget));
|
||||
handleAddNode(data);
|
||||
const newNodeId = crypto.randomUUID()
|
||||
addNodes({
|
||||
id: newNodeId,
|
||||
type: nodeType.type,
|
||||
data: { ...data, label: data[nodeType.fields[0]] },
|
||||
position: { x: -100, y: -100 }
|
||||
});
|
||||
if (nodeId)
|
||||
addEdges({
|
||||
source: nodeId,
|
||||
target: newNodeId,
|
||||
type: 'custom',
|
||||
id: `${nodeId}-${newNodeId}`.toString(),
|
||||
label: nodeType.type,
|
||||
});
|
||||
await handleAddNode({ ...data, id: newNodeId });
|
||||
};
|
||||
|
||||
const handleAddNode = async (data: any) => {
|
||||
@@ -74,19 +94,6 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
|
||||
relation_type: "relation"
|
||||
}).then(({ error }) => console.log(error))
|
||||
}
|
||||
addNodes({
|
||||
id: node.id,
|
||||
type: nodeType.type,
|
||||
data: { ...node, label: node[nodeType.field] },
|
||||
position: { x: -100, y: -100 }
|
||||
});
|
||||
addEdges({
|
||||
source: nodeId,
|
||||
target: node.id,
|
||||
type: 'custom',
|
||||
id: `${nodeId}-${node.id}`.toString(),
|
||||
label: nodeType.type,
|
||||
});
|
||||
setLoading(false)
|
||||
setOpenNodeModal(false)
|
||||
}
|
||||
@@ -119,7 +126,7 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
|
||||
}, [nodeId, setNodes, setEdges]);
|
||||
|
||||
return (
|
||||
<NodeContext.Provider {...props} value={{ setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode }}>
|
||||
<NodeContext.Provider {...props} value={{ setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode, loading }}>
|
||||
{props.children}
|
||||
<Dialog.Root open={openAddNodeModal && nodeType} onOpenChange={setOpenNodeModal}>
|
||||
<Dialog.Content maxWidth="450px">
|
||||
@@ -129,16 +136,23 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
|
||||
</Dialog.Description>
|
||||
<form onSubmit={onSubmitNewNodeModal}>
|
||||
<Flex direction="column" gap="3">
|
||||
<label>
|
||||
<Text as="div" size="2" mb="1" weight="bold">
|
||||
Value
|
||||
</Text>
|
||||
<TextField.Root
|
||||
defaultValue={""}
|
||||
name={nodeType?.field}
|
||||
placeholder={`Your value here (${nodeType?.field})`}
|
||||
/>
|
||||
</label>
|
||||
{nodeType?.fields.map((field: any, i: number) => {
|
||||
const [key, value] = field.split(":")
|
||||
console.log(Boolean(value))
|
||||
return (
|
||||
<label key={i}>
|
||||
<Text as="div" size="2" mb="1" weight="bold">
|
||||
{key}
|
||||
</Text>
|
||||
<TextField.Root
|
||||
defaultValue={value || ""}
|
||||
disabled={Boolean(value)}
|
||||
name={key}
|
||||
placeholder={`Your value here (${key})`}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
|
||||
@@ -1,39 +1,55 @@
|
||||
"use client"
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Card, Box, Text, ContextMenu, Flex, Inset, Badge } from '@radix-ui/themes';
|
||||
import { Card, Box, Text, ContextMenu, Flex, Inset, Badge, Tooltip, Avatar } from '@radix-ui/themes';
|
||||
import { NodeProvider, useNodeContext } from './node-context';
|
||||
import { PhoneIcon } from 'lucide-react';
|
||||
import { cn } from '@/utils';
|
||||
import { useInvestigationContext } from '../investigation-provider';
|
||||
|
||||
function Custom({ data }: any) {
|
||||
const { handleDeleteNode } = useNodeContext()
|
||||
const { handleDeleteNode, loading } = useNodeContext()
|
||||
const { settings } = useInvestigationContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<Box>
|
||||
<Card>
|
||||
<Inset>
|
||||
<Flex className='items-center p-0'>
|
||||
<Badge className='!h-[24px] !rounded-r-none'>
|
||||
<PhoneIcon className='h-3 w-3' />
|
||||
</Badge>
|
||||
<Box className='p-1'>
|
||||
<Text as="div" size="1" weight="regular">
|
||||
{data.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Inset>
|
||||
</Card>
|
||||
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
|
||||
{settings.showNodeLabel ?
|
||||
<Card>
|
||||
<Inset>
|
||||
<Flex className='items-center p-0'>
|
||||
<Badge className='!h-[24px] !rounded-r-none'>
|
||||
<PhoneIcon className='h-3 w-3' />
|
||||
</Badge>
|
||||
<Box className='p-1'>
|
||||
<Text as="div" size="1" weight="regular">
|
||||
{data.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Inset>
|
||||
</Card>
|
||||
:
|
||||
<Tooltip content={data.label}>
|
||||
<button className='!rounded-full border-transparent'>
|
||||
<Avatar
|
||||
size="1"
|
||||
src={data?.image_url}
|
||||
radius="full"
|
||||
/* @ts-ignore */
|
||||
fallback={<PhoneIcon className='h-3 w-3' />}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-16 !bg-teal-500"
|
||||
/>
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>
|
||||
|
||||
80
src/components/investigations/nodes/social.tsx
Normal file
80
src/components/investigations/nodes/social.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client"
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { Card, Box, Text, ContextMenu, Flex, Inset, Badge, Tooltip, Avatar } from '@radix-ui/themes';
|
||||
import { NodeProvider, useNodeContext } from './node-context';
|
||||
import { CameraIcon, FacebookIcon, InstagramIcon, MessageCircleDashedIcon, SendIcon } from 'lucide-react';
|
||||
import { cn } from '@/utils';
|
||||
import { useInvestigationContext } from '../investigation-provider';
|
||||
|
||||
function Custom({ data }: any) {
|
||||
const { handleDeleteNode, loading } = useNodeContext()
|
||||
const { settings } = useInvestigationContext()
|
||||
|
||||
const platformsIcons = useMemo(() => ({
|
||||
"facebook": <FacebookIcon className='h-3 w-3' />,
|
||||
"instagram": <InstagramIcon className='h-3 w-3' />,
|
||||
"telegram": <SendIcon className='h-3 w-3' />,
|
||||
"signal": <MessageCircleDashedIcon className='h-3 w-3' />,
|
||||
"snapchat": <CameraIcon className='h-3 w-3' />
|
||||
}), [])
|
||||
return (
|
||||
<>
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
|
||||
{settings.showNodeLabel ?
|
||||
<Card>
|
||||
<Inset>
|
||||
<Flex className='items-center p-0'>
|
||||
<Badge className='!h-[24px] !rounded-r-none'>
|
||||
{/* @ts-ignore */}
|
||||
{platformsIcons[data.platform]}
|
||||
</Badge>
|
||||
<Box maxWidth={"20px"} className='!max-w-[240px] p-1'>
|
||||
<Text as="div" size="1" weight="medium" color='blue' className='truncate text-ellipsis underline'>
|
||||
{data.username || data.profile_url}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Inset>
|
||||
</Card>
|
||||
:
|
||||
<Tooltip content={data.username || data.profile_url}>
|
||||
<button className='!rounded-full border-transparent'>
|
||||
<Avatar
|
||||
size="1"
|
||||
src={data?.image_url}
|
||||
radius="full"
|
||||
/* @ts-ignore */
|
||||
fallback={platformsIcons[data.platform]}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="w-16 !bg-teal-500"
|
||||
/>
|
||||
</Box>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>
|
||||
<ContextMenu.Item shortcut="⌘ D">Duplicate</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item onClick={handleDeleteNode} shortcut="⌘ ⌫" color="red">
|
||||
Delete
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Root >
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SocialNode = (props: any) => (
|
||||
<NodeProvider>
|
||||
<Custom {...props} />
|
||||
</NodeProvider>
|
||||
);
|
||||
|
||||
export default memo(SocialNode);
|
||||
Reference in New Issue
Block a user