fix: smooth flow dragging and refresh chunks

This commit is contained in:
dhruv465 2026-05-30 17:01:14 +05:30
parent 1b66431acf
commit 3854502652
3 changed files with 75 additions and 25 deletions

View File

@ -67,9 +67,9 @@ const nextConfig: NextConfig = {
* did nothing because the cache is server-side. * did nothing because the cache is server-side.
* *
* Strategy: * Strategy:
* - /_next/static/* — immutable for a year. Filenames are * - /_next/static/* — leave to Next. Turbopack dev chunks can go
* content-hashed, so a new build produces new filenames; the * stale if we force immutable caching here; Next already emits
* old ones are safe to keep indefinitely in caches. * the correct production headers for hashed assets.
* - /api/* — no-store. API responses are per-user and * - /api/* — no-store. API responses are per-user and
* must never be shared across requests at the edge. * must never be shared across requests at the edge.
* - Everything else — public, brief s-maxage + generous * - Everything else — public, brief s-maxage + generous
@ -93,21 +93,12 @@ const nextConfig: NextConfig = {
*/ */
async headers() { async headers() {
return [ return [
{
source: "/_next/static/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
{ {
source: "/api/:path*", source: "/api/:path*",
headers: [{ key: "Cache-Control", value: "no-store" }], headers: [{ key: "Cache-Control", value: "no-store" }],
}, },
{ {
source: "/:path*", source: "/:path((?!_next/static|_next/image|api).*)",
headers: [ headers: [
{ {
key: "Cache-Control", key: "Cache-Control",

View File

@ -36,8 +36,9 @@
* list view reads. * list view reads.
*/ */
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { import {
applyNodeChanges,
Background, Background,
Controls, Controls,
Handle, Handle,
@ -50,6 +51,7 @@ import {
type Connection, type Connection,
type Node as RfNode, type Node as RfNode,
type Edge as RfEdge, type Edge as RfEdge,
type NodeChange,
type NodeProps, type NodeProps,
type OnNodeDrag, type OnNodeDrag,
} from "@xyflow/react"; } from "@xyflow/react";
@ -227,6 +229,7 @@ function FlowCanvasInner() {
setState, setState,
updateNodeConfig, updateNodeConfig,
updateNodePosition, updateNodePosition,
updateNodePositions,
removeNode, removeNode,
flashKey, flashKey,
} = useFlowEditor(); } = useFlowEditor();
@ -246,15 +249,10 @@ function FlowCanvasInner() {
[selectedNodeKey, builderNodes], [selectedNodeKey, builderNodes],
); );
const { rfNodes, rfEdges } = useMemo(() => { const autoLayoutPositions = useMemo(() => {
const canvasEdges = deriveCanvasEdges(builderNodes); const canvasEdges = deriveCanvasEdges(builderNodes);
// Decide whether to auto-layout. The helper guards against return shouldAutoLayout(builderNodes)
// overwriting a user's manual arrangement (only fires when ALL
// nodes sit at the origin), so we can safely call it
// unconditionally — if any node has been positioned, this is a
// no-op.
const positions = shouldAutoLayout(builderNodes)
? autoLayout( ? autoLayout(
builderNodes.map((n) => ({ builderNodes.map((n) => ({
id: n.node_key, id: n.node_key,
@ -265,9 +263,26 @@ function FlowCanvasInner() {
{ direction: "TB" }, { direction: "TB" },
) )
: null; : null;
}, [builderNodes]);
const rfNodes: RfNode<NodeData>[] = builderNodes.map((n) => { // If dagre had to place an all-zero flow, persist the generated
const fallback = positions?.get(n.node_key); // positions into editor state once. Otherwise the next drag would
// save only the dragged node and every other node would fall back
// to (0,0), which feels like nodes teleporting around the canvas.
const persistedAutoLayoutRef = useRef(false);
useEffect(() => {
if (!autoLayoutPositions || persistedAutoLayoutRef.current) return;
persistedAutoLayoutRef.current = true;
updateNodePositions(
Object.fromEntries(
[...autoLayoutPositions].map(([key, pos]) => [key, pos]),
),
);
}, [autoLayoutPositions, updateNodePositions]);
const derivedRfNodes = useMemo(() => {
const nodes: RfNode<NodeData>[] = builderNodes.map((n) => {
const fallback = autoLayoutPositions?.get(n.node_key);
return { return {
id: n.node_key, id: n.node_key,
type: "flow", type: "flow",
@ -283,6 +298,18 @@ function FlowCanvasInner() {
}; };
}); });
return nodes;
}, [builderNodes, entryNodeId, flashKey, autoLayoutPositions]);
const [rfNodes, setRfNodes] = useState<RfNode<NodeData>[]>(derivedRfNodes);
useEffect(() => {
setRfNodes(derivedRfNodes);
}, [derivedRfNodes]);
const rfEdges = useMemo(() => {
const canvasEdges = deriveCanvasEdges(builderNodes);
// sourceHandle is now wired up — the FlowNodeCard renders a Handle // sourceHandle is now wired up — the FlowNodeCard renders a Handle
// per slot whose id matches the scheme in edges.ts, so React-Flow // per slot whose id matches the scheme in edges.ts, so React-Flow
// can hang the arrow off the right place on each card. // can hang the arrow off the right place on each card.
@ -299,8 +326,15 @@ function FlowCanvasInner() {
style: { stroke: "#475569", strokeWidth: 1.5 }, style: { stroke: "#475569", strokeWidth: 1.5 },
})); }));
return { rfNodes, rfEdges }; return rfEdges;
}, [builderNodes, entryNodeId, flashKey]); }, [builderNodes]);
const handleNodesChange = useCallback(
(changes: NodeChange<RfNode<NodeData>>[]) => {
setRfNodes((nodes) => applyNodeChanges(changes, nodes));
},
[],
);
// Drag-to-position: React-Flow tracks the visual drag internally and // Drag-to-position: React-Flow tracks the visual drag internally and
// fires this once on release. We write the final coordinate back to // fires this once on release. We write the final coordinate back to
@ -439,6 +473,7 @@ function FlowCanvasInner() {
fitView fitView
fitViewOptions={{ padding: 0.2, maxZoom: 1 }} fitViewOptions={{ padding: 0.2, maxZoom: 1 }}
proOptions={{ hideAttribution: true }} proOptions={{ hideAttribution: true }}
onNodesChange={handleNodesChange}
onNodeDragStop={handleNodeDragStop} onNodeDragStop={handleNodeDragStop}
onNodeClick={handleNodeClick} onNodeClick={handleNodeClick}
onConnect={handleConnect} onConnect={handleConnect}

View File

@ -97,6 +97,9 @@ export interface FlowEditorContextValue {
updateNode: (key: string, patch: Partial<BuilderNode>) => void; updateNode: (key: string, patch: Partial<BuilderNode>) => void;
updateNodeConfig: (key: string, patch: Record<string, unknown>) => void; updateNodeConfig: (key: string, patch: Record<string, unknown>) => void;
updateNodePosition: (key: string, x: number, y: number) => void; updateNodePosition: (key: string, x: number, y: number) => void;
updateNodePositions: (
positions: Record<string, { x: number; y: number }>,
) => void;
removeNode: (key: string) => void; removeNode: (key: string) => void;
// Actions // Actions
@ -438,6 +441,25 @@ export function FlowEditorProvider({
[setState], [setState],
); );
const updateNodePositions = useCallback(
(positions: Record<string, { x: number; y: number }>) => {
setState((s) => ({
...s,
nodes: s.nodes.map((n) => {
const next = positions[n.node_key];
return next
? {
...n,
position_x: Math.round(next.x),
position_y: Math.round(next.y),
}
: n;
}),
}));
},
[setState],
);
const addNode = useCallback( const addNode = useCallback(
(type: NodeType): string => { (type: NodeType): string => {
const meta = NODE_META[type]; const meta = NODE_META[type];
@ -498,6 +520,7 @@ export function FlowEditorProvider({
updateNode, updateNode,
updateNodeConfig, updateNodeConfig,
updateNodePosition, updateNodePosition,
updateNodePositions,
removeNode, removeNode,
save, save,
setStatus, setStatus,
@ -518,6 +541,7 @@ export function FlowEditorProvider({
updateNode, updateNode,
updateNodeConfig, updateNodeConfig,
updateNodePosition, updateNodePosition,
updateNodePositions,
removeNode, removeNode,
save, save,
setStatus, setStatus,