feat: vehicles

This commit is contained in:
dextmorgn
2025-04-02 08:47:50 +02:00
parent 9dab0a65c5
commit 75049f25c4
6 changed files with 351 additions and 372 deletions

View File

@@ -14,7 +14,7 @@ export async function GET(_: Request, { params }: { params: Promise<{ investigat
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
} const { data: individuals, error: indError } = await supabase
.from("individuals")
.select("*, ip_addresses(*), phone_numbers(*), social_accounts(*), emails(*), physical_addresses(*), group_id")
.select("*, ip_addresses(*), phone_numbers(*), social_accounts(*), emails(*), physical_addresses(*), vehicles(*), group_id")
.eq("investigation_id", investigation_id)
const { data: groups, error: groupsError } = await supabase
.from("groups")
@@ -127,6 +127,22 @@ export async function GET(_: Request, { params }: { params: Promise<{ investigat
label: "IP",
})
})
// Ajouter les adresses IP
ind.vehicles?.forEach((vehicle: any) => {
nodes.push({
id: vehicle.id.toString(),
type: "vehicle",
data: { label: `${vehicle.plate}-${vehicle.model}` },
position: { x: -100, y: -100 },
})
edges.push({
source: individualId,
target: vehicle.id.toString(),
type: "custom",
id: `${individualId}-${vehicle.id}`.toString(),
label: vehicle.type,
})
})
// Ajouter les adresses physiques
ind.physical_addresses?.forEach((address: any) => {
nodes.push({

View File

@@ -5,8 +5,7 @@ import { useParams } from "next/navigation"
import { supabase } from "@/lib/supabase/client"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { AlertCircle, AtSign, Camera, Facebook, Github, GithubIcon, Instagram, Locate, MapPin, MessageCircleDashed, Phone, PlusIcon, Send, User } from "lucide-react"
import type React from "react" // Added import for React
import { AlertCircle } from "lucide-react"
import { toast } from "sonner"
import {
DropdownMenu,
@@ -16,12 +15,14 @@ import {
DropdownMenuSub,
DropdownMenuPortal,
DropdownMenuContent,
DropdownMenuSubContent
DropdownMenuSubContent,
} from "@/components/ui/dropdown-menu"
import { nodesTypes } from "@/lib/utils"
import { Alert, AlertTitle, AlertDescription } from "../ui/alert"
import { Badge } from "../ui/badge"
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "../ui/dialog"
import { actionItems, type ActionItem } from "@/lib/action-items"
import { PlusIcon } from "lucide-react"
export default function NewActions({ addNodes }: { addNodes: any }) {
const { investigation_id } = useParams()
@@ -30,13 +31,13 @@ export default function NewActions({ addNodes }: { addNodes: any }) {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const handleOpenAddNodeModal = (e: { stopPropagation: () => void }, tableName: string, individualId?: string) => {
const handleOpenAddNodeModal = (e: { stopPropagation: () => void }, key: string) => {
e.stopPropagation()
if (!nodesTypes[tableName as keyof typeof nodesTypes]) {
if (!nodesTypes[key as keyof typeof nodesTypes]) {
toast.error("Invalid node type.")
return
}
setCurrentNodeType(nodesTypes[tableName as keyof typeof nodesTypes])
setCurrentNodeType(nodesTypes[key as keyof typeof nodesTypes])
setError(null)
setOpenNodeModal(true)
}
@@ -49,6 +50,7 @@ export default function NewActions({ addNodes }: { addNodes: any }) {
const data = Object.fromEntries(new FormData(e.currentTarget))
await handleAddNode(data)
}
const handleAddNode = async (data: any) => {
try {
setLoading(true)
@@ -58,8 +60,9 @@ export default function NewActions({ addNodes }: { addNodes: any }) {
.insert(dataToInsert)
.select("*")
.single()
console.log(insertError)
if (insertError) {
toast.error(insertError.details)
toast.error("Failed to create node.")
setLoading(false)
return
}
@@ -79,77 +82,63 @@ export default function NewActions({ addNodes }: { addNodes: any }) {
setError(null)
} catch (error) {
toast.error("An unexpected error occurred")
} finally {
setLoading(false)
}
}
// Render a dropdown menu item
const renderMenuItem = (item: ActionItem) => {
const Icon = item.icon
if (item.children) {
return (
<DropdownMenuSub key={item.id}>
<DropdownMenuSubTrigger>
<Icon className="mr-3 h-4 w-4 opacity-50" />
{item.label}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{item.children.map((childItem) => renderMenuItem(childItem))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)
}
return (
<DropdownMenuItem
key={item.id}
disabled={item.disabled}
onClick={(e) => handleOpenAddNodeModal(e, item.key)}
>
<Icon className="mr-2 h-4 w-4 opacity-70" />
{item.label}
{item.comingSoon && (
<Badge variant="outline" className="ml-2">
soon
</Badge>
)}
</DropdownMenuItem>
)
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild><Button size={"icon"}><PlusIcon /></Button></DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "individuals")}>
<User className="mr-2 h-4 w-4 opacity-70" /> New relation
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "phone_numbers")}>
<Phone className="mr-2 h-4 w-4 opacity-70" />
Phone number
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "physical_addresses")}>
<MapPin className="mr-2 h-4 w-4 opacity-70" />
Physical address
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "emails")}>
<AtSign className="mr-2 h-4 w-4 opacity-70" />
Email address
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "ip_addresses")}>
<Locate className="mr-2 h-4 w-4 opacity-70" />
IP address
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Social account</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "social_accounts_facebook")}>
<Facebook className="mr-2 h-4 w-4 opacity-70" />
Facebook
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "social_accounts_instagram")}>
<Instagram className="mr-2 h-4 w-4 opacity-70" />
Instagram
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "social_accounts_telegram")}>
<Send className="mr-2 h-4 w-4 opacity-70" />
Telegram
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "social_accounts_signal")}>
<MessageCircleDashed className="mr-2 h-4 w-4 opacity-70" />
Signal
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "social_accounts_snapchat")}>
<Camera className="mr-2 h-4 w-4 opacity-70" />
Snapchat
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleOpenAddNodeModal(e, "social_accounts_github")}>
<Github className="mr-2 h-4 w-4 opacity-70" />
Github
</DropdownMenuItem>
<DropdownMenuItem disabled onClick={(e) => handleOpenAddNodeModal(e, "social_accounts_coco")}>
Coco{" "}
<Badge variant="outline" className="ml-2">
soon
</Badge>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu >
<DropdownMenuTrigger asChild>
<Button size={"icon"}>
<PlusIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" align="start">{actionItems.map((item) => renderMenuItem(item))}</DropdownMenuContent>
</DropdownMenu>
<Dialog open={openAddNodeModal && currentNodeType} onOpenChange={setOpenNodeModal}>
<DialogContent>
<DialogTitle>New {currentNodeType?.type}</DialogTitle>
<DialogDescription>Add a new related {currentNodeType?.type}.</DialogDescription>
<form onSubmit={onSubmitNewNodeModal}>
<div className="flex flex-col ga-3">
<div className="flex flex-col gap-3">
{currentNodeType?.fields.map((field: any, i: number) => {
const [key, value] = field.split(":")
return (
@@ -182,6 +171,5 @@ export default function NewActions({ addNodes }: { addNodes: any }) {
</Dialog>
</>
)
}

View File

@@ -47,11 +47,8 @@ import AddNodeModal from "../add-node-modal"
import { Dialog, DialogTrigger } from "../../ui/dialog"
import { memo } from "react"
import { shallow } from "zustand/shallow"
import FloatingEdge from "./simple-floating-edge"
import { TooltipProvider } from "@/components/ui/tooltip"
import NodeContextMenu from "./nodes/node-context-menu"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import ProfilePanel from "./profile-panel"
import GroupNode from "./nodes/group"
import CustomEdge from "./nodes/custom-edge"
const edgeTypes = {

View File

@@ -72,13 +72,13 @@ const EditableEdge = memo((props: EdgeProps) => {
value={editValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
className="h-7 w-24"
className="h-5 text-xs rounded-sm"
/>
<Button
ref={submitButtonRef}
size="icon"
variant="ghost"
className="h-7 w-7"
className="h-5 w-5"
type="submit"
disabled={isLoading}
>

View File

@@ -4,26 +4,12 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from "@/components/ui/dropdown-menu"
import {
AtSign,
Camera,
Facebook,
GithubIcon,
Instagram,
Locate,
MapPin,
MessageCircleDashed,
Phone,
Send,
SquarePenIcon,
User,
} from "lucide-react"
import { SquarePenIcon } from "lucide-react"
import { useQueryState } from "nuqs"
import { memo, useCallback, useState } from "react"
import { useParams } from "next/navigation"
@@ -40,7 +26,7 @@ import { AlertCircle } from "lucide-react"
import { NodeNotesEditor } from "./node-notes-editor"
import { checkEmail } from "@/lib/actions/search"
import { nodesTypes } from "@/lib/utils"
// Node types definition
import { actionItems, type ActionItem } from "@/lib/action-items"
// Node Context Menu component
interface NodeContextMenuProps {
@@ -49,293 +35,285 @@ interface NodeContextMenuProps {
onClose: () => void | null | undefined
}
const NodeContextMenu = memo(
({ x, y, onClose }: NodeContextMenuProps) => {
const { currentNode, setCurrentNode } = useFlowStore()
const { addNodes, addEdges, setNodes, setEdges } = useReactFlow()
const { investigation_id } = useParams()
const [openAddNodeModal, setOpenNodeModal] = useState(false)
const [error, setError] = useState<null | string>(null)
const [openNote, setOpenNote] = useState(false)
const [loading, setLoading] = useState(false)
const [currentNodeType, setCurrentNodeType] = useState<any | null>(null)
const { confirm } = useConfirm()
const [_, setIndividualId] = useQueryState("individual_id")
const NodeContextMenu = memo(({ x, y, onClose }: NodeContextMenuProps) => {
const { currentNode, setCurrentNode } = useFlowStore()
const { addNodes, addEdges, setNodes, setEdges } = useReactFlow()
const { investigation_id } = useParams()
const [openAddNodeModal, setOpenNodeModal] = useState(false)
const [error, setError] = useState<null | string>(null)
const [openNote, setOpenNote] = useState(false)
const [loading, setLoading] = useState(false)
const [currentNodeType, setCurrentNodeType] = useState<any | null>(null)
const { confirm } = useConfirm()
const [_, setIndividualId] = useQueryState("individual_id")
const handleCheckEmail = useCallback(() => {
// @ts-ignore
if (!currentNode && currentNode?.data && !currentNode?.data?.email) return
// @ts-ignore
toast.promise(checkEmail(currentNode?.data?.email, investigation_id), {
loading: "Loading...",
success: () => {
return `Scan on ${currentNode?.data?.email} has been launched.`
},
error: (error: any) => {
return (
<div className="overflow-hidden">
<p className="font-bold">An error occured.</p>
<pre className="overflow-auto">
<code>{JSON.stringify(error, null, 2)}</code>
</pre>
</div>
)
},
})
}, [currentNode, investigation_id])
const handleDuplicateNode = async () => {
if (!currentNode) return
await supabase
.from("individuals")
.select("*")
.eq("id", currentNode.id)
.single()
.then(async ({ data, error }) => {
if (error) throw error
const { data: node, error: insertError } = await supabase
.from("individuals")
.insert({ full_name: data.full_name })
.select("*")
.single()
if (insertError) toast.error(insertError.details)
addNodes({
id: node.id,
type: "individual",
data: node,
position: { x: 0, y: -100 },
})
})
}
const _handleDeleteNode = async (type: string) => {
if (!currentNode) return
if (await confirm({ title: "Node deletion", message: "Are you sure you want to delete this node?" })) {
await supabase
const handleCheckEmail = useCallback(() => {
// @ts-ignore
if (!currentNode && currentNode?.data && !currentNode?.data?.email) return
// @ts-ignore
toast.promise(checkEmail(currentNode?.data?.email, investigation_id), {
loading: "Loading...",
success: () => {
return `Scan on ${currentNode?.data?.email} has been launched.`
},
error: (error: any) => {
return (
<div className="overflow-hidden">
<p className="font-bold">An error occured.</p>
<pre className="overflow-auto">
<code>{JSON.stringify(error, null, 2)}</code>
</pre>
</div>
)
},
})
}, [currentNode, investigation_id])
const handleDuplicateNode = async () => {
if (!currentNode) return
await supabase
.from("individuals")
.select("*")
.eq("id", currentNode.id)
.single()
.then(async ({ data, error }) => {
if (error) throw error
const { data: node, error: insertError } = await supabase
.from("individuals")
.delete()
.eq("id", currentNode.id)
.then(({ error }) => {
if (error) toast.error(error.details)
})
setNodes((nodes: any[]) => nodes.filter((node: { id: any }) => node.id !== currentNode?.id?.toString()))
setEdges((edges: any[]) => edges.filter((edge: { source: any }) => edge.source !== currentNode?.id?.toString()))
onClose()
toast.success("Node deleted.")
}
}
const handleDeleteNode = useCallback(_handleDeleteNode, [currentNode?.id, setNodes, setEdges, confirm, onClose])
const setOpenAddNodeModal = (e: { stopPropagation: () => void }, tableName: string, individualId?: string) => {
e.stopPropagation()
if (!currentNode) return
if (!nodesTypes[tableName as keyof typeof nodesTypes]) {
toast.error("Invalid node type.")
return
}
setCurrentNodeType(nodesTypes[tableName as keyof typeof nodesTypes])
setError(null)
setOpenNodeModal(true)
}
const onSubmitNewNodeModal = async (e: {
preventDefault: () => void
currentTarget: HTMLFormElement | undefined
}) => {
e.preventDefault()
const data = Object.fromEntries(new FormData(e.currentTarget))
await handleAddNode(data)
}
const handleAddNode = async (data: any) => {
try {
setLoading(true)
if (!currentNode) {
toast.error("No node detected.")
setLoading(false)
return
}
const dataToInsert = { ...data, investigation_id }
if (currentNodeType.table !== "individuals") {
dataToInsert["individual_id"] = currentNode.id
}
const { data: nodeData, error: insertError } = await supabase
.from(currentNodeType.table)
.insert(dataToInsert)
.insert({ full_name: data.full_name })
.select("*")
.single()
if (insertError) {
toast.error(insertError.details)
setLoading(false)
return
}
if (!nodeData) {
toast.error("Failed to create node.")
setLoading(false)
return
}
if (currentNodeType.table === "individuals") {
const { error: relationshipError } = await supabase.from("relationships").upsert({
individual_a: currentNode.id,
individual_b: nodeData.id,
relation_type: "relation",
})
if (relationshipError) {
toast.error("Error creating new relation: " + relationshipError.message)
}
}
const newNode = {
id: nodeData.id,
type: currentNodeType.type,
data: { ...nodeData, label: data[currentNodeType.fields[0]] },
position: { x: 0, y: 0 },
}
addNodes(newNode)
if (currentNode.id) {
const newEdge = {
source: currentNode.id as string,
target: nodeData.id,
type: "custom",
id: `${currentNode.id}-${nodeData.id}`.toString(),
label: currentNodeType.type === "individual" ? "relation" : currentNodeType.type,
}
addEdges(newEdge)
}
setOpenNodeModal(false)
setError(null)
setTimeout(() => {
setCurrentNode(nodeData)
setLoading(false)
}, 0)
} catch (error) {
toast.error("An unexpected error occurred")
setLoading(false)
}
if (insertError) toast.error(insertError.details)
addNodes({
id: node.id,
type: "individual",
data: node,
position: { x: 0, y: -100 },
})
})
}
const _handleDeleteNode = async (type: string) => {
if (!currentNode) return
if (await confirm({ title: "Node deletion", message: "Are you sure you want to delete this node?" })) {
await supabase
.from("individuals")
.delete()
.eq("id", currentNode.id)
.then(({ error }) => {
if (error) toast.error(error.details)
})
setNodes((nodes: any[]) => nodes.filter((node: { id: any }) => node.id !== currentNode?.id?.toString()))
setEdges((edges: any[]) => edges.filter((edge: { source: any }) => edge.source !== currentNode?.id?.toString()))
onClose()
toast.success("Node deleted.")
}
const handleEditClick = useCallback(() => setIndividualId(currentNode?.id as string), [currentNode?.id, setIndividualId])
const handleDuplicateClick = useCallback(() => handleDuplicateNode(), [])
const handleDeleteClick = useCallback(() => handleDeleteNode(currentNode?.type as string), [currentNode?.type, handleDeleteNode])
const handleNoteClick = useCallback(() => setOpenNote(true), [setOpenNote])
if (!currentNode) return null
}
const handleDeleteNode = useCallback(_handleDeleteNode, [currentNode?.id, setNodes, setEdges, confirm, onClose])
const handleOpenAddNodeModal = (e: { stopPropagation: () => void }, key: string) => {
e.stopPropagation()
if (!currentNode) return
if (!nodesTypes[key as keyof typeof nodesTypes]) {
toast.error("Invalid node type.")
return
}
setCurrentNodeType(nodesTypes[key as keyof typeof nodesTypes])
setError(null)
setOpenNodeModal(true)
}
const onSubmitNewNodeModal = async (e: {
preventDefault: () => void
currentTarget: HTMLFormElement | undefined
}) => {
e.preventDefault()
const data = Object.fromEntries(new FormData(e.currentTarget))
await handleAddNode(data)
}
const handleAddNode = async (data: any) => {
try {
setLoading(true)
if (!currentNode) {
toast.error("No node detected.")
setLoading(false)
return
}
const dataToInsert = { ...data, investigation_id }
if (currentNodeType.table !== "individuals") {
dataToInsert["individual_id"] = currentNode.id
}
const { data: nodeData, error: insertError } = await supabase
.from(currentNodeType.table)
.insert(dataToInsert)
.select("*")
.single()
if (insertError) {
toast.error("An error occured during the creation.")
setLoading(false)
return
}
if (!nodeData) {
toast.error("An error occured during the creation.")
setLoading(false)
return
}
if (currentNodeType.table === "individuals") {
const { error: relationshipError } = await supabase.from("relationships").upsert({
individual_a: currentNode.id,
individual_b: nodeData.id,
relation_type: "relation",
})
if (relationshipError) {
toast.error("Error creating new relation.")
}
}
const newNode = {
id: nodeData.id,
type: currentNodeType.type,
data: { ...nodeData, label: data[currentNodeType.fields[0]] },
position: { x: 0, y: 0 },
}
addNodes(newNode)
if (currentNode.id) {
const newEdge = {
source: currentNode.id as string,
target: nodeData.id,
type: "custom",
id: `${currentNode.id}-${nodeData.id}`.toString(),
label: currentNodeType.type === "individual" ? "relation" : currentNodeType.type,
}
addEdges(newEdge)
}
setOpenNodeModal(false)
setError(null)
setTimeout(() => {
setCurrentNode(nodeData)
setLoading(false)
}, 0)
} catch (error) {
toast.error("An unexpected error occurred.")
setLoading(false)
}
}
const handleEditClick = useCallback(
() => setIndividualId(currentNode?.id as string),
[currentNode?.id, setIndividualId],
)
const handleDuplicateClick = useCallback(() => handleDuplicateNode(), [])
const handleDeleteClick = useCallback(
() => handleDeleteNode(currentNode?.type as string),
[currentNode?.type, handleDeleteNode],
)
const handleNoteClick = useCallback(() => setOpenNote(true), [setOpenNote])
// Render a dropdown menu item with the current node's ID
const renderMenuItem = (item: ActionItem) => {
const Icon = item.icon
if (item.children) {
return (
<DropdownMenuSub key={item.id}>
<DropdownMenuSubTrigger>
<Icon className="mr-3 h-4 w-4 opacity-50" />
{item.label}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>{item.children.map((childItem) => renderMenuItem(childItem))}</DropdownMenuSubContent>
</DropdownMenuSub>
)
}
return (
<>
<DropdownMenu open={Boolean(currentNode) && Boolean(x) && Boolean(y)} onOpenChange={onClose}>
<DropdownMenuContent
className="absolute z-50 min-w-40 max-w-48 bg-popover text-popover-foreground rounded-md border shadow-md py-1 overflow-hidden"
style={{ top: y, left: x }}
>
{Boolean(currentNode?.data?.email) &&
(<DropdownMenuItem onClick={handleCheckEmail}>
Search {currentNodeType}
</DropdownMenuItem>)}
<DropdownMenuItem onClick={handleNoteClick}>
New note
<SquarePenIcon className="!h-4 !w-4" />
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>New</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "individuals")}>
<User className="mr-2 h-4 w-4 opacity-70" /> New relation
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "phone_numbers", currentNode.id)}>
<Phone className="mr-2 h-4 w-4 opacity-70" />
Phone number
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "physical_addresses", currentNode.id)}>
<MapPin className="mr-2 h-4 w-4 opacity-70" />
Physical address
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "emails", currentNode.id)}>
<AtSign className="mr-2 h-4 w-4 opacity-70" />
Email address
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "ip_addresses", currentNode.id)}>
<Locate className="mr-2 h-4 w-4 opacity-70" />
IP address
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Social account</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_facebook", currentNode.id)}>
<Facebook className="mr-2 h-4 w-4 opacity-70" />
Facebook
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_instagram", currentNode.id)}>
<Instagram className="mr-2 h-4 w-4 opacity-70" />
Instagram
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_telegram", currentNode.id)}>
<Send className="mr-2 h-4 w-4 opacity-70" />
Telegram
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_signal", currentNode.id)}>
<MessageCircleDashed className="mr-2 h-4 w-4 opacity-70" />
Signal
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_snapchat", currentNode.id)}>
<Camera className="mr-2 h-4 w-4 opacity-70" />
Snapchat
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_github", currentNode.id)}>
<GithubIcon className="mr-2 h-4 w-4 opacity-70" />
Github
</DropdownMenuItem>
<DropdownMenuItem disabled onClick={(e) => setOpenAddNodeModal(e, "social_accounts_coco", currentNode.id)}>
Coco{" "}
<Badge variant="outline" className="ml-2">
soon
</Badge>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem onClick={handleEditClick}>View and edit</DropdownMenuItem>
<DropdownMenuItem onClick={handleDuplicateClick}>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleDeleteClick} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog open={openAddNodeModal && currentNodeType} onOpenChange={setOpenNodeModal}>
<DialogContent>
<DialogTitle>New {currentNodeType?.type}</DialogTitle>
<DialogDescription>Add a new related {currentNodeType?.type}.</DialogDescription>
<form onSubmit={onSubmitNewNodeModal}>
<div className="flex flex-col ga-3">
{currentNodeType?.fields.map((field: any, i: number) => {
const [key, value] = field.split(":")
return (
<label key={i}>
<p className="my-2">{key}</p>
<Input defaultValue={value || ""} name={key} placeholder={`Your value here (${key})`} />
</label>
)
})}
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex items-center gap-2 justify-end mt-4">
<DialogClose asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>
<Button disabled={loading} type="submit">
Save
</Button>
</div>
</form>
</DialogContent>
</Dialog>
{currentNode.id && <NodeNotesEditor openNote={openNote} setOpenNote={setOpenNote} individualId={currentNode.id} />}
</>
<DropdownMenuItem
key={item.id}
disabled={item.disabled}
onClick={(e) => handleOpenAddNodeModal(e, item.key)}
>
<Icon className="mr-3 h-4 w-4 opacity-70" />
{item.label}
{item.comingSoon && (
<Badge variant="outline" className="ml-2">
soon
</Badge>
)}
</DropdownMenuItem>
)
},
)
}
if (!currentNode) return null
return (
<>
<DropdownMenu open={Boolean(currentNode) && Boolean(x) && Boolean(y)} onOpenChange={onClose}>
<DropdownMenuContent
className="absolute z-50 min-w-40 max-w-48 bg-popover text-popover-foreground rounded-md border shadow-md py-1 overflow-hidden"
style={{ top: y, left: x }}
>
{Boolean(currentNode?.data?.email) && (
<DropdownMenuItem onClick={handleCheckEmail}>Search {currentNodeType}</DropdownMenuItem>
)}
<DropdownMenuItem onClick={handleNoteClick}>
New note
<SquarePenIcon className="ml-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>New</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-48">{actionItems.map((item) => renderMenuItem(item))}</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem onClick={handleEditClick}>View and edit</DropdownMenuItem>
<DropdownMenuItem onClick={handleDuplicateClick}>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleDeleteClick} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog open={openAddNodeModal && currentNodeType} onOpenChange={setOpenNodeModal}>
<DialogContent>
<DialogTitle>New {currentNodeType?.type}</DialogTitle>
<DialogDescription>Add a new related {currentNodeType?.type}.</DialogDescription>
<form onSubmit={onSubmitNewNodeModal}>
<div className="flex flex-col gap-3">
{currentNodeType?.fields.map((field: any, i: number) => {
const [key, value] = field.split(":")
return (
<label key={i}>
<p className="my-2">{key}</p>
<Input defaultValue={value || ""} name={key} placeholder={`Your value here (${key})`} />
</label>
)
})}
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex items-center gap-2 justify-end mt-4">
<DialogClose asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>
<Button disabled={loading} type="submit">
Save
</Button>
</div>
</form>
</DialogContent>
</Dialog>
{currentNode.id && (
<NodeNotesEditor openNote={openNote} setOpenNote={setOpenNote} individualId={currentNode.id} />
)}
</>
)
})
export default NodeContextMenu

View File

@@ -176,7 +176,7 @@ const createStore = (initialNodes: AppNode[] = [], initialEdges: Edge[] = []) =>
animated,
style: {
...edge.style,
stroke: animated ? "var(--primary)" : "#b1b1b750",
// stroke: animated ? "var(--primary)" : "#b1b1b750",
opacity: animated ? 1 : 0.25,
},
};