feat: improvements
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import {
|
||||
ReactFlow,
|
||||
ReactFlowProvider,
|
||||
@@ -37,8 +36,8 @@ import { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu'
|
||||
import { CanvasContextMenuContent } from '@/app/canvas/CanvasContextMenuContent'
|
||||
import { useCanvasConnectionPathFromStore } from '@/app/canvas/useCanvasConnectionPathFromStore'
|
||||
import { dispatchCanvasCommand } from '@/app/canvas/canvasStore'
|
||||
import { useRecollectionTitleData } from '@/app/recollections/RecollectionTitleDataContext'
|
||||
import { FluxMenubarContent } from '@/app/recollections/flux/FluxMenubarContent'
|
||||
import { useRecollectionActions } from '@/app/recollections/RecollectionActionsContext'
|
||||
import type { StoredGraphState } from '@/app/recollections/recollectionStore'
|
||||
import { createContextualNode } from '@/app/canvas/ContextualZoomNode'
|
||||
import { ViewportDisplayProvider } from '@/app/canvas/ViewportDisplayContext'
|
||||
import { FlowKeyboardShortcuts } from '@/components/graph/FlowKeyboardShortcuts'
|
||||
@@ -68,7 +67,7 @@ import {
|
||||
} from '@/lib/graph/nodeRegistry'
|
||||
import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes'
|
||||
import { toast } from 'sonner'
|
||||
import { RECOLLECTION_FILE_EXT, RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage'
|
||||
import { RECOLLECTION_VERSION } from '@/app/recollections/recollectionGraphStorage'
|
||||
|
||||
const SNAP_GRID: [number, number] = [15, 15]
|
||||
const DUPLICATE_OFFSET = { x: 30, y: 30 }
|
||||
@@ -176,7 +175,6 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
|
||||
saveStatus,
|
||||
} = useCanvasGraph(recollectionId)
|
||||
|
||||
const importInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [rfInstance, setRfInstance] = React.useState<unknown>(null)
|
||||
const [renamingNodeId, setRenamingNodeId] = React.useState<string | null>(null)
|
||||
const [connectionFrom, setConnectionFrom] = React.useState<{ nodeId: string; sourceHandle?: string } | null>(null)
|
||||
@@ -325,57 +323,12 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
|
||||
const onNodeDragStart = useCallback(() => saveForDragEnd(), [saveForDragEnd])
|
||||
const onNodeDragStop = useCallback(() => commitDragEnd(), [commitDragEnd])
|
||||
|
||||
const handleExportRecollection = useCallback(() => {
|
||||
const state = { version: RECOLLECTION_VERSION, nodes, edges }
|
||||
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `recollection${RECOLLECTION_FILE_EXT}`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
toast.success('Recollection exported')
|
||||
}, [nodes, edges])
|
||||
|
||||
const handleImportRecollection = useCallback(() => importInputRef.current?.click(), [])
|
||||
|
||||
const handleLoadExample = useCallback(() => {
|
||||
const { nodes: exampleNodes, edges: exampleEdges } = getExampleGraph()
|
||||
setStateImmediate({ nodes: exampleNodes, edges: exampleEdges })
|
||||
toast.success('Example loaded')
|
||||
}, [setStateImmediate])
|
||||
|
||||
const onImportFileChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
e.target.value = ''
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const text = reader.result as string
|
||||
const state = JSON.parse(text) as { version?: number; nodes?: unknown[]; edges?: unknown[] }
|
||||
if (!state || !Array.isArray(state.nodes) || !Array.isArray(state.edges)) {
|
||||
toast.error('Invalid file: expected nodes and edges arrays')
|
||||
return
|
||||
}
|
||||
const nodes = state.nodes as AppNode[]
|
||||
const edges = backfillEdgeTargetTypes(nodes, state.edges as AppEdge[])
|
||||
setStateImmediate({ nodes, edges })
|
||||
if (state.version != null && state.version > RECOLLECTION_VERSION) {
|
||||
toast.error('Recollection was created with a newer app version')
|
||||
} else {
|
||||
toast.success('Recollection loaded')
|
||||
}
|
||||
} catch {
|
||||
toast.error('Invalid file: not valid JSON')
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
},
|
||||
[setStateImmediate]
|
||||
)
|
||||
|
||||
const selectedNodes = useMemo(
|
||||
() => nodes.filter((n) => (n as Node & { selected?: boolean }).selected),
|
||||
[nodes]
|
||||
@@ -429,56 +382,56 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
|
||||
flowActionsRef.current?.pasteAtViewportCenter?.()
|
||||
}, [])
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const isFluxActive = pathname.endsWith('/flux')
|
||||
const { setTitleData } = useRecollectionTitleData()
|
||||
const { setFluxSlot, onImport } = useRecollectionActions()
|
||||
|
||||
const onRefreshFromStore = useCallback(
|
||||
(graph: StoredGraphState) => {
|
||||
const nodes = graph.nodes as AppNode[]
|
||||
const edges = backfillEdgeTargetTypes(nodes, graph.edges as AppEdge[])
|
||||
setStateImmediate({ nodes, edges })
|
||||
},
|
||||
[setStateImmediate]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setTitleData({
|
||||
const slot = {
|
||||
saveStatus,
|
||||
onSave: recollectionId
|
||||
? () => {
|
||||
save()
|
||||
toast.success('Saved')
|
||||
}
|
||||
: undefined,
|
||||
: () => {},
|
||||
canSave: Boolean(recollectionId),
|
||||
onImport: handleImportRecollection,
|
||||
onExport: handleExportRecollection,
|
||||
})
|
||||
return () => setTitleData(null)
|
||||
}, [
|
||||
setTitleData,
|
||||
saveStatus,
|
||||
recollectionId,
|
||||
save,
|
||||
handleImportRecollection,
|
||||
handleExportRecollection,
|
||||
])
|
||||
|
||||
const fluxMenubarProps = useMemo(
|
||||
() => ({
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
onRefreshFromStore,
|
||||
onDuplicate: handleDuplicate,
|
||||
onCopy: handleCopy,
|
||||
onPaste: handlePaste,
|
||||
canDuplicate: selectedNodes.length > 0,
|
||||
canCopy: selectedNodes.length === 1,
|
||||
onFitView: () => flowActionsRef.current?.fitView?.(),
|
||||
}),
|
||||
[
|
||||
canUndo,
|
||||
canRedo,
|
||||
selectedNodes.length,
|
||||
undo,
|
||||
redo,
|
||||
handleDuplicate,
|
||||
handleCopy,
|
||||
handlePaste,
|
||||
]
|
||||
)
|
||||
}
|
||||
setFluxSlot(slot)
|
||||
return () => setFluxSlot(null)
|
||||
}, [
|
||||
setFluxSlot,
|
||||
saveStatus,
|
||||
recollectionId,
|
||||
save,
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
onRefreshFromStore,
|
||||
handleDuplicate,
|
||||
handleCopy,
|
||||
handlePaste,
|
||||
selectedNodes.length,
|
||||
])
|
||||
|
||||
const graphContextValue = useMemo(
|
||||
() => ({ setNodes, setEdges, graphRef, edges }),
|
||||
@@ -688,18 +641,9 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
|
||||
onContextMenu={onCanvasContextMenu}
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
{isFluxActive && <FluxMenubarContent {...fluxMenubarProps} />}
|
||||
<div role="status" aria-live="polite" aria-atomic className="sr-only">
|
||||
{ariaAnnouncement}
|
||||
</div>
|
||||
<input
|
||||
ref={importInputRef}
|
||||
type="file"
|
||||
accept=".json,.zui.json,application/json"
|
||||
className="hidden"
|
||||
onChange={onImportFileChange}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="flex-1 min-h-0 relative flex flex-col">
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<GraphContext.Provider value={graphContextValue}>
|
||||
@@ -729,7 +673,7 @@ export function CanvasPage({ recollectionId }: CanvasPageProps) {
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent className="flex-row flex-wrap justify-center gap-2">
|
||||
<Button onClick={handleImportRecollection} variant="outline" size="sm">
|
||||
<Button onClick={onImport} variant="outline" size="sm">
|
||||
<FolderOpen className="size-4" />
|
||||
Import…
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user