mirror of
https://github.com/reconurge/flowsint.git
synced 2026-03-11 17:34:31 -05:00
feat: editor useRef to prevent re-renders
This commit is contained in:
@@ -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]"
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user