fix: several smaller improvements
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user