fix: several smaller improvements

This commit is contained in:
2026-03-14 22:13:38 +01:00
parent a1ad4d52cf
commit bcb9c5946d
5 changed files with 35 additions and 50 deletions

View File

@@ -80,7 +80,13 @@ export function CanvasMenubar({
const prevSaveStatusRef = useRef<SaveStatus>(saveStatus) const prevSaveStatusRef = useRef<SaveStatus>(saveStatus)
useEffect(() => { useEffect(() => {
if (prevSaveStatusRef.current === 'saving' && saveStatus === 'saved') { if (saveStatus === 'unsaved') {
setShowSavedBriefly(false)
if (savedBrieflyTimerRef.current) {
clearTimeout(savedBrieflyTimerRef.current)
savedBrieflyTimerRef.current = null
}
} else if (prevSaveStatusRef.current === 'saving' && saveStatus === 'saved') {
setShowSavedBriefly(true) setShowSavedBriefly(true)
if (savedBrieflyTimerRef.current) clearTimeout(savedBrieflyTimerRef.current) if (savedBrieflyTimerRef.current) clearTimeout(savedBrieflyTimerRef.current)
savedBrieflyTimerRef.current = setTimeout(() => { savedBrieflyTimerRef.current = setTimeout(() => {
@@ -330,7 +336,7 @@ export function CanvasMenubar({
{saveStatus === 'unsaved' && ( {saveStatus === 'unsaved' && (
<CircleDot className="size-3.5" aria-hidden /> <CircleDot className="size-3.5" aria-hidden />
)} )}
{(saveStatus === 'saved' || showSavedBriefly) && ( {saveStatus !== 'unsaved' && (saveStatus === 'saved' || showSavedBriefly) && (
<CheckCircle2 className="size-3.5 text-muted-foreground/70" aria-hidden /> <CheckCircle2 className="size-3.5 text-muted-foreground/70" aria-hidden />
)} )}
</span> </span>

View File

@@ -1,13 +1,11 @@
/** /**
* Hook for canvas graph state and persistence. Wraps useGraphStateWithHistory * Hook for canvas graph state and persistence. Wraps useGraphStateWithHistory
* with initial graph from project storage (or example). Save is explicit via save(); * with initial graph from project storage (or example). Save is explicit via save().
* autosave runs on an interval (~10s) and saves when the graph has changed.
*/ */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useMemo, useRef, useState } from 'react'
import { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory' import { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory'
import { getInitialGraph } from '@/app/canvas/canvasGraphUtils' import { getInitialGraph } from '@/app/canvas/canvasGraphUtils'
import { useCanvasStore } from '@/app/canvas/canvasStore'
import { saveGraphToStorage, PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage' import { saveGraphToStorage, PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage'
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes' import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
@@ -20,70 +18,46 @@ export type UseCanvasGraphResult = ReturnType<typeof useGraphStateWithHistory> &
saveStatus: SaveStatus saveStatus: SaveStatus
} }
const AUTOSAVE_INTERVAL_MS = 10_000
export function useCanvasGraph(projectId: string | undefined): UseCanvasGraphResult { export function useCanvasGraph(projectId: string | undefined): UseCanvasGraphResult {
const initialGraph = useMemo(() => getInitialGraph(projectId), [projectId]) const initialGraph = useMemo(() => getInitialGraph(projectId), [projectId])
const result = useGraphStateWithHistory(initialGraph.nodes, initialGraph.edges) const result = useGraphStateWithHistory(initialGraph.nodes, initialGraph.edges)
const { nodes, edges } = result const { nodes, edges } = result
const storeNodes = useCanvasStore((s) => s.graph.nodes)
const storeEdges = useCanvasStore((s) => s.graph.edges)
const nodesRef = useRef(nodes) const nodesRef = useRef(nodes)
const edgesRef = useRef(edges) const edgesRef = useRef(edges)
nodesRef.current = nodes nodesRef.current = nodes
edgesRef.current = edges edgesRef.current = edges
const [isSaving, setIsSaving] = useState(false) const [isSaving, setIsSaving] = useState(false)
const lastSavedSnapshotRef = useRef( const [lastSavedSerialized, setLastSavedSerialized] = useState(() =>
JSON.stringify({ nodes: initialGraph.nodes, edges: initialGraph.edges }) JSON.stringify({ nodes: initialGraph.nodes, edges: initialGraph.edges })
) )
const serializedFromStore = useMemo( const currentSerialized = useMemo(
() => JSON.stringify({ nodes: storeNodes, edges: storeEdges }), () => JSON.stringify({ nodes, edges }),
[storeNodes, storeEdges] [nodes, edges]
) )
const isDirty = serializedFromStore !== lastSavedSnapshotRef.current const isDirty = currentSerialized !== lastSavedSerialized
const saveStatus: SaveStatus = isSaving ? 'saving' : isDirty ? 'unsaved' : 'saved' const saveStatus: SaveStatus = isSaving ? 'saving' : isDirty ? 'unsaved' : 'saved'
const performSave = useCallback(() => {
if (!projectId) return
const current = JSON.stringify({
nodes: nodesRef.current,
edges: edgesRef.current,
})
if (current === lastSavedSnapshotRef.current) return
setIsSaving(true)
saveGraphToStorage(projectId, {
version: PROJECT_VERSION,
nodes: nodesRef.current,
edges: edgesRef.current,
})
lastSavedSnapshotRef.current = current
setIsSaving(false)
}, [projectId])
const save = useCallback(() => { const save = useCallback(() => {
if (!projectId) return if (!projectId) return
const snapshot = JSON.stringify({
nodes: nodesRef.current,
edges: edgesRef.current,
})
setIsSaving(true) setIsSaving(true)
saveGraphToStorage(projectId, { saveGraphToStorage(projectId, {
version: PROJECT_VERSION, version: PROJECT_VERSION,
nodes: nodesRef.current, nodes: nodesRef.current,
edges: edgesRef.current, edges: edgesRef.current,
}) })
lastSavedSnapshotRef.current = JSON.stringify({ const SAVING_DISPLAY_MS = 360
nodes: nodesRef.current, setTimeout(() => {
edges: edgesRef.current, setLastSavedSerialized(snapshot)
})
setIsSaving(false) setIsSaving(false)
}, SAVING_DISPLAY_MS)
}, [projectId]) }, [projectId])
useEffect(() => {
if (!projectId) return
const id = setInterval(performSave, AUTOSAVE_INTERVAL_MS)
return () => clearInterval(id)
}, [projectId, performSave])
return { ...result, save, saveStatus } return { ...result, save, saveStatus }
} }

View File

@@ -92,7 +92,7 @@ export function CodeEditor({
}, [value, textareaId]) }, [value, textareaId])
return ( return (
<div ref={containerRef} className="code-editor" style={{ minHeight: 0 }}> <div ref={containerRef} className="code-editor min-h-0 h-full overflow-auto" style={{ minHeight: 0 }}>
<Editor <Editor
value={value} value={value}
onValueChange={handleValueChange} onValueChange={handleValueChange}

View File

@@ -75,11 +75,14 @@ export function BaseNode({
data-selected={selected} data-selected={selected}
data-path-role={connectionPathRole ?? undefined} data-path-role={connectionPathRole ?? undefined}
style={appliedStyle} style={appliedStyle}
tabIndex={0} tabIndex={selected ? 0 : -1}
{...props} {...props}
> >
<div <div
className="min-h-0 flex-1 flex flex-col overflow-hidden" className={cn(
"min-h-0 flex-1 flex flex-col overflow-hidden",
!selected && "pointer-events-none",
)}
style={{ contain: "layout" }} style={{ contain: "layout" }}
> >
{children} {children}
@@ -96,7 +99,9 @@ export function BaseNode({
handleClassName="base-node-resize-handle nodrag nopan" handleClassName="base-node-resize-handle nodrag nopan"
/> />
)} )}
{!isFullscreenInstance && handles} {!isFullscreenInstance && (
<div className={cn(!selected && "pointer-events-none")}>{handles}</div>
)}
</div> </div>
); );
} }
@@ -198,7 +203,7 @@ export function BaseNodeContent({
return ( return (
<div <div
data-slot="base-node-content" data-slot="base-node-content"
className={cn("min-h-0 flex-1 flex flex-col overflow-auto", className)} className={cn("min-h-0 flex-1 flex flex-col", className)}
{...props} {...props}
/> />
); );

View File

@@ -307,7 +307,7 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) {
/> />
</div> </div>
<div ref={editorContainerRef as React.RefObject<HTMLDivElement>} className="min-h-0 flex-1 w-full nodrag nopan overflow-auto border-t border-input" style={{ minHeight: editorHeight }}> <div ref={editorContainerRef as React.RefObject<HTMLDivElement>} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden border-t border-input" style={{ minHeight: editorHeight }}>
<CodeEditor <CodeEditor
textareaId={editorId} textareaId={editorId}
value={content} value={content}