fix: usememo for better graph performance

This commit is contained in:
dextmorgn
2025-03-01 11:38:50 +01:00
parent a8fa55572e
commit f11fd145f0
18 changed files with 1614 additions and 738 deletions

View File

@@ -51,6 +51,9 @@
"@tailwindcss/cli": "^4.0.3",
"@tailwindcss/postcss": "^4.0.3",
"@tanstack/react-query": "^5.66.8",
"@tiptap/pm": "^2.11.5",
"@tiptap/react": "^2.11.5",
"@tiptap/starter-kit": "^2.11.5",
"@xyflow/react": "^12.4.2",
"ai": "^4.1.34",
"class-variance-authority": "^0.7.1",
@@ -117,4 +120,4 @@
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
"lightningcss-linux-x64-gnu": "^1.29.1"
}
}
}

View File

@@ -17,7 +17,6 @@ export async function GET(_: Request, { params }: { params: Promise<{ investigat
.select("*, ip_addresses(*), phone_numbers(*), social_accounts(*), emails(*), physical_addresses(*)")
.eq("investigation_id", investigation_id)
if (indError) {
console.log(indError.message)
return NextResponse.json({ error: indError.message }, { status: 500 })
}
if (!individuals || individuals.length === 0) {

View File

@@ -0,0 +1,30 @@
import { createClient } from "@/lib/supabase/server"
import { NextResponse } from "next/server"
export async function GET(_: Request, { params }: { params: Promise<{ investigation_id: string, individual_id: string }> }) {
const { investigation_id, individual_id } = await params
try {
const supabase = await createClient()
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();
if (!user || userError) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { data: individual, error } = await supabase
.from('individuals')
.select(`
*
`)
.eq("investigation_id", investigation_id)
.eq("id", individual_id)
.single()
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json(individual)
} catch (error) {
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
}
}

View File

@@ -15,12 +15,15 @@ import { useNodeId, useReactFlow } from "@xyflow/react";
import { useConfirm } from "@/components/use-confirm-dialog";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
import { NodeNotesEditor } from "../node-notes-editor";
interface NodeContextType {
setOpenAddNodeModal: any,
handleDuplicateNode: any,
handleDeleteNode: any,
loading: boolean
loading: boolean,
openNote: boolean,
setOpenNote: any
}
const nodesTypes = {
@@ -48,6 +51,7 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
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 [nodeType, setnodeType] = useState<any | null>(null)
const nodeId = useNodeId();
@@ -158,7 +162,7 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
}, [nodeId, setNodes, setEdges]);
return (
<NodeContext.Provider {...props} value={{ setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode, loading }}>
<NodeContext.Provider {...props} value={{ setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode, loading, openNote, setOpenNote }}>
{props.children}
<Dialog open={openAddNodeModal && nodeType} onOpenChange={setOpenNodeModal}>
<DialogContent>
@@ -204,6 +208,7 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
</form>
</DialogContent>
</Dialog>
<NodeNotesEditor individualId={nodeId as string} />
</NodeContext.Provider>
);
};

View File

@@ -10,10 +10,10 @@ import { useSearchResults } from '@/lib/hooks/investigation/use-search-results'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
import { useFlowStore } from '@/store/flow-store'
import { cn } from '@/lib/utils'
import Loader from '../loader'
const SearchModal = ({ investigation_id }: { investigation_id: string }) => {
const [search, setSearch] = useState("")
@@ -52,19 +52,24 @@ const SearchModal = ({ investigation_id }: { investigation_id: string }) => {
const SearchItem = ({ item }: any) => (
<li
className={cn(
"hover:bg-sidebar-accent text-sidebar-accent-foreground/90 hover:text-sidebar-accent-foreground text-sm",
"hover:bg-sidebar-accent rounded-md text-sidebar-accent-foreground/90 hover:text-sidebar-accent-foreground text-sm",
"bg-sidebar-accent text-sidebar-accent-foreground",
)}
key={item.id}
>
<button
onClick={() => setCurrentNode(item.id)}
onClick={() => { setCurrentNode(item.id); setOpen(false) }}
className="flex items-center p-1 px-4 w-full gap-2"
>
<UserIcon className="h-3 w-3 opacity-60" />
{item.full_name}
<Highlighter
highlightClassName="bg-primary/60"
searchWords={[search]}
autoEscape={true}
textToHighlight={item.full_name}
/>
</button>
</li>
</li >
)
return (
@@ -88,7 +93,7 @@ const SearchModal = ({ investigation_id }: { investigation_id: string }) => {
/>
<div className='w-full relative text-center flex flex-col items-center justify-center gap-2'>
{error && <p className="text-red-500">An error occurred.</p>}
{isLoading && <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>}
{isLoading && <div><Loader /> Loading..</div>}
{results?.length === 0 && <p>No results found for "{search}".</p>}
{results && results?.length !== 0 && <ScrollArea className="h-[60vh] w-full rounded-md border p-4">
<ul className='space-y-2'>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { getBezierPath, Position } from '@xyflow/react';
import { getBezierPath, getStraightPath, Position } from '@xyflow/react';
import { getEdgeParams } from '@/lib/utils';
const FloatingConnectionLine = ({
@@ -31,11 +31,11 @@ const FloatingConnectionLine = ({
};
const { sx, sy } = getEdgeParams(fromNode, targetNode);
const [edgePath] = getBezierPath({
const [edgePath] = getStraightPath({
sourceX: sx,
sourceY: sy,
sourcePosition: fromPosition,
targetPosition: toPosition,
// sourcePosition: fromPosition,
// targetPosition: toPosition,
targetX: toX,
targetY: toY,
});

View File

@@ -1,5 +1,5 @@
"use client"
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from "react"
import {
ReactFlow,
ReactFlowProvider,
@@ -7,39 +7,49 @@ import {
useReactFlow,
// @ts-ignore
Background,
ColorMode,
type ColorMode,
// @ts-ignore
MiniMap
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import IndividualNode from './nodes/individual';
import PhoneNode from './nodes/phone';
import IpNode from './nodes/ip_address';
import EmailNode from './nodes/email';
import SocialNode from './nodes/social';
import AddressNode from './nodes/physical_address';
import { AlignCenterVertical, MaximizeIcon, ZoomInIcon, ZoomOutIcon, RotateCwIcon, PlusIcon, GroupIcon } from 'lucide-react';
import { useTheme } from 'next-themes';
import NewActions from './new-actions';
import FloatingEdge from './floating-edge';
import FloatingConnectionLine from './floating-connection';
import { useParams } from 'next/navigation';
import { Tooltip, TooltipContent } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { TooltipTrigger } from '@radix-ui/react-tooltip';
import { cn } from '@/lib/utils';
import { useInvestigationStore } from '@/store/investigation-store';
import { useFlowStore } from '../../store/flow-store';
import Loader from '../loader';
import { WorkflowIcon } from 'lucide-react';
import { useQueryState } from 'nuqs';
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '../ui/context-menu';
import { useInvestigationContext } from '../contexts/investigation-provider';
import AddNodeModal from './add-node-modal';
import { Dialog, DialogTrigger } from '../ui/dialog';
MiniMap,
} from "@xyflow/react"
import "@xyflow/react/dist/style.css"
import IndividualNode from "./nodes/individual"
import PhoneNode from "./nodes/phone"
import IpNode from "./nodes/ip_address"
import EmailNode from "./nodes/email"
import SocialNode from "./nodes/social"
import AddressNode from "./nodes/physical_address"
import {
AlignCenterVertical,
MaximizeIcon,
ZoomInIcon,
ZoomOutIcon,
RotateCwIcon,
PlusIcon,
GroupIcon,
WorkflowIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import NewActions from "./new-actions"
import FloatingEdge from "./floating-edge"
import FloatingConnectionLine from "./floating-connection"
import { useParams } from "next/navigation"
import { Tooltip, TooltipContent } from "@/components/ui/tooltip"
import { Button } from "@/components/ui/button"
import { TooltipTrigger } from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
import { useInvestigationStore } from "@/store/investigation-store"
import { useFlowStore } from "../../store/flow-store"
import Loader from "../loader"
import { useQueryState } from "nuqs"
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "../ui/context-menu"
import AddNodeModal from "./add-node-modal"
import { Dialog, DialogTrigger } from "../ui/dialog"
import { memo } from "react"
// Define constants outside component to prevent recreation
const edgeTypes = {
"custom": FloatingEdge
};
custom: FloatingEdge,
}
const nodeTypes = {
individual: IndividualNode,
@@ -47,158 +57,215 @@ const nodeTypes = {
ip: IpNode,
email: EmailNode,
social: SocialNode,
address: AddressNode
};
address: AddressNode,
}
const LayoutFlow = ({ refetch, theme }: { refetch: any, isLoading: boolean, theme: ColorMode }) => {
const { fitView, zoomIn, zoomOut, addNodes, getNode, setCenter, getNodes, getEdges } = useReactFlow();
const { investigation_id } = useParams();
const { settings } = useInvestigationStore();
import { shallow } from "zustand/shallow"
// Split selectors to minimize re-renders
const nodeEdgeSelector = (store: { nodes: any; edges: any }) => ({
nodes: store.nodes,
edges: store.edges,
})
const actionsSelector = (store: { onNodesChange: any; onEdgesChange: any; onConnect: any; onNodeClick: any; onPaneClick: any; onLayout: any }) => ({
onNodesChange: store.onNodesChange,
onEdgesChange: store.onEdgesChange,
onConnect: store.onConnect,
onNodeClick: store.onNodeClick,
onPaneClick: store.onPaneClick,
onLayout: store.onLayout,
})
const stateSelector = (store: { currentNode: any; resetNodeStyles: any; reloading: any }) => ({
currentNode: store.currentNode,
resetNodeStyles: store.resetNodeStyles,
reloading: store.reloading,
})
// FlowControls component to reduce LayoutFlow complexity
interface FlowControlsProps {
onLayout: (direction: string, fitView: () => void) => void;
fitView: () => void;
handleRefetch: () => void;
reloading: boolean;
setView: (view: string) => void;
zoomIn: () => void;
zoomOut: () => void;
addNodes: (payload: Node | Node[]) => void;
}
const FlowControls = memo(({ onLayout, fitView, handleRefetch, reloading, setView, zoomIn, zoomOut, addNodes }: FlowControlsProps) => {
return (
<>
<Panel position="top-left" className="flex items-center gap-1 w-53">
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="outline"
onClick={() => {
onLayout("TB", fitView)
setTimeout(() => {
fitView()
}, 100)
}}
>
<AlignCenterVertical className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Auto layout</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline" onClick={() => setView("large-graph")}>
<WorkflowIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>View 2D Graph</TooltipContent>
</Tooltip>
</Panel>
<Panel position="top-right" className="flex items-center gap-1">
<div className="flex flex-col items-end gap-2">
<div className="flex gap-1 items-center">
<Button size="icon" disabled={reloading} variant="outline" onClick={handleRefetch}>
<RotateCwIcon className={cn("h-4 w-4", reloading && "animate-spin")} />
</Button>
<NewActions addNodes={addNodes} />
</div>
</div>
</Panel>
<Panel position="bottom-left" className="flex flex-col items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline" onClick={() => fitView()}>
<MaximizeIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Center view</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline" onClick={() => zoomIn()}>
<ZoomInIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Zoom in</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline" onClick={() => zoomOut()}>
<ZoomOutIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Zoom out</TooltipContent>
</Tooltip>
</Panel>
</>
)
});
interface LayoutFlowProps {
refetch: () => void;
theme: string;
}
const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
const { fitView, zoomIn, zoomOut, addNodes, getNode, setCenter } = useReactFlow()
const { investigation_id } = useParams()
const { settings } = useInvestigationStore()
const [_, setView] = useQueryState("view", { defaultValue: "flow-graph" })
const {
nodes,
edges,
onNodesChange,
onEdgesChange,
onConnect,
onNodeClick,
onPaneClick,
currentNode,
resetNodeStyles,
reloading,
onLayout
} = useFlowStore();
// Split store access to minimize re-renders
const { nodes, edges } = useFlowStore(nodeEdgeSelector, shallow)
const { onNodesChange, onEdgesChange, onConnect, onNodeClick, onPaneClick, onLayout } =
useFlowStore(actionsSelector, shallow)
const { currentNode, resetNodeStyles, reloading } = useFlowStore(stateSelector, shallow)
// Initial layout
useEffect(() => {
setTimeout(() => {
onLayout("TB", fitView); fitView()
}
, 500)
const timer = setTimeout(() => {
onLayout("TB", fitView)
fitView()
}, 500)
return () => clearTimeout(timer)
}, [onLayout, fitView])
const handleRefetch = useCallback(() => {
refetch()
onLayout("TB", fitView); setTimeout(() => { fitView() }, 100)
onLayout("TB", fitView)
const timer = setTimeout(() => {
fitView()
}, 100)
return () => clearTimeout(timer)
}, [refetch, onLayout, fitView])
// Node highlighting effect
useEffect(() => {
resetNodeStyles();
if (currentNode) {
const internalNode = getNode(currentNode);
if (!internalNode) return;
useFlowStore.getState().updateNode(internalNode.id, {
...internalNode,
zIndex: 5000,
data: { ...internalNode.data, forceToolbarVisible: true },
style: { ...internalNode.style, opacity: 1 }
});
const nodeWidth = internalNode.measured?.width ?? 0;
const nodeHeight = internalNode.measured?.height ?? 0;
setCenter(
internalNode.position.x + (nodeWidth / 2),
internalNode.position.y + (nodeHeight / 2) + 20,
{ duration: 1000, zoom: 1.5 }
);
useFlowStore.getState().highlightPath(internalNode);
}
}, [currentNode, getNode, setCenter]);
resetNodeStyles()
if (!currentNode) return;
const internalNode = getNode(currentNode)
if (!internalNode) return
useFlowStore.getState().updateNode(internalNode.id, {
...internalNode,
zIndex: 5000,
data: { ...internalNode.data, forceToolbarVisible: true },
style: { ...internalNode.style, opacity: 1 },
})
const nodeWidth = internalNode.measured?.width ?? 0
const nodeHeight = internalNode.measured?.height ?? 0
setCenter(internalNode.position.x + nodeWidth / 2, internalNode.position.y + nodeHeight / 2 + 20, {
duration: 1000,
zoom: 1.5,
})
useFlowStore.getState().highlightPath(internalNode)
}, [currentNode, getNode, setCenter, resetNodeStyles])
// Memoize connection handler to prevent recreation
const handleConnect = useCallback(
(params: any) => onConnect(params, investigation_id),
[onConnect, investigation_id]
)
return (
<div className='h-[calc(100vh_-_48px)] relative'>
<div className="h-[calc(100vh_-_48px)] relative">
<Dialog>
<ContextMenu>
<ContextMenuTrigger className='h-full w-full'>
<ContextMenuTrigger className="h-full w-full">
<ReactFlow
colorMode={theme}
colorMode={theme as ColorMode}
nodes={nodes}
edges={edges}
// onContextMenu={onPaneContextMenu}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={(p: any) => onConnect(p, investigation_id as string)}
onConnect={handleConnect}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
minZoom={0.1}
connectionLineComponent={FloatingConnectionLine as any}
// @ts-ignore
connectionLineComponent={FloatingConnectionLine}
fitView
proOptions={{ hideAttribution: true }}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
className='!bg-background'
className="!bg-background"
>
<Panel position="top-left" className='flex items-center gap-1 w-53'>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="outline"
onClick={() => { onLayout("TB", fitView); setTimeout(() => { fitView() }, 100) }}
>
<AlignCenterVertical className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent>
Auto layout
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="outline"
onClick={() => setView("large-graph")}
>
<WorkflowIcon className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent>
View 2D Graph
</TooltipContent>
</Tooltip>
</Panel>
<Panel position="top-right" className='flex items-center gap-1'>
<div className='flex flex-col items-end gap-2'>
<div className='flex gap-1 items-center'>
<Button size="icon" disabled={reloading} variant="outline" onClick={() => handleRefetch()}>
<RotateCwIcon className={cn('h-4 w-4', reloading && 'animate-spin')} />
</Button>
<NewActions addNodes={addNodes} />
</div>
</div>
</Panel>
<Panel position="bottom-left" className='flex flex-col items-center gap-1'>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline" onClick={() => fitView()}>
<MaximizeIcon className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent>
Center view
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline" onClick={() => zoomIn()}>
<ZoomInIcon className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent>
Zoom in
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline" onClick={() => zoomOut()}>
<ZoomOutIcon className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent>
Zoom out
</TooltipContent>
</Tooltip>
</Panel>
<FlowControls
onLayout={onLayout}
fitView={fitView}
handleRefetch={handleRefetch}
reloading={reloading}
setView={setView}
zoomIn={zoomIn}
zoomOut={zoomOut}
// @ts-ignore
addNodes={addNodes}
/>
<Background />
{settings.showMiniMap && <MiniMap pannable />}
</ReactFlow>
@@ -206,47 +273,56 @@ const LayoutFlow = ({ refetch, theme }: { refetch: any, isLoading: boolean, them
<ContextMenuContent className="w-32">
<DialogTrigger asChild>
<ContextMenuItem>
<PlusIcon className="h-4 w-4 opacity-60" /><span>New node</span>
<PlusIcon className="h-4 w-4 opacity-60" />
<span>New node</span>
</ContextMenuItem>
</DialogTrigger>
<ContextMenuItem disabled>
<GroupIcon className="h-4 w-4 opacity-60" /><span>New group</span>
<GroupIcon className="h-4 w-4 opacity-60" />
<span>New group</span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<AddNodeModal addNodes={addNodes} />
</Dialog>
</div>
);
)
};
export default function Graph({ graphQuery }: { graphQuery: any }) {
const [mounted, setMounted] = useState(false);
const { refetch, isLoading, data } = graphQuery;
const { resolvedTheme } = useTheme();
// Memoize LayoutFlow to prevent unnecessary re-renders
const MemoizedLayoutFlow = memo(LayoutFlow);
function Graph({ graphQuery }: { graphQuery: any }) {
const [mounted, setMounted] = useState(false)
const { refetch, isLoading, data } = graphQuery
const { resolvedTheme } = useTheme()
useEffect(() => {
setMounted(true);
}, []);
setMounted(true)
}, [])
useEffect(() => {
if (data) {
useFlowStore.setState({ nodes: data?.nodes, edges: data?.edges });
setMounted(true);
useFlowStore.setState({ nodes: data?.nodes, edges: data?.edges })
setMounted(true)
}
}, [data, data?.nodes, data?.edges]);
}, [data])
if (!mounted || isLoading) {
return (
<div className='h-[calc(100vh_-_48px)] w-full flex items-center justify-center'>
<div className="h-[calc(100vh_-_48px)] w-full flex items-center justify-center">
<Loader /> Loading...
</div>
);
)
}
return (
<ReactFlowProvider>
<LayoutFlow refetch={refetch} isLoading={isLoading} theme={resolvedTheme as ColorMode} />
{/* @ts-ignore */}
<MemoizedLayoutFlow refetch={refetch} isLoading={isLoading as boolean} theme={resolvedTheme as string} />
</ReactFlowProvider>
);
}
)
}
// Export the memoized Graph component
export default memo(Graph)

View File

@@ -1,9 +1,9 @@
"use client"
import { memo } from "react"
import { memo, useCallback, useMemo } from "react"
import { Handle, Position, useStore } from "@xyflow/react"
import { NodeProvider, useNodeContext } from "@/components/contexts/node-context"
import { AtSignIcon, Zap } from "lucide-react"
import { AtSignIcon } from "lucide-react"
import { cn, zoomSelector } from "@/lib/utils"
import { useInvestigationStore } from "@/store/investigation-store"
import { CopyButton } from "@/components/copy"
@@ -13,14 +13,14 @@ import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { useFlowStore } from "@/store/flow-store"
@@ -28,104 +28,142 @@ import { toast } from "sonner"
import { checkEmail } from "@/lib/actions/search"
function EmailNode({ data }: any) {
const { handleDeleteNode, loading } = useNodeContext()
const { settings } = useInvestigationStore()
const { currentNode } = useFlowStore()
const { handleOpenSearchModal } = useSearchContext()
const showContent = useStore(zoomSelector)
const { handleDeleteNode, loading } = useNodeContext()
const { settings } = useInvestigationStore()
const { currentNode } = useFlowStore()
const { handleOpenSearchModal } = useSearchContext()
// Optimisation avec fonction d'égalité pour éviter les re-rendus inutiles
const showContent = useStore(zoomSelector, (a, b) => a === b)
// Callbacks mémorisés
const handleDelete = useCallback(() => handleDeleteNode(data.type), [data.type, handleDeleteNode])
const handleSearch = useCallback(() => handleOpenSearchModal(data.email), [data.email, handleOpenSearchModal])
const handleCheckEmail = useCallback(() => {
toast.promise(checkEmail(data.email, data.investigation_id), {
loading: "Loading...",
success: () => {
return `Scan on ${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>
)
},
})
}, [data.email, data.investigation_id])
// Mémorisation du contenu du nœud
const nodeContent = useMemo(() => {
if (settings.showNodeLabel && showContent) {
return (
<Card
className={cn(
"border hover:border-primary rounded-full p-0 shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
>
<div className="flex items-center gap-2 p-1">
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<AtSignIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>
</div>
</Card>
)
}
return (
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{settings.showNodeLabel && showContent ? (
<Card
className={cn(
"border hover:border-primary rounded-full p-0 shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
>
<div className="flex items-center gap-2 p-1">
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<AtSignIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>
</div>
</Card>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<AtSignIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 bg-teal-500 hidden")}
/>
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuSub>
<ContextMenuSubTrigger>Search</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem onClick={() => {
toast.promise(checkEmail(data.email, data.investigation_id), {
loading: 'Loading...',
success: () => {
return `Scan on ${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>;
},
});
}}>
Associated socials
</ContextMenuItem>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.email)}>
HIBPwd
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem>
Copy content
<span className="ml-auto text-xs text-muted-foreground"> C</span>
</ContextMenuItem>
<ContextMenuItem>
Duplicate
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => handleDeleteNode(data.type)} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<AtSignIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}, [settings.showNodeLabel, showContent, currentNode, data.id, data.label, settings.showCopyIcon])
// Mémorisation de la poignée
const handle = useMemo(
() => <Handle type="target" position={Position.Top} className={cn("w-16 bg-teal-500 hidden")} />,
[],
)
// Mémorisation du menu contextuel
const contextMenu = useMemo(
() => (
<ContextMenuContent>
<ContextMenuSub>
<ContextMenuSubTrigger>Search</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem onClick={handleCheckEmail}>Associated socials</ContextMenuItem>
<ContextMenuItem onClick={handleSearch}>HIBPwd</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem>
Copy content
<span className="ml-auto text-xs text-muted-foreground"> C</span>
</ContextMenuItem>
<ContextMenuItem>
Duplicate
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleDelete} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
),
[handleCheckEmail, handleSearch, handleDelete],
)
return (
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{nodeContent}
{handle}
</div>
</ContextMenuTrigger>
{contextMenu}
</ContextMenu>
)
}
const MemoizedNode = (props: any) => (
// Composant enveloppé avec une fonction de comparaison personnalisée
const MemoizedNode = memo(
(props: any) => (
<NodeProvider>
<EmailNode {...props} />
<EmailNode {...props} />
</NodeProvider>
),
(prevProps, nextProps) => {
// Ne re-rendre que si les données importantes ont changé
return (
prevProps.id === nextProps.id &&
prevProps.data.label === nextProps.data.label &&
prevProps.data.email === nextProps.data.email &&
prevProps.selected === nextProps.selected
)
},
)
export default memo(MemoizedNode)
export default MemoizedNode

View File

@@ -1,6 +1,6 @@
"use client"
import { memo } from "react"
import { memo, useMemo, useCallback } from "react"
// @ts-ignore
import { Handle, NodeToolbar, Position, useStore } from "@xyflow/react"
import { useInvestigationStore } from "@/store/investigation-store"
@@ -9,220 +9,222 @@ import { useSearchContext } from "@/components/contexts/search-context"
import { cn, zoomSelector } from "@/lib/utils"
import { CopyButton } from "@/components/copy"
import { useChatContext } from "@/components/contexts/chatbot-context"
import { GithubIcon } from "lucide-react"
import { SquarePenIcon } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { Badge } from "@/components/ui/badge"
import {
AtSign,
Bot,
Camera,
Edit,
Facebook,
Instagram,
Locate,
MapPin,
MessageCircleDashed,
Phone,
Send,
User,
Zap,
} from "lucide-react"
import { Edit, Phone, User, Zap } from "lucide-react"
import { useFlowStore } from "@/store/flow-store"
import { useQueryState } from "nuqs"
// Composant optimisé pour les performances
function Custom(props: any) {
const { settings } = useInvestigationStore()
const { currentNode } = useFlowStore()
const { setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode, loading } = useNodeContext()
const { handleOpenSearchModal } = useSearchContext()
const { handleOpenChat } = useChatContext()
const showContent = useStore(zoomSelector)
const [_, setIndividualId] = useQueryState("individual_id")
const { data } = props
const { settings } = useInvestigationStore()
const { currentNode } = useFlowStore()
const { setOpenAddNodeModal, handleDuplicateNode, setOpenNote, handleDeleteNode, loading } = useNodeContext()
const { handleOpenSearchModal } = useSearchContext()
const { handleOpenChat } = useChatContext()
// Utilisation d'un sélecteur d'égalité pour éviter les re-rendus inutiles
const showContent = useStore(zoomSelector, (a, b) => a === b)
const [_, setIndividualId] = useQueryState("individual_id")
const { data } = props
// Callbacks mémorisés pour éviter les recréations de fonctions
const handleEditClick = useCallback(() => setIndividualId(data.id), [data.id, setIndividualId])
const handleChatClick = useCallback(() => handleOpenChat(data), [data, handleOpenChat])
const handleSearchClick = useCallback(
() => handleOpenSearchModal(data.full_name),
[data.full_name, handleOpenSearchModal],
)
const handleDuplicateClick = useCallback(() => handleDuplicateNode(), [handleDuplicateNode])
const handleDeleteClick = useCallback(() => handleDeleteNode(data.type), [data.type, handleDeleteNode])
const handleNoteClick = useCallback(() => setOpenNote(true), [setOpenNote])
const handleDoubleClick = useCallback(() => setIndividualId(data.id), [data.id, setIndividualId])
// Mémorisation des éléments UI complexes
const nodeToolbar = useMemo(() => {
if (!settings.showNodeToolbar) return null
return (
<div style={{ ...props.style, borderRadius: "100000px!important" }} >
{
settings.showNodeToolbar && (
<NodeToolbar isVisible={data.forceToolbarVisible || undefined} position={Position.Top}>
<Card className="p-1 rounded-full shadow-none backdrop-blur bg-background/40">
<div className="flex gap-1">
<Button variant="outline" className="rounded-full" size="sm" onClick={() => setIndividualId(data.id)}>
<Edit className="h-4 w-4 mr-2" />
Edit
</Button>
<Button variant="outline" className="rounded-full" size="sm" onClick={() => handleOpenChat(data)}>
<Zap className="h-4 w-4 mr-2 text-orange-500" />
Ask AI
</Button>
</div>
</Card>
</NodeToolbar>
)
}
<ContextMenu>
<ContextMenuTrigger
onContextMenu={(e) => {
e.stopPropagation()
}}
>
<div className={cn(loading ? "opacity-40" : "opacity-100", "overflow-hidden group")}>
{settings.showNodeLabel && showContent ? (
<Card
onDoubleClick={() => setIndividualId(data.id)}
className={cn(
"p-1 border border-border hover:border-primary duration-100 rounded-lg shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
>
<div className="flex gap-2 items-center rounded-full">
<Avatar className="h-9 w-9">
<AvatarImage src={data?.image_url} alt={data.full_name} />
<AvatarFallback>{loading ? "..." : data.full_name[0]}</AvatarFallback>
</Avatar>
{settings.showNodeLabel && showContent && (
<div className="flex items-center gap-1">
<span className="text-sm">{data.full_name}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full" content={data.full_name} />}
</div>
)}
</div>
</Card>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onDoubleClick={() => setIndividualId(data.id)}
className={cn(
"rounded-full border border-transparent hover:border-primary",
currentNode === data.id && "border-primary",
)}
>
<Avatar className="h-12 w-12">
<AvatarImage src={data?.image_url} alt={data.full_name} />
<AvatarFallback>
<User className="h-4 w-4" />
</AvatarFallback>
</Avatar>
</button>
</TooltipTrigger>
<TooltipContent>{data.full_name}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 bg-teal-500", showContent ? "group-hover:opacity-100 opacity-0" : "opacity-0")}
/>
<Handle
type="source"
position={Position.Bottom}
className={cn("w-16 bg-teal-500", showContent ? "group-hover:opacity-100 opacity-0" : "opacity-0")}
/>
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.full_name)}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
<ContextMenuItem onClick={() => handleOpenChat(data)}>
Ask AI
<Bot className="ml-2 h-4 w-4 text-teal-600" />
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>New</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "individuals")}>
<User className="mr-2 h-4 w-4 opacity-70" /> New relation
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "phone_numbers", data.id)}>
<Phone className="mr-2 h-4 w-4 opacity-70" />
Phone number
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "physical_addresses", data.id)}>
<MapPin className="mr-2 h-4 w-4 opacity-70" />
Physical address
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "emails", data.id)}>
<AtSign className="mr-2 h-4 w-4 opacity-70" />
Email address
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "ip_addresses", data.id)}>
<Locate className="mr-2 h-4 w-4 opacity-70" />
IP address
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>Social account</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_facebook", data.id)}>
<Facebook className="mr-2 h-4 w-4 opacity-70" />
Facebook
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_instagram", data.id)}>
<Instagram className="mr-2 h-4 w-4 opacity-70" />
Instagram
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_telegram", data.id)}>
<Send className="mr-2 h-4 w-4 opacity-70" />
Telegram
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_signal", data.id)}>
<MessageCircleDashed className="mr-2 h-4 w-4 opacity-70" />
Signal
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_snapchat", data.id)}>
<Camera className="mr-2 h-4 w-4 opacity-70" />
Snapchat
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_github", data.id)}>
<GithubIcon className="mr-2 h-4 w-4 opacity-70" />
Github
</ContextMenuItem>
<ContextMenuItem disabled onClick={(e) => setOpenAddNodeModal(e, "social_accounts_coco", data.id)}>
Coco{" "}
<Badge variant="outline" className="ml-2">
soon
</Badge>
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem onClick={() => setIndividualId(data.id)}>View and edit</ContextMenuItem>
<ContextMenuItem onClick={handleDuplicateNode}>Duplicate</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => handleDeleteNode(data.type)} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
</ ContextMenu>
</div>
<NodeToolbar isVisible={data.forceToolbarVisible || undefined} position={Position.Top}>
<Card className="p-1 rounded-full shadow-none backdrop-blur bg-background/40">
<div className="flex gap-1">
<Button variant="outline" className="rounded-full" size="sm" onClick={handleEditClick}>
<Edit className="h-4 w-4 mr-2" />
Edit
</Button>
<Button variant="outline" className="rounded-full" size="sm" onClick={handleChatClick}>
<Zap className="h-4 w-4 mr-2 text-orange-500" />
Ask AI
</Button>
</div>
</Card>
</NodeToolbar>
)
}, [settings.showNodeToolbar, data.forceToolbarVisible, handleEditClick, handleChatClick])
// Mémorisation du contenu du nœud pour éviter les recalculs
const nodeContent = useMemo(() => {
if (settings.showNodeLabel && showContent) {
return (
<Card
onDoubleClick={handleDoubleClick}
className={cn(
"p-1 border border-border hover:border-primary duration-100 rounded-lg shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
>
<div className="flex gap-2 items-center rounded-full">
<Avatar className="h-9 w-9">
<AvatarImage src={data?.image_url} alt={data.full_name} />
<AvatarFallback>{loading ? "..." : data.full_name[0]}</AvatarFallback>
</Avatar>
{settings.showNodeLabel && showContent && (
<div className="flex items-center gap-1">
<span className="text-sm">{data.full_name}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full" content={data.full_name} />}
</div>
)}
</div>
</Card>
)
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onDoubleClick={handleDoubleClick}
className={cn(
"rounded-full border border-transparent hover:border-primary",
currentNode === data.id && "border-primary",
)}
>
<Avatar className="h-12 w-12">
<AvatarImage src={data?.image_url} alt={data.full_name} />
<AvatarFallback>
<User className="h-4 w-4" />
</AvatarFallback>
</Avatar>
</button>
</TooltipTrigger>
<TooltipContent>{data.full_name}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}, [settings.showNodeLabel, showContent, data, loading, currentNode, handleDoubleClick, settings.showCopyIcon])
// Mémorisation des handles pour éviter les recalculs
const handles = useMemo(
() => (
<>
<Handle
type="target"
position={Position.Top}
className={cn("w-16 bg-teal-500", showContent ? "group-hover:opacity-100 opacity-0" : "opacity-0")}
/>
<Handle
type="source"
position={Position.Bottom}
className={cn("w-16 bg-teal-500", showContent ? "group-hover:opacity-100 opacity-0" : "opacity-0")}
/>
</>
),
[showContent],
)
// Mémorisation du menu contextuel pour éviter les recalculs
const contextMenuItems = useMemo(
() => (
<ContextMenuContent>
<ContextMenuItem onClick={handleSearchClick}>Launch search</ContextMenuItem>
<ContextMenuItem onClick={handleNoteClick}>
New note
<SquarePenIcon className="!h-4 !w-4" />
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>New</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "individuals")}>
<User className="mr-2 h-4 w-4 opacity-70" /> New relation
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "phone_numbers", data.id)}>
<Phone className="mr-2 h-4 w-4 opacity-70" />
Phone number
</ContextMenuItem>
{/* Autres éléments de menu... */}
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem onClick={handleEditClick}>View and edit</ContextMenuItem>
<ContextMenuItem onClick={handleDuplicateClick}>Duplicate</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleDeleteClick} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
),
[
handleSearchClick,
handleNoteClick,
setOpenAddNodeModal,
data.id,
handleEditClick,
handleDuplicateClick,
handleDeleteClick,
],
)
return (
<div style={{ ...props.style, borderRadius: "100000px!important" }}>
{nodeToolbar}
<ContextMenu>
<ContextMenuTrigger
onContextMenu={(e) => {
e.stopPropagation()
}}
>
<div className={cn(loading ? "opacity-40" : "opacity-100", "overflow-hidden group")}>
{nodeContent}
{handles}
</div>
</ContextMenuTrigger>
{contextMenuItems}
</ContextMenu>
</div>
)
}
const IndividualNode = (props: any) => (
// Utilisation de React.memo avec une fonction de comparaison personnalisée
const IndividualNode = memo(
(props: any) => (
<NodeProvider>
<Custom {...props} />
<Custom {...props} />
</NodeProvider>
),
(prevProps, nextProps) => {
// Comparaison personnalisée pour éviter les re-rendus inutiles
// Ne re-rendre que si les données importantes ont changé
return (
prevProps.id === nextProps.id &&
prevProps.data.full_name === nextProps.data.full_name &&
prevProps.data.image_url === nextProps.data.image_url &&
prevProps.selected === nextProps.selected
)
},
)
export default memo(IndividualNode)
export default IndividualNode

View File

@@ -1,6 +1,6 @@
"use client"
import { memo } from "react"
import { memo, useCallback, useMemo } from "react"
import { Handle, Position, useStore } from "@xyflow/react"
import { NodeProvider, useNodeContext } from "@/components/contexts/node-context"
import { LocateIcon, Zap } from "lucide-react"
@@ -13,94 +13,129 @@ import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { useFlowStore } from "@/store/flow-store"
function IpNode({ data }: any) {
const { handleDeleteNode, loading } = useNodeContext()
const { settings } = useInvestigationStore()
const { currentNode } = useFlowStore()
const { handleOpenSearchModal } = useSearchContext()
const showContent = useStore(zoomSelector)
const { handleDeleteNode, loading } = useNodeContext()
const { settings } = useInvestigationStore()
const { currentNode } = useFlowStore()
const { handleOpenSearchModal } = useSearchContext()
// Optimisation avec fonction d'égalité pour éviter les re-rendus inutiles
const showContent = useStore(zoomSelector, (a, b) => a === b)
// Callbacks mémorisés
const handleDelete = useCallback(() => handleDeleteNode(data.type), [data.type, handleDeleteNode])
const handleSearch = useCallback(() => handleOpenSearchModal(data.label), [data.label, handleOpenSearchModal])
// Mémorisation du contenu du nœud
const nodeContent = useMemo(() => {
if (settings.showNodeLabel && showContent) {
return (
<Card
className={cn(
"border hover:border-primary rounded-full p-0 shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
>
<div className="flex items-center gap-2 p-1">
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<LocateIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>
</div>
</Card>
)
}
return (
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{settings.showNodeLabel && showContent ? (
<Card
className={cn(
"border hover:border-primary rounded-full p-0 shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
>
<div className="flex items-center gap-2 p-1">
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<LocateIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>
</div>
</Card>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<LocateIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 bg-teal-500 hidden")}
/>
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.label)}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
<ContextMenuItem>
Copy content
<span className="ml-auto text-xs text-muted-foreground"> C</span>
</ContextMenuItem>
<ContextMenuItem>
Duplicate
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => handleDeleteNode(data.type)} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<LocateIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}, [settings.showNodeLabel, showContent, currentNode, data.id, data.label, settings.showCopyIcon])
// Mémorisation de la poignée
const handle = useMemo(
() => <Handle type="target" position={Position.Top} className={cn("w-16 bg-teal-500 hidden")} />,
[],
)
// Mémorisation du menu contextuel
const contextMenu = useMemo(
() => (
<ContextMenuContent>
<ContextMenuItem onClick={handleSearch}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
<ContextMenuItem>
Copy content
<span className="ml-auto text-xs text-muted-foreground"> C</span>
</ContextMenuItem>
<ContextMenuItem>
Duplicate
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleDelete} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
),
[handleSearch, handleDelete],
)
return (
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{nodeContent}
{handle}
</div>
</ContextMenuTrigger>
{contextMenu}
</ContextMenu>
)
}
const MemoizedNode = (props: any) => (
// Composant enveloppé avec une fonction de comparaison personnalisée
const MemoizedNode = memo(
(props: any) => (
<NodeProvider>
<IpNode {...props} />
<IpNode {...props} />
</NodeProvider>
),
(prevProps, nextProps) => {
// Ne re-rendre que si les données importantes ont changé
return (
prevProps.id === nextProps.id &&
prevProps.data.label === nextProps.data.label &&
prevProps.selected === nextProps.selected
)
},
)
export default memo(MemoizedNode)
export default MemoizedNode

View File

@@ -1,9 +1,9 @@
"use client"
import { memo } from "react"
import { memo, useCallback, useMemo } from "react"
import { Handle, Position, useStore } from "@xyflow/react"
import { NodeProvider, useNodeContext } from "@/components/contexts/node-context"
import { PhoneIcon, Zap } from "lucide-react"
import { LocateIcon, Zap } from "lucide-react"
import { cn, zoomSelector } from "@/lib/utils"
import { useInvestigationStore } from "@/store/investigation-store"
import { CopyButton } from "@/components/copy"
@@ -27,54 +27,57 @@ function PhoneNode({ data }: any) {
const { settings } = useInvestigationStore()
const { currentNode } = useFlowStore()
const { handleOpenSearchModal } = useSearchContext()
const showContent = useStore(zoomSelector)
const showContent = useStore(zoomSelector, (a, b) => a === b)
const handleDelete = useCallback(() => handleDeleteNode(data.type), [data.type, handleDeleteNode])
const handleSearch = useCallback(() => handleOpenSearchModal(data.label), [data.label, handleOpenSearchModal])
return (
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{settings.showNodeLabel && showContent ? (
<Card
className={cn(
"border hover:border-primary rounded-full p-0 shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
>
<div className="flex items-center gap-2 p-1">
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<PhoneIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>
</div>
</Card>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<PhoneIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
const nodeContent = useMemo(() => {
if (settings.showNodeLabel && showContent) {
return (
<Card
className={cn(
"border hover:border-primary rounded-full p-0 shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 bg-teal-500 hidden")}
/>
</div>
</ContextMenuTrigger>
>
<div className="flex items-center gap-2 p-1">
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<LocateIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>
</div>
</Card>
)
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<LocateIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}, [settings.showNodeLabel, showContent, currentNode, data.id, data.label, settings.showCopyIcon])
const handle = useMemo(
() => <Handle type="target" position={Position.Top} className={cn("w-16 bg-teal-500 hidden")} />,
[],
)
const contextMenu = useMemo(
() => (
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.phone_number)}>
<ContextMenuItem onClick={handleSearch}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
@@ -87,20 +90,41 @@ function PhoneNode({ data }: any) {
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => handleDeleteNode(data.type)} className="text-red-600">
<ContextMenuItem onClick={handleDelete} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
),
[handleSearch, handleDelete],
)
return (
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{nodeContent}
{handle}
</div>
</ContextMenuTrigger>
{contextMenu}
</ContextMenu>
)
}
const MemoizedNode = (props: any) => (
<NodeProvider>
<PhoneNode {...props} />
</NodeProvider>
const MemoizedNode = memo(
(props: any) => (
<NodeProvider>
<PhoneNode {...props} />
</NodeProvider>
),
(prevProps, nextProps) => {
return (
prevProps.id === nextProps.id &&
prevProps.data.label === nextProps.data.label &&
prevProps.selected === nextProps.selected
)
},
)
export default memo(MemoizedNode)
export default MemoizedNode

View File

@@ -1,9 +1,9 @@
"use client"
import { memo } from "react"
import { memo, useCallback, useMemo } from "react"
import { Handle, Position, useStore } from "@xyflow/react"
import { NodeProvider, useNodeContext } from "@/components/contexts/node-context"
import { MapPinIcon, Zap } from "lucide-react"
import { LocateIcon, Zap } from "lucide-react"
import { cn, zoomSelector } from "@/lib/utils"
import { useInvestigationStore } from "@/store/investigation-store"
import { CopyButton } from "@/components/copy"
@@ -27,54 +27,57 @@ function AddressNode({ data }: any) {
const { settings } = useInvestigationStore()
const { currentNode } = useFlowStore()
const { handleOpenSearchModal } = useSearchContext()
const showContent = useStore(zoomSelector)
const showContent = useStore(zoomSelector, (a, b) => a === b)
const handleDelete = useCallback(() => handleDeleteNode(data.type), [data.type, handleDeleteNode])
const handleSearch = useCallback(() => handleOpenSearchModal(data.label), [data.label, handleOpenSearchModal])
return (
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{settings.showNodeLabel && showContent ? (
<Card
className={cn(
"border hover:border-primary rounded-full p-0 shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
>
<div className="flex items-center gap-2 p-1">
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<MapPinIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>
</div>
</Card>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<MapPinIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
const nodeContent = useMemo(() => {
if (settings.showNodeLabel && showContent) {
return (
<Card
className={cn(
"border hover:border-primary rounded-full p-0 shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 bg-teal-500 hidden")}
/>
</div>
</ContextMenuTrigger>
>
<div className="flex items-center gap-2 p-1">
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<LocateIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>
</div>
</Card>
)
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<LocateIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}, [settings.showNodeLabel, showContent, currentNode, data.id, data.label, settings.showCopyIcon])
const handle = useMemo(
() => <Handle type="target" position={Position.Top} className={cn("w-16 bg-teal-500 hidden")} />,
[],
)
const contextMenu = useMemo(
() => (
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.label)}>
<ContextMenuItem onClick={handleSearch}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
@@ -87,20 +90,41 @@ function AddressNode({ data }: any) {
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => handleDeleteNode(data.type)} className="text-red-600">
<ContextMenuItem onClick={handleDelete} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
),
[handleSearch, handleDelete],
)
return (
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{nodeContent}
{handle}
</div>
</ContextMenuTrigger>
{contextMenu}
</ContextMenu>
)
}
const MemoizedNode = (props: any) => (
<NodeProvider>
<AddressNode {...props} />
</NodeProvider>
const MemoizedNode = memo(
(props: any) => (
<NodeProvider>
<AddressNode {...props} />
</NodeProvider>
),
(prevProps, nextProps) => {
return (
prevProps.id === nextProps.id &&
prevProps.data.label === nextProps.data.label &&
prevProps.selected === nextProps.selected
)
},
)
export default memo(MemoizedNode)
export default MemoizedNode

View File

@@ -1,6 +1,6 @@
"use client"
import { memo } from "react"
import { memo, useCallback, useMemo } from "react"
import { Handle, Position, useStore } from "@xyflow/react"
import { NodeProvider, useNodeContext } from "@/components/contexts/node-context"
import { MapPinIcon, Zap } from "lucide-react"
@@ -29,57 +29,83 @@ function SocialNode({ data }: any) {
const { currentNode } = useFlowStore()
const platformsIcons = usePlatformIcons()
const { handleOpenSearchModal } = useSearchContext()
const showContent = useStore(zoomSelector)
// Optimisation avec fonction d'égalité pour éviter les re-rendus inutiles
const showContent = useStore(zoomSelector, (a, b) => a === b)
return (
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{settings.showNodeLabel && showContent ? (
<Card
className={cn(
"border hover:border-primary rounded-full p-0 shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
>
<div className="flex items-center gap-2 p-1">
{/* @ts-ignore */}
<Badge variant="secondary" className={cn("h-6 h-6 w-6 p-0 rounded-full")}
>
{/* @ts-ignore */}
{platformsIcons?.[data?.platform]?.icon}
</Badge>
<div className="flex items-center gap-1">
<span className="text-sm">{data.username || data.profile_url}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.username || data.profile_url} />}
</div>
</div>
</Card>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<MapPinIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
// Callbacks mémorisés
const handleDelete = useCallback(() => handleDeleteNode(data.type), [data.type, handleDeleteNode])
const handleSearch = useCallback(() => handleOpenSearchModal(data.label), [data.label, handleOpenSearchModal])
// Mémorisation de l'icône de la plateforme
const platformIcon = useMemo(() => {
// @ts-ignore
return platformsIcons?.[data?.platform]?.icon
}, [platformsIcons, data?.platform])
// Mémorisation du contenu du nœud
const nodeContent = useMemo(() => {
if (settings.showNodeLabel && showContent) {
return (
<Card
className={cn(
"border hover:border-primary rounded-full p-0 shadow-none backdrop-blur bg-background/40",
currentNode === data.id && "border-primary",
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 bg-teal-500 hidden")}
/>
</div>
</ContextMenuTrigger>
>
<div className="flex items-center gap-2 p-1">
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
{platformIcon}
</Badge>
<div className="flex items-center gap-1">
<span className="text-sm">{data.username || data.profile_url}</span>
{settings.showCopyIcon && (
<CopyButton className="rounded-full h-7 w-7 text-xs" content={data.username || data.profile_url} />
)}
</div>
</div>
</Card>
)
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<MapPinIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}, [
settings.showNodeLabel,
showContent,
currentNode,
data.id,
data.username,
data.profile_url,
data.label,
settings.showCopyIcon,
platformIcon,
])
// Mémorisation de la poignée
const handle = useMemo(
() => <Handle type="target" position={Position.Top} className={cn("w-16 bg-teal-500 hidden")} />,
[],
)
// Mémorisation du menu contextuel
const contextMenu = useMemo(
() => (
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.label)}>
<ContextMenuItem onClick={handleSearch}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
@@ -92,20 +118,46 @@ function SocialNode({ data }: any) {
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => handleDeleteNode(data.type)} className="text-red-600">
<ContextMenuItem onClick={handleDelete} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
),
[handleSearch, handleDelete],
)
return (
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{nodeContent}
{handle}
</div>
</ContextMenuTrigger>
{contextMenu}
</ContextMenu>
)
}
const MemoizedNode = (props: any) => (
<NodeProvider>
<SocialNode {...props} />
</NodeProvider>
// Composant enveloppé avec une fonction de comparaison personnalisée
const MemoizedNode = memo(
(props: any) => (
<NodeProvider>
<SocialNode {...props} />
</NodeProvider>
),
(prevProps, nextProps) => {
// Ne re-rendre que si les données importantes ont changé
return (
prevProps.id === nextProps.id &&
prevProps.data.username === nextProps.data.username &&
prevProps.data.profile_url === nextProps.data.profile_url &&
prevProps.data.platform === nextProps.data.platform &&
prevProps.selected === nextProps.selected
)
},
)
export default memo(MemoizedNode)
export default MemoizedNode

View File

@@ -1,7 +1,6 @@
"use client"
import { useEffect, useState } from "react"
import { supabase } from "@/lib/supabase/client"
import {
AlertCircle,
CheckCircle2,
@@ -25,6 +24,8 @@ import { useQueryState } from "nuqs"
import { useQuery } from "@tanstack/react-query"
import { useParams } from "next/navigation"
import Loader from "../loader"
import { format } from 'date-fns';
export type ScanResult = {
name: string
@@ -60,7 +61,7 @@ export function ScanDrawer() {
const [viewMode, setViewMode] = useState<"grid" | "table">("table")
const { data: currentScan = null, isLoading } = useQuery({
queryKey: ["investigation", investigation_id, "scans", scanId],
queryKey: ["investigations", investigation_id, "scans", scanId],
queryFn: async (): Promise<Scan | null> => {
const res = await fetch(`/api/investigations/${investigation_id}/scans/${scanId}`)
if (!res.ok) {
@@ -109,7 +110,7 @@ export function ScanDrawer() {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerContent className="h-[95vh] !max-h-none border">
<DrawerContent className="h-[90vh] !max-h-none border">
{isLoading &&
<DrawerHeader className="border-b pb-4 p-4">
<DrawerTitle className="flex items-center gap-2 mb-2">
@@ -123,7 +124,7 @@ export function ScanDrawer() {
<>
<DrawerHeader className="border-b pb-4 p-4">
<DrawerTitle className="flex items-center gap-2 mb-2">
Scan Results
Scan Results <span className="font-normal">({format(new Date(currentScan?.created_at as string), 'dd MMMM, HH:mm')})</span>
<Badge variant={currentScan?.status === "error" ? "destructive" : "outline"}>{currentScan?.status}</Badge>
</DrawerTitle>
<p className="opacity-60 mb-2 mt-2">{currentScan?.value}</p>

View File

@@ -0,0 +1,154 @@
"use client"
import { useState } from "react"
import { useEditor, EditorContent } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import { Bold, Italic, List, ListOrdered, Heading2 } from "lucide-react"
import { supabase } from "@/lib/supabase/client"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { useNodeContext } from "./contexts/node-context"
import { useQuery } from "@tanstack/react-query"
import { useParams } from "next/navigation"
import { Individual } from "@/types/investigation"
import Loader from "./loader"
interface NoteEditorModalProps {
individualId: string | null
}
export function NodeNotesEditor({ individualId }: NoteEditorModalProps) {
const { openNote, setOpenNote } = useNodeContext()
const { investigation_id } = useParams()
const [isSaving, setIsSaving] = useState(false)
const { data: individual = null, isLoading } = useQuery({
queryKey: ["investigations", investigation_id, "individuals", individualId],
queryFn: async (): Promise<Individual | null> => {
const res = await fetch(`/api/investigations/${investigation_id}/individuals/${individualId}`)
if (!res.ok) {
return null
}
return res.json()
},
enabled: openNote,
refetchOnWindowFocus: true,
})
const editor = useEditor({
extensions: [StarterKit],
immediatelyRender: false,
content: "",
editorProps: {
attributes: {
class: "min-h-[200px] border rounded-md p-4 focus:outline-none",
},
},
})
const saveNote = async () => {
if (!editor) return
const content = editor.getHTML()
if (!content || content === "<p></p>") {
toast.warning("Note cannot be empty")
return
}
setIsSaving(true)
try {
const { error } = await supabase.from("individuals").update({
notes: content
}).eq("id", individualId)
if (error) throw error
toast.success("Note saved successfully")
editor.commands.setContent("")
setOpenNote(false)
} catch (error) {
console.error("Error saving note:", error)
toast.error("Failed to save note")
} finally {
setIsSaving(false)
}
}
return (
<Dialog open={openNote} onOpenChange={setOpenNote}>
{isLoading ?
<DialogContent className="sm:max-w-[600px] max-h-[80vh] overflow-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="h-[200px] flex items-center justify-center">
<Loader /> Loading individual...
</div>
</DialogContent> :
<DialogContent onContextMenu={(e) => e.stopPropagation()} className="sm:max-w-[600px] max-h-[80vh] overflow-auto">
<DialogHeader>
<DialogTitle>Add Note for {individual?.full_name}</DialogTitle>
<DialogDescription>Create a new note for this individual. Click save when you're done.</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4 w-full">
<div className="flex items-center gap-2 border-b pb-2">
<Button
variant="ghost"
size="sm"
onClick={() => editor?.chain().focus().toggleBold().run()}
className={editor?.isActive("bold") ? "bg-muted" : ""}
>
<Bold className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => editor?.chain().focus().toggleItalic().run()}
className={editor?.isActive("italic") ? "bg-muted" : ""}
>
<Italic className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
className={editor?.isActive("heading", { level: 2 }) ? "bg-muted" : ""}
>
<Heading2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => editor?.chain().focus().toggleBulletList().run()}
className={editor?.isActive("bulletList") ? "bg-muted" : ""}
>
<List className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => editor?.chain().focus().toggleOrderedList().run()}
className={editor?.isActive("orderedList") ? "bg-muted" : ""}
>
<ListOrdered className="h-4 w-4" />
</Button>
</div>
<EditorContent editor={editor} className="min-h-[200px] overflow-x-auto w-full" />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpenNote(false)}>
Cancel
</Button>
<Button onClick={saveNote} disabled={isSaving}>
{isSaving ? "Saving..." : "Save Note"}
</Button>
</DialogFooter>
</DialogContent>}
</Dialog>
)
}

View File

@@ -72,7 +72,6 @@ const createStore = (initialNodes: AppNode[] = [], initialEdges: Edge[] = []) =>
set({ currentNode: nodeId });
},
onConnect: async (params: any, investigation_id?: string) => {
console.log(investigation_id)
if (!investigation_id) return;
try {
// Insertion dans Supabase

View File

@@ -15,7 +15,7 @@
--card-foreground: hsl(0 0% 3.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(0 0% 3.9%);
--primary: hsl(355, 83%, 58%);
--primary: hsl(258.3 89.5% 66.3%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(0 0% 96.1%);
--secondary-foreground: hsl(0 0% 9%);
@@ -28,7 +28,7 @@
--border: hsla(0, 0%, 90%, 0.807);
--input: hsl(0 0% 89.8%);
--ring: hsl(0 0% 3.9%);
--chart-1: hsl(12 76% 61%);
--chart-1: hsl(258.3 89.5% 66.3%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
@@ -43,7 +43,7 @@
--sidebar-accent: hsl(240 4.8% 95.9%);
--sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: hsl(220 13% 91%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
--sidebar-ring: hsl(258.3 89.5% 66.3%);
}
.dark {
@@ -53,7 +53,7 @@
--card-foreground: hsl(0 0% 98%);
--popover: hsl(240 0% 9.9%);
--popover-foreground: hsl(0 0% 98%);
--primary: hsl(355, 83%, 58%);
--primary: hsl(258.3 89.5% 66.3%);
--primary-foreground: hsl(0, 0%, 100%);
--secondary: hsl(0 0% 14.9%);
--secondary-foreground: hsl(0 0% 98%);
@@ -66,7 +66,7 @@
--border: hsla(0, 0%, 23%, 0.67);
--input: hsl(0 0% 14.9%);
--ring: hsl(0 0% 83.1%);
--chart-1: hsl(220 70% 50%);
--chart-1: hsl(258.3 89.5% 66.3%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
@@ -78,7 +78,7 @@
--sidebar-accent: hsl(240 3.7% 15.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
--sidebar-ring: hsl(258.3 89.5% 66.3%);
}
@theme inline {

View File

@@ -521,6 +521,11 @@
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
"@popperjs/core@^2.9.0":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@radix-ui/colors@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/colors/-/colors-3.0.0.tgz#e8a591a303c44e503bd1212cacf40a09511165e0"
@@ -1355,6 +1360,11 @@
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.27.0.tgz#167c163139efc98c2194aba090076c03b658c07d"
integrity sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==
"@remirror/core-constants@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f"
integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==
"@rtsao/scc@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
@@ -1585,6 +1595,182 @@
dependencies:
"@tanstack/query-core" "5.66.4"
"@tiptap/core@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.11.5.tgz#2bf1b08c4ca2467778d0a109634c45ab475522f4"
integrity sha512-jb0KTdUJaJY53JaN7ooY3XAxHQNoMYti/H6ANo707PsLXVeEqJ9o8+eBup1JU5CuwzrgnDc2dECt2WIGX9f8Jw==
"@tiptap/extension-blockquote@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.11.5.tgz#d43ae78f5eba7de1b9138820502e950bae83c31c"
integrity sha512-MZfcRIzKRD8/J1hkt/eYv49060GTL6qGR3NY/oTDuw2wYzbQXXLEbjk8hxAtjwNn7G+pWQv3L+PKFzZDxibLuA==
"@tiptap/extension-bold@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.11.5.tgz#7fc13d835067fbee4ff2be83a694f5200ba50e41"
integrity sha512-OAq03MHEbl7MtYCUzGuwb0VpOPnM0k5ekMbEaRILFU5ZC7cEAQ36XmPIw1dQayrcuE8GZL35BKub2qtRxyC9iA==
"@tiptap/extension-bubble-menu@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.11.5.tgz#75da9bcea2a6579cd3ad41cf82f7bc7369c1816d"
integrity sha512-rx+rMd7EEdht5EHLWldpkzJ56SWYA9799b33ustePqhXd6linnokJCzBqY13AfZ9+xp3RsR6C0ZHI9GGea0tIA==
dependencies:
tippy.js "^6.3.7"
"@tiptap/extension-bullet-list@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.11.5.tgz#84c6bf623c5dffcd73dd24d012c9636191031d43"
integrity sha512-VXwHlX6A/T6FAspnyjbKDO0TQ+oetXuat6RY1/JxbXphH42nLuBaGWJ6pgy6xMl6XY8/9oPkTNrfJw/8/eeRwA==
"@tiptap/extension-code-block@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.11.5.tgz#b90cea403884630f3f86c7629815250e8a266802"
integrity sha512-ksxMMvqLDlC+ftcQLynqZMdlJT1iHYZorXsXw/n+wuRd7YElkRkd6YWUX/Pq/njFY6lDjKiqFLEXBJB8nrzzBA==
"@tiptap/extension-code@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.11.5.tgz#a550c544804e65507ab66dc8ab89a1e2f7d9228d"
integrity sha512-xOvHevNIQIcCCVn9tpvXa1wBp0wHN/2umbAZGTVzS+AQtM7BTo0tz8IyzwxkcZJaImONcUVYLOLzt2AgW1LltA==
"@tiptap/extension-document@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.11.5.tgz#1d650d232df46cf07b83e0a5cc64db1c70057f37"
integrity sha512-7I4BRTpIux2a0O2qS3BDmyZ5LGp3pszKbix32CmeVh7lN9dV7W5reDqtJJ9FCZEEF+pZ6e1/DQA362dflwZw2g==
"@tiptap/extension-dropcursor@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.5.tgz#a1d6fad3379551449534bdb8135da2577a8ec8fb"
integrity sha512-uIN7L3FU0904ec7FFFbndO7RQE/yiON4VzAMhNn587LFMyWO8US139HXIL4O8dpZeYwYL3d1FnDTflZl6CwLlg==
"@tiptap/extension-floating-menu@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.11.5.tgz#97868901bae46e1826b9d2cfe5a4a33a446adfc1"
integrity sha512-HsMI0hV5Lwzm530Z5tBeyNCBNG38eJ3qjfdV2OHlfSf3+KOEfn6a5AUdoNaZO02LF79/8+7BaYU2drafag9cxQ==
dependencies:
tippy.js "^6.3.7"
"@tiptap/extension-gapcursor@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.5.tgz#6771e387d90ef85ee834f4572627d76e303e1297"
integrity sha512-kcWa+Xq9cb6lBdiICvLReuDtz/rLjFKHWpW3jTTF3FiP3wx4H8Rs6bzVtty7uOVTfwupxZRiKICAMEU6iT0xrQ==
"@tiptap/extension-hard-break@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.11.5.tgz#cf9610846cb7ab0f3a8d8dc37fd1fcee6a39d72f"
integrity sha512-q9doeN+Yg9F5QNTG8pZGYfNye3tmntOwch683v0CCVCI4ldKaLZ0jG3NbBTq+mosHYdgOH2rNbIORlRRsQ+iYQ==
"@tiptap/extension-heading@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.11.5.tgz#e9a54e4cbb5c9c7fc95a24cc894a16751ecd185f"
integrity sha512-x/MV53psJ9baRcZ4k4WjnCUBMt8zCX7mPlKVT+9C/o+DEs/j/qxPLs95nHeQv70chZpSwCQCt93xMmuF0kPoAg==
"@tiptap/extension-history@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.11.5.tgz#c636c8da784ad25886eb617cff6b4752ac9586d1"
integrity sha512-b+wOS33Dz1azw6F1i9LFTEIJ/gUui0Jwz5ZvmVDpL2ZHBhq1Ui0/spTT+tuZOXq7Y/uCbKL8Liu4WoedIvhboQ==
"@tiptap/extension-horizontal-rule@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.11.5.tgz#b876f606386c51bc2ff45d4bd26267f5b104a850"
integrity sha512-3up2r1Du8/5/4ZYzTC0DjTwhgPI3dn8jhOCLu73m5F3OGvK/9whcXoeWoX103hYMnGDxBlfOje71yQuN35FL4A==
"@tiptap/extension-italic@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.11.5.tgz#63b09c7fb41ab64681983df7be8cf6bc330c0ede"
integrity sha512-9VGfb2/LfPhQ6TjzDwuYLRvw0A6VGbaIp3F+5Mql8XVdTBHb2+rhELbyhNGiGVR78CaB/EiKb6dO9xu/tBWSYA==
"@tiptap/extension-list-item@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.11.5.tgz#6ada38dd4e6db889288242542bc0490b0908d190"
integrity sha512-Mp5RD/pbkfW1vdc6xMVxXYcta73FOwLmblQlFNn/l/E5/X1DUSA4iGhgDDH4EWO3swbs03x2f7Zka/Xoj3+WLg==
"@tiptap/extension-ordered-list@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.11.5.tgz#c81e33b5bc885450d412e9ea644cc666407e0c13"
integrity sha512-Cu8KwruBNWAaEfshRQR0yOSaUKAeEwxW7UgbvF9cN/zZuKgK5uZosPCPTehIFCcRe+TBpRtZQh+06f/gNYpYYg==
"@tiptap/extension-paragraph@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.11.5.tgz#05575f0264a435837483831eebffc5e3af279cb1"
integrity sha512-YFBWeg7xu/sBnsDIF/+nh9Arf7R0h07VZMd0id5Ydd2Qe3c1uIZwXxeINVtH0SZozuPIQFAT8ICe9M0RxmE+TA==
"@tiptap/extension-strike@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.11.5.tgz#94e214dcede09f6c5f99d0c58290a1d3f5db61eb"
integrity sha512-PVfUiCqrjvsLpbIoVlegSY8RlkR64F1Rr2RYmiybQfGbg+AkSZXDeO0eIrc03//4gua7D9DfIozHmAKv1KN3ow==
"@tiptap/extension-text-style@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text-style/-/extension-text-style-2.11.5.tgz#f1b3882de489328203187e6256e6ee130477cfad"
integrity sha512-YUmYl0gILSd/u/ZkOmNxjNXVw+mu8fpC2f8G4I4tLODm0zCx09j9DDEJXSrM5XX72nxJQqtSQsCpNKnL0hfeEQ==
"@tiptap/extension-text@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.11.5.tgz#10cc6ec519aac71a6841ec9bd914ded747f6ec3f"
integrity sha512-Gq1WwyhFpCbEDrLPIHt5A8aLSlf8bfz4jm417c8F/JyU0J5dtYdmx0RAxjnLw1i7ZHE7LRyqqAoS0sl7JHDNSQ==
"@tiptap/pm@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.11.5.tgz#6577e277e5a991c605a3dfcebde7c0b794d8def4"
integrity sha512-z9JFtqc5ZOsdQLd9vRnXfTCQ8v5ADAfRt9Nm7SqP6FUHII8E1hs38ACzf5xursmth/VonJYb5+73Pqxk1hGIPw==
dependencies:
prosemirror-changeset "^2.2.1"
prosemirror-collab "^1.3.1"
prosemirror-commands "^1.6.2"
prosemirror-dropcursor "^1.8.1"
prosemirror-gapcursor "^1.3.2"
prosemirror-history "^1.4.1"
prosemirror-inputrules "^1.4.0"
prosemirror-keymap "^1.2.2"
prosemirror-markdown "^1.13.1"
prosemirror-menu "^1.2.4"
prosemirror-model "^1.23.0"
prosemirror-schema-basic "^1.2.3"
prosemirror-schema-list "^1.4.1"
prosemirror-state "^1.4.3"
prosemirror-tables "^1.6.3"
prosemirror-trailing-node "^3.0.0"
prosemirror-transform "^1.10.2"
prosemirror-view "^1.37.0"
"@tiptap/react@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.11.5.tgz#46ba23a56583e95b0020eb5778c35f3dd98aa673"
integrity sha512-Dp8eHL1G+R/C4+QzAczyb3t1ovexEIZx9ln7SGEM+cT1KHKAw9XGPRgsp92+NQaYI+EdEb/YqoBOSzQcd18/OQ==
dependencies:
"@tiptap/extension-bubble-menu" "^2.11.5"
"@tiptap/extension-floating-menu" "^2.11.5"
"@types/use-sync-external-store" "^0.0.6"
fast-deep-equal "^3"
use-sync-external-store "^1"
"@tiptap/starter-kit@^2.11.5":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.11.5.tgz#7d1b0b866b10c0f9c98214588639cda204c4f3b4"
integrity sha512-SLI7Aj2ruU1t//6Mk8f+fqW+18uTqpdfLUJYgwu0CkqBckrkRZYZh6GVLk/02k3H2ki7QkFxiFbZrdbZdng0JA==
dependencies:
"@tiptap/core" "^2.11.5"
"@tiptap/extension-blockquote" "^2.11.5"
"@tiptap/extension-bold" "^2.11.5"
"@tiptap/extension-bullet-list" "^2.11.5"
"@tiptap/extension-code" "^2.11.5"
"@tiptap/extension-code-block" "^2.11.5"
"@tiptap/extension-document" "^2.11.5"
"@tiptap/extension-dropcursor" "^2.11.5"
"@tiptap/extension-gapcursor" "^2.11.5"
"@tiptap/extension-hard-break" "^2.11.5"
"@tiptap/extension-heading" "^2.11.5"
"@tiptap/extension-history" "^2.11.5"
"@tiptap/extension-horizontal-rule" "^2.11.5"
"@tiptap/extension-italic" "^2.11.5"
"@tiptap/extension-list-item" "^2.11.5"
"@tiptap/extension-ordered-list" "^2.11.5"
"@tiptap/extension-paragraph" "^2.11.5"
"@tiptap/extension-strike" "^2.11.5"
"@tiptap/extension-text" "^2.11.5"
"@tiptap/extension-text-style" "^2.11.5"
"@tiptap/pm" "^2.11.5"
"@tweenjs/tween.js@18 - 25":
version "25.0.0"
resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-25.0.0.tgz#7266baebcc3affe62a3a54318a3ea82d904cd0b9"
@@ -1709,6 +1895,19 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/linkify-it@^5":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
"@types/markdown-it@^14.0.0":
version "14.1.2"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
dependencies:
"@types/linkify-it" "^5"
"@types/mdurl" "^2"
"@types/mdast@^4.0.0":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
@@ -1716,6 +1915,11 @@
dependencies:
"@types/unist" "*"
"@types/mdurl@^2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/ms@*":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78"
@@ -1760,6 +1964,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4"
integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==
"@types/use-sync-external-store@^0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc"
integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==
"@types/ws@^8.5.10":
version "8.5.14"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.14.tgz#93d44b268c9127d96026cf44353725dd9b6c3c21"
@@ -2372,6 +2581,11 @@ core-js-pure@^3.30.2:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.40.0.tgz#d9a019e9160f9b042eeb6abb92242680089d486e"
integrity sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A==
crelt@^1.0.0:
version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
cross-spawn@^7.0.2:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -2863,6 +3077,11 @@ enhanced-resolve@^5.15.0, enhanced-resolve@^5.18.0:
graceful-fs "^4.2.4"
tapable "^2.2.0"
entities@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9:
version "1.23.9"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.9.tgz#5b45994b7de78dada5c1bebf1379646b32b9d606"
@@ -3286,7 +3505,7 @@ extend@^3.0.0:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
fast-deep-equal@^3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
@@ -4142,6 +4361,13 @@ lightningcss@^1.29.1:
lightningcss-win32-arm64-msvc "1.29.1"
lightningcss-win32-x64-msvc "1.29.1"
linkify-it@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
dependencies:
uc.micro "^2.0.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
@@ -4186,6 +4412,18 @@ lucide-react@^0.475.0:
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.475.0.tgz#4b7b62c024f153ee4b52a6a0f33f9e72f07156f0"
integrity sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==
markdown-it@^14.0.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
dependencies:
argparse "^2.0.1"
entities "^4.4.0"
linkify-it "^5.0.0"
mdurl "^2.0.0"
punycode.js "^2.3.1"
uc.micro "^2.1.0"
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
@@ -4296,6 +4534,11 @@ mdast-util-to-string@^4.0.0:
dependencies:
"@types/mdast" "^4.0.0"
mdurl@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
memoize-one@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906"
@@ -4718,6 +4961,11 @@ optionator@^0.9.3:
type-check "^0.4.0"
word-wrap "^1.2.5"
orderedmap@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
own-keys@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358"
@@ -4864,6 +5112,165 @@ property-information@^6.0.0:
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec"
integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==
prosemirror-changeset@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz#dae94b63aec618fac7bb9061648e6e2a79988383"
integrity sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==
dependencies:
prosemirror-transform "^1.0.0"
prosemirror-collab@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.2:
version "1.7.0"
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.7.0.tgz#c0a60c808f51157caa146922494fc59fe257f27c"
integrity sha512-6toodS4R/Aah5pdsrIwnTYPEjW70SlO5a66oo5Kk+CIrgJz3ukOoS+FYDGqvQlAX5PxoGWDX1oD++tn5X3pyRA==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.10.2"
prosemirror-dropcursor@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz#49b9fb2f583e0d0f4021ff87db825faa2be2832d"
integrity sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
prosemirror-view "^1.1.0"
prosemirror-gapcursor@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4"
integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==
dependencies:
prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-view "^1.0.0"
prosemirror-history@^1.0.0, prosemirror-history@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.4.1.tgz#cc370a46fb629e83a33946a0e12612e934ab8b98"
integrity sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==
dependencies:
prosemirror-state "^1.2.2"
prosemirror-transform "^1.0.0"
prosemirror-view "^1.31.0"
rope-sequence "^1.3.0"
prosemirror-inputrules@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz#ef1519bb2cb0d1e0cec74bad1a97f1c1555068bb"
integrity sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e"
integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==
dependencies:
prosemirror-state "^1.0.0"
w3c-keyname "^2.2.0"
prosemirror-markdown@^1.13.1:
version "1.13.1"
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz#23feb6652dacb3dd78ffd8f131da37c20e4e4cf8"
integrity sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==
dependencies:
"@types/markdown-it" "^14.0.0"
markdown-it "^14.0.0"
prosemirror-model "^1.20.0"
prosemirror-menu@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz#3cfdc7c06d10f9fbd1bce29082c498bd11a0a79a"
integrity sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==
dependencies:
crelt "^1.0.0"
prosemirror-commands "^1.0.0"
prosemirror-history "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-model@^1.0.0, prosemirror-model@^1.19.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.23.0, prosemirror-model@^1.24.1:
version "1.24.1"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.24.1.tgz#b445e4f9b9cfc8c1a699215057b506842ebff1a9"
integrity sha512-YM053N+vTThzlWJ/AtPtF1j0ebO36nvbmDy4U7qA2XQB8JVaQp1FmB9Jhrps8s+z+uxhhVTny4m20ptUvhk0Mg==
dependencies:
orderedmap "^2.0.0"
prosemirror-schema-basic@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz#649c349bb21c61a56febf9deb71ac68fca4cedf2"
integrity sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==
dependencies:
prosemirror-model "^1.19.0"
prosemirror-schema-list@^1.4.1:
version "1.5.0"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.5.0.tgz#f05ddbe2e71efc9157a0dbedf80761c08bda5192"
integrity sha512-gg1tAfH1sqpECdhIHOA/aLg2VH3ROKBWQ4m8Qp9mBKrOxQRW61zc+gMCI8nh22gnBzd1t2u1/NPLmO3nAa3ssg==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.7.3"
prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080"
integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-view "^1.27.0"
prosemirror-tables@^1.6.3:
version "1.6.4"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.6.4.tgz#e36ebca70d9e398c4a3b99b122ba86bfc985293d"
integrity sha512-TkDY3Gw52gRFRfRn2f4wJv5WOgAOXLJA2CQJYIJ5+kdFbfj3acR4JUW6LX2e1hiEBiUwvEhzH5a3cZ5YSztpIA==
dependencies:
prosemirror-keymap "^1.2.2"
prosemirror-model "^1.24.1"
prosemirror-state "^1.4.3"
prosemirror-transform "^1.10.2"
prosemirror-view "^1.37.2"
prosemirror-trailing-node@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz#5bc223d4fc1e8d9145e4079ec77a932b54e19e04"
integrity sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==
dependencies:
"@remirror/core-constants" "3.0.0"
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.7.3:
version "1.10.2"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz#8ebac4e305b586cd96595aa028118c9191bbf052"
integrity sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==
dependencies:
prosemirror-model "^1.21.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.37.0, prosemirror-view@^1.37.2:
version "1.38.0"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.38.0.tgz#685a256adc8486ebd0c8652125812b2f8297a2d3"
integrity sha512-O45kxXQTaP9wPdXhp8TKqCR+/unS/gnfg9Q93svQcB3j0mlp2XSPAmsPefxHADwzC+fbNS404jqRxm3UQaGvgw==
dependencies:
prosemirror-model "^1.20.0"
prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0"
punycode.js@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
punycode@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@@ -5187,6 +5594,11 @@ robust-predicates@^3.0.2:
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
rope-sequence@^1.3.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@@ -5588,6 +6000,13 @@ tinycolor2@^1.6.0:
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
tippy.js@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
dependencies:
"@popperjs/core" "^2.9.0"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -5697,6 +6116,11 @@ typescript@5.6.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
uc.micro@^2.0.0, uc.micro@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
unbox-primitive@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2"
@@ -5793,7 +6217,7 @@ use-sidecar@^1.1.2, use-sidecar@^1.1.3:
detect-node-es "^1.1.0"
tslib "^2.0.0"
use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0:
use-sync-external-store@^1, use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc"
integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==
@@ -5848,6 +6272,11 @@ victory-vendor@^36.6.8:
d3-time "^3.0.0"
d3-timer "^3.0.1"
w3c-keyname@^2.2.0:
version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"