feat: editor useRef to prevent re-renders

This commit is contained in:
dextmorgn
2025-09-17 15:21:29 +02:00
parent 73b2c59763
commit 4734ba75ae
4 changed files with 88 additions and 81 deletions

View File

@@ -77,8 +77,8 @@ export const AnalysisEditor = ({
const { investigationId: routeInvestigationId, type } = useParams({ strict: false }) as { investigationId: string, type: string }
const queryClient = useQueryClient()
// State for editor
const [editorValue, setEditorValue] = useState<any>("")
// State/refs for editor
const editorContentRef = useRef<any>("")
const [titleValue, setTitleValue] = useState("")
const [editor, setEditor] = useState<Editor | undefined>(undefined)
const [isEditingTitle, setIsEditingTitle] = useState(false)
@@ -96,7 +96,7 @@ export const AnalysisEditor = ({
// Handle editor content changes
const handleEditorChange = useCallback((value: any) => {
setEditorValue(value)
editorContentRef.current = value
if (analysis) {
setSaveStatus("unsaved")
debouncedSave()
@@ -143,7 +143,7 @@ export const AnalysisEditor = ({
return analysisService.update(analysis.id, JSON.stringify({
...analysis,
...updated,
content: editorValue
content: editorContentRef.current
}))
},
onSuccess: async (data) => {
@@ -223,45 +223,19 @@ export const AnalysisEditor = ({
}
}
// Update editor content when analysis changes
// Update non-editor UI when analysis changes (avoid resetting content on same doc)
useEffect(() => {
if (analysis) {
// Handle both string content and object content
const content = analysis.content
if (typeof content === 'string') {
try {
// Try to parse if it's a JSON string
const parsedContent = JSON.parse(content)
setEditorValue(parsedContent)
if (editor) {
editor.commands.setContent(parsedContent)
}
} catch {
// If parsing fails, treat as plain text and convert to editor format
setEditorValue(content || "")
if (editor) {
editor.commands.setContent(content || "")
}
}
} else {
// If it's already an object, use it directly
setEditorValue(content || "")
if (editor) {
editor.commands.setContent(content || "")
}
}
setTitleValue(analysis.title || "")
setSaveStatus("saved")
} else {
// Reset when no analysis is selected
setEditorValue("")
setTitleValue("")
setSaveStatus("saved")
if (editor) {
editor.commands.setContent("")
}
}
}, [analysis?.id, analysis?.content, analysis?.title, editor])
}, [analysis?.id, analysis?.title, editor])
@@ -441,7 +415,17 @@ export const AnalysisEditor = ({
<MinimalTiptapEditor
key={analysis.id}
immediatelyRender={true}
value={editorValue}
value={(function getInitialContent() {
const content = analysis.content as any
if (typeof content === 'string') {
try {
return JSON.parse(content)
} catch {
return content || ""
}
}
return content || ""
})()}
onChange={handleEditorChange}
className="w-full h-full"
editorContentClassName="p-5 min-h-[300px]"

View File

@@ -21,7 +21,7 @@ export interface MinimalTiptapProps
className?: string
editorContentClassName?: string
onEditorReady?: (editor: Editor) => void
showToolbar?:boolean
showToolbar?: boolean
}
//@ts-ignore
const Toolbar = ({ editor }: { editor: Editor }) => (
@@ -62,10 +62,11 @@ const Toolbar = ({ editor }: { editor: Editor }) => (
export const MinimalTiptapEditor = React.forwardRef<
HTMLDivElement,
MinimalTiptapProps
>(({ value, onChange, className, editorContentClassName, onEditorReady, showToolbar=false, ...props }, ref) => {
>(({ value, onChange, className, editorContentClassName, onEditorReady, showToolbar = false, ...props }, ref) => {
const editor = useMinimalTiptapEditor({
value,
onUpdate: onChange,
shouldRerenderOnTransaction: false,
...props,
})

View File

@@ -19,7 +19,7 @@ import {
} from "@/components/ui/command"
import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"
import { Button } from "./ui/button"
import { Link } from "@tanstack/react-router"
import { useNavigate } from "@tanstack/react-router"
import { useQuery } from "@tanstack/react-query"
import { investigationService } from "@/api/investigation-service"
import { analysisService } from "@/api/analysis-service"
@@ -44,6 +44,7 @@ function CommandSkeleton() {
export function Command() {
const [open, setOpen] = React.useState(false)
const [searchQuery, setSearchQuery] = React.useState("")
const navigate = useNavigate()
// Fetch investigations when dialog opens
const { data: investigations = [], isLoading: isLoadingInvestigations } = useQuery({
@@ -121,7 +122,7 @@ export function Command() {
return (
<>
<Button variant="ghost" onClick={() => setOpen(true)} className="text-xs h-8 w-full max-w-3xs border flex items-center justify-between hover:border-muted-foreground text-muted-foreground">
<Button variant="ghost" onClick={() => setOpen(true)} className="text-xs h-8 w-full max-w-3xs border flex rounded-full items-center justify-between hover:border-muted-foreground text-muted-foreground">
<span className="flex items-center gap-2"><Search /> Search Flowsint{" "}</span>
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
<span className="text-xs"></span>J
@@ -146,53 +147,64 @@ export function Command() {
<CommandGroup heading="Investigations">
{filteredData.investigations.map((investigation: Investigation) => (
<React.Fragment key={investigation.id}>
<CommandItem asChild>
<Link
onClick={() => setOpen(false)}
to="/dashboard/investigations/$investigationId"
params={{ investigationId: investigation.id }}
className="flex items-center gap-2"
>
<CommandItem
onSelect={() => {
navigate({
to: "/dashboard/investigations/$investigationId",
params: { investigationId: investigation.id },
})
setOpen(false)
}}
>
<div className="flex items-center gap-2">
<Fingerprint className="h-4 w-4" />
<span className="flex-1">{investigation.name}</span>
</Link>
</div>
</CommandItem>
{/* Show sketches for this investigation */}
{investigation.sketches?.map((sketch) => (
<CommandItem asChild key={sketch.id}>
<Link
onClick={() => setOpen(false)}
to="/dashboard/investigations/$investigationId/$type/$id"
params={{
investigationId: investigation.id,
type: "graph",
id: sketch.id
}}
className="flex items-center gap-2 pl-6"
>
<CommandItem
key={sketch.id}
onSelect={() => {
navigate({
to: "/dashboard/investigations/$investigationId/$type/$id",
params: {
investigationId: investigation.id,
type: "graph",
id: sketch.id,
},
})
setOpen(false)
}}
>
<div className="flex items-center gap-2 pl-6">
<Waypoints className="h-4 w-4" />
<span className="flex-1">{sketch.title}</span>
</Link>
</div>
</CommandItem>
))}
{/* Show analyses for this investigation */}
{analysesByInvestigation[investigation.id]?.map((analysis) => (
<CommandItem asChild key={analysis.id}>
<Link
onClick={() => setOpen(false)}
to="/dashboard/investigations/$investigationId/$type/$id"
params={{
investigationId: investigation.id,
type: "analysis",
id: analysis.id
}}
className="flex items-center gap-2 pl-6"
>
<CommandItem
key={analysis.id}
onSelect={() => {
navigate({
to: "/dashboard/investigations/$investigationId/$type/$id",
params: {
investigationId: investigation.id,
type: "analysis",
id: analysis.id,
},
})
setOpen(false)
}}
>
<div className="flex items-center gap-2 pl-6">
<FileText className="h-4 w-4" />
<span className="flex-1">{analysis.title}</span>
</Link>
</div>
</CommandItem>
))}
</React.Fragment>
@@ -202,17 +214,27 @@ export function Command() {
<CommandSeparator />
<CommandGroup heading="Quick Actions">
<CommandItem asChild>
<Link onClick={() => setOpen(false)} to="/dashboard/investigations">
<CommandItem
onSelect={() => {
navigate({ to: "/dashboard/investigations" })
setOpen(false)
}}
>
<div className="flex items-center gap-2">
<Fingerprint className="h-4 w-4" />
<span>All Investigations</span>
</Link>
</div>
</CommandItem>
<CommandItem asChild>
<Link onClick={() => setOpen(false)} to="/dashboard/flows">
<CommandItem
onSelect={() => {
navigate({ to: "/dashboard/flows" })
setOpen(false)
}}
>
<div className="flex items-center gap-2">
<Workflow className="h-4 w-4" />
<span>Transforms</span>
</Link>
</div>
</CommandItem>
</CommandGroup>
</>

View File

@@ -433,11 +433,11 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
}, [graphData.nodes.length, initializeGraph, instanceId]);
// New function to determine which labels should be visible based on zoom and weight
const getVisibleLabels = useMemo(() => {
const handleShouldShowLabel = useCallback((nodeId: string, ctx: CanvasRenderingContext2D, globalScale: number) => {
if (!showLabels || !labelRenderingCompound.current) return new Set<string>();
const nodestoDisplay: string[] = [];
// Min and max are actually dependant on the zoom level
const zoom = Math.round(zoomRef.current.k)
const zoom = Math.round(globalScale)
const zoomCursor = Math.max(CONSTANTS.MIN_ZOOM, Math.min(CONSTANTS.MAX_ZOOM, zoom))
const cursor = Math.round(zoomCursor * labelRenderingCompound.current.constants.nodesLength / (CONSTANTS.MAX_ZOOM))
const min = Math.round(cursor - CONSTANTS.MAX_VISIBLE_LABELS)
@@ -452,8 +452,8 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
}
}
// We compute the number of labels to show, making sure it doesn't reach the threshold
return new Set<string>(nodestoDisplay);
}, [labelRenderingCompound.current, zoomRef.current.k, showLabels]);
return new Set<string>(nodestoDisplay).has(nodeId);
}, [labelRenderingCompound.current, showLabels]);
// Event handlers with proper memoization
const handleNodeClick = useCallback((node: any, event: MouseEvent) => {
@@ -626,7 +626,7 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
const label = truncateText(node.nodeLabel || node.label || node.id, 58);
if (label) {
// Check if this node's label should be visible in current layer
const shouldShowLabel = getVisibleLabels.has(node.id);
const shouldShowLabel = handleShouldShowLabel(node.id, ctx, globalScale);
// Always show labels for highlighted nodes
if (!shouldShowLabel && !isHighlighted) {
return;
@@ -646,7 +646,7 @@ const GraphViewer: React.FC<GraphViewerProps> = ({
ctx.fillText(label, node.x, bgY + bgHeight / 2);
}
}
}, [forceSettings, showLabels, showIcons, isCurrent, isSelected, theme, highlightNodes, highlightLinks, hoverNode, getVisibleLabels]);
}, [forceSettings, showLabels, showIcons, isCurrent, isSelected, theme, highlightNodes, highlightLinks, hoverNode]);
const renderLink = useCallback((link: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
const { source: start, target: end } = link;