mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Merge pull request #163 from dhruv465/codex/fix-cache-flow-canvas-drag
fix flow drag responsiveness and stale chunks
This commit is contained in:
commit
47987acb06
@ -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",
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
applyNodePositions,
|
||||||
defaultConfigFor,
|
defaultConfigFor,
|
||||||
uniqueNodeKey,
|
uniqueNodeKey,
|
||||||
} from "./flow-editor-state";
|
} from "./flow-editor-state";
|
||||||
@ -33,6 +34,48 @@ describe("uniqueNodeKey", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("applyNodePositions", () => {
|
||||||
|
it("rounds and applies positions without changing unrelated nodes", () => {
|
||||||
|
const nodes: BuilderNode[] = [
|
||||||
|
{
|
||||||
|
node_key: "start",
|
||||||
|
node_type: "start",
|
||||||
|
config: {},
|
||||||
|
position_x: 0,
|
||||||
|
position_y: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node_key: "message",
|
||||||
|
node_type: "send_message",
|
||||||
|
config: {},
|
||||||
|
position_x: 10,
|
||||||
|
position_y: 20,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
applyNodePositions(nodes, {
|
||||||
|
start: { x: 10.4, y: 20.6 },
|
||||||
|
}),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
node_key: "start",
|
||||||
|
node_type: "start",
|
||||||
|
config: {},
|
||||||
|
position_x: 10,
|
||||||
|
position_y: 21,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node_key: "message",
|
||||||
|
node_type: "send_message",
|
||||||
|
config: {},
|
||||||
|
position_x: 10,
|
||||||
|
position_y: 20,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("defaultConfigFor", () => {
|
describe("defaultConfigFor", () => {
|
||||||
// The hook's addNode and the validator both depend on these defaults
|
// The hook's addNode and the validator both depend on these defaults
|
||||||
// being self-consistent. A broken default would surface as a
|
// being self-consistent. A broken default would surface as a
|
||||||
|
|||||||
@ -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
|
||||||
@ -186,6 +189,22 @@ export function defaultConfigFor(type: NodeType): Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyNodePositions(
|
||||||
|
nodes: BuilderNode[],
|
||||||
|
positions: Record<string, { x: number; y: number }>,
|
||||||
|
): BuilderNode[] {
|
||||||
|
return 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Context
|
// Context
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@ -438,6 +457,19 @@ export function FlowEditorProvider({
|
|||||||
[setState],
|
[setState],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateNodePositions = useCallback(
|
||||||
|
(positions: Record<string, { x: number; y: number }>) => {
|
||||||
|
// Initial Dagre layout hydration should not dirty the editor:
|
||||||
|
// opening a legacy all-zero flow must not enable Save or arm
|
||||||
|
// beforeunload before the user actually edits anything.
|
||||||
|
setStateRaw((s) => ({
|
||||||
|
...s,
|
||||||
|
nodes: applyNodePositions(s.nodes, positions),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const addNode = useCallback(
|
const addNode = useCallback(
|
||||||
(type: NodeType): string => {
|
(type: NodeType): string => {
|
||||||
const meta = NODE_META[type];
|
const meta = NODE_META[type];
|
||||||
@ -498,6 +530,7 @@ export function FlowEditorProvider({
|
|||||||
updateNode,
|
updateNode,
|
||||||
updateNodeConfig,
|
updateNodeConfig,
|
||||||
updateNodePosition,
|
updateNodePosition,
|
||||||
|
updateNodePositions,
|
||||||
removeNode,
|
removeNode,
|
||||||
save,
|
save,
|
||||||
setStatus,
|
setStatus,
|
||||||
@ -518,6 +551,7 @@ export function FlowEditorProvider({
|
|||||||
updateNode,
|
updateNode,
|
||||||
updateNodeConfig,
|
updateNodeConfig,
|
||||||
updateNodePosition,
|
updateNodePosition,
|
||||||
|
updateNodePositions,
|
||||||
removeNode,
|
removeNode,
|
||||||
save,
|
save,
|
||||||
setStatus,
|
setStatus,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user