improve empty and initial state
This commit is contained in:
88
src/App.tsx
88
src/App.tsx
@@ -31,7 +31,16 @@ import {
|
|||||||
ContextMenuGroup
|
ContextMenuGroup
|
||||||
} from "@/components/ui/context-menu"
|
} from "@/components/ui/context-menu"
|
||||||
import { AppMenubar } from '@/components/AppMenubar'
|
import { AppMenubar } from '@/components/AppMenubar'
|
||||||
import { ClipboardPaste } from 'lucide-react'
|
import {
|
||||||
|
Empty,
|
||||||
|
EmptyContent,
|
||||||
|
EmptyDescription,
|
||||||
|
EmptyHeader,
|
||||||
|
EmptyMedia,
|
||||||
|
EmptyTitle,
|
||||||
|
} from '@/components/ui/empty'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { ClipboardPaste, FolderOpen, FileStack, CircleDotDashed } from 'lucide-react'
|
||||||
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from './lib/flowUtils'
|
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from './lib/flowUtils'
|
||||||
import {
|
import {
|
||||||
getRegisteredNodeTypes,
|
getRegisteredNodeTypes,
|
||||||
@@ -47,17 +56,31 @@ const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
|
|||||||
y: Math.round(y / SNAP_GRID[1]) * SNAP_GRID[1],
|
y: Math.round(y / SNAP_GRID[1]) * SNAP_GRID[1],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const NODE_GAP = 150
|
||||||
const initialNodes: AppNode[] = [
|
const initialNodes: AppNode[] = [
|
||||||
|
{
|
||||||
|
id: 'var_001',
|
||||||
|
position: { x: 50, y: 100 },
|
||||||
|
data: { value: 'Zoe', valueType: 'string' as const },
|
||||||
|
type: 'variable',
|
||||||
|
style: DEFAULT_NODE_STYLE.variable,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'cfg_001',
|
id: 'cfg_001',
|
||||||
position: { x: 50, y: 50 },
|
position: { x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP, y: 50 },
|
||||||
data: { plantuml: '@startuml\nactor User\nparticipant "Render" as R\nUser -> R : config\n@enduml\n', title: 'config-cfg_001' },
|
data: {
|
||||||
|
plantuml: '@startuml\nactor User\nparticipant "{{ var_001 }}" as R\nUser -> R : loves\n@enduml\n',
|
||||||
|
title: 'config-cfg_001',
|
||||||
|
},
|
||||||
type: 'config',
|
type: 'config',
|
||||||
style: DEFAULT_NODE_STYLE.config,
|
style: DEFAULT_NODE_STYLE.config,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'rnd_001',
|
id: 'rnd_001',
|
||||||
position: { x: 350, y: 80 },
|
position: {
|
||||||
|
x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP + DEFAULT_NODE_STYLE.config.width + NODE_GAP,
|
||||||
|
y: 50,
|
||||||
|
},
|
||||||
data: {},
|
data: {},
|
||||||
type: 'render',
|
type: 'render',
|
||||||
style: DEFAULT_NODE_STYLE.render,
|
style: DEFAULT_NODE_STYLE.render,
|
||||||
@@ -65,9 +88,20 @@ const initialNodes: AppNode[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const initialEdges: AppEdge[] = [
|
const initialEdges: AppEdge[] = [
|
||||||
|
{ id: 'e-var_001-cfg_001', source: 'var_001', target: 'cfg_001', type: 'animated' },
|
||||||
{ id: 'e-cfg_001-rnd_001', source: 'cfg_001', target: 'rnd_001', type: 'animated' },
|
{ id: 'e-cfg_001-rnd_001', source: 'cfg_001', target: 'rnd_001', type: 'animated' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function getExampleGraph(): { nodes: AppNode[]; edges: AppEdge[] } {
|
||||||
|
return {
|
||||||
|
nodes: initialNodes.map((n) => ({
|
||||||
|
...n,
|
||||||
|
data: n.data && typeof n.data === 'object' ? { ...(n.data as object) } : n.data,
|
||||||
|
})),
|
||||||
|
edges: initialEdges.map((e) => ({ ...e })),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const PROJECT_FILE_EXT = '.zui.json'
|
const PROJECT_FILE_EXT = '.zui.json'
|
||||||
const PROJECT_VERSION = 1
|
const PROJECT_VERSION = 1
|
||||||
|
|
||||||
@@ -89,7 +123,7 @@ export default function App() {
|
|||||||
canUndo,
|
canUndo,
|
||||||
canRedo,
|
canRedo,
|
||||||
setStateImmediate,
|
setStateImmediate,
|
||||||
} = useGraphStateWithHistory(initialNodes, initialEdges)
|
} = useGraphStateWithHistory(getExampleGraph().nodes, getExampleGraph().edges)
|
||||||
|
|
||||||
const importInputRef = useRef<HTMLInputElement | null>(null)
|
const importInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
||||||
@@ -219,6 +253,12 @@ export default function App() {
|
|||||||
importInputRef.current?.click()
|
importInputRef.current?.click()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleLoadExample = useCallback(() => {
|
||||||
|
const { nodes: exampleNodes, edges: exampleEdges } = getExampleGraph()
|
||||||
|
setStateImmediate({ nodes: exampleNodes, edges: exampleEdges })
|
||||||
|
setProjectMessage({ type: 'success', text: 'Example loaded' })
|
||||||
|
}, [setStateImmediate])
|
||||||
|
|
||||||
const onImportFileChange = useCallback(
|
const onImportFileChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0]
|
||||||
@@ -406,11 +446,10 @@ export default function App() {
|
|||||||
{projectMessage && (
|
{projectMessage && (
|
||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
className={`shrink-0 px-3 py-2 text-sm ${
|
className={`shrink-0 px-3 py-2 text-sm ${projectMessage.type === 'success'
|
||||||
projectMessage.type === 'success'
|
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200'
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200'
|
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200'
|
||||||
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{projectMessage.text}
|
{projectMessage.text}
|
||||||
</div>
|
</div>
|
||||||
@@ -420,7 +459,34 @@ export default function App() {
|
|||||||
<FlowContext.Provider value={flowContextValue}>
|
<FlowContext.Provider value={flowContextValue}>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<div className="flex-1 min-h-0 w-full" onWheelCapture={onWheelCapture}>
|
<div className="flex-1 min-h-0 w-full relative" onWheelCapture={onWheelCapture}>
|
||||||
|
{nodes.length === 0 && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
|
||||||
|
<div className="pointer-events-auto rounded-lg border border-border bg-background/95 dark:bg-background/90 shadow-sm">
|
||||||
|
<Empty className="p-6 md:p-8">
|
||||||
|
<EmptyHeader>
|
||||||
|
<EmptyMedia variant="icon">
|
||||||
|
<CircleDotDashed className="size-6" />
|
||||||
|
</EmptyMedia>
|
||||||
|
<EmptyTitle>Start adding a new node!</EmptyTitle>
|
||||||
|
<EmptyDescription>
|
||||||
|
Right‑click to add nodes. <br />Import a project or paste a node.
|
||||||
|
</EmptyDescription>
|
||||||
|
</EmptyHeader>
|
||||||
|
<EmptyContent className="flex-row flex-wrap justify-center gap-2">
|
||||||
|
<Button onClick={handleImportProject} variant="outline" size="sm">
|
||||||
|
<FolderOpen className="size-4" />
|
||||||
|
Import…
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleLoadExample} variant="outline" size="sm">
|
||||||
|
<FileStack className="size-4" />
|
||||||
|
Load example
|
||||||
|
</Button>
|
||||||
|
</EmptyContent>
|
||||||
|
</Empty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView>
|
<ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
|
|||||||
Reference in New Issue
Block a user