diff --git a/app/globals.css b/app/globals.css index a1c65c7..1d9d88b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -145,17 +145,6 @@ } } -@keyframes ls-edge-insert-enter { - 0% { - opacity: 0; - scale: 0.82; - } - 100% { - opacity: 1; - scale: 1; - } -} - .ls-connection-line-marching { animation: ls-connection-dash-offset 0.4s linear infinite; } @@ -262,17 +251,52 @@ .react-flow.canvas-edge-insert-reflowing .react-flow__node { transition-property: transform; transition-duration: var(--ls-edge-insert-reflow-duration, 1297ms); - transition-timing-function: cubic-bezier(0.68, -0.6, 0.32, 1.6); + transition-timing-function: linear( + 0 0%, + 0.2718 2.5%, + 0.6464 5%, + 1 7.5%, + 1.25 10%, + 1.3641 12.5%, + 1.3536 15%, + 1.2575 17.5%, + 1.125 20%, + 1 22.5%, + 0.9116 25%, + 0.8713 27.5%, + 0.875 30%, + 0.909 32.5%, + 0.9558 35%, + 1 37.5%, + 1.0313 40%, + 1.0455 42.5%, + 1.0442 45%, + 1.0322 47.5%, + 1.0156 50%, + 1 52.5%, + 0.989 55%, + 0.9839 57.5%, + 0.9844 60%, + 0.9886 62.5%, + 0.9945 65%, + 1 67.5%, + 1.0039 70%, + 1.0057 72.5%, + 1.0055 75%, + 1.004 77.5%, + 1.002 80%, + 1 82.5%, + 0.9986 85%, + 0.998 87.5%, + 0.998 90%, + 0.9986 92.5%, + 0.9993 95%, + 1 97.5%, + 1 100% + ); will-change: transform; } - .react-flow__node.canvas-edge-insert-enter, - .react-flow__node .canvas-edge-insert-enter { - animation: ls-edge-insert-enter 300ms cubic-bezier(0.68, -0.6, 0.32, 1.6) both; - transform-origin: center center; - will-change: scale, opacity; - } - @media (prefers-reduced-motion: reduce) { .react-flow.canvas-scissors-mode .react-flow__edge:not(.temp) .react-flow__edge-path { transition: none; @@ -281,13 +305,5 @@ .react-flow.canvas-edge-insert-reflowing .react-flow__node { transition: none; } - - .react-flow__node.canvas-edge-insert-enter, - .react-flow__node .canvas-edge-insert-enter { - animation: none; - opacity: 1; - scale: 1; - will-change: auto; - } } } diff --git a/components/canvas/__tests__/use-canvas-sync-engine-hook.test.tsx b/components/canvas/__tests__/use-canvas-sync-engine-hook.test.tsx index 9761683..549003e 100644 --- a/components/canvas/__tests__/use-canvas-sync-engine-hook.test.tsx +++ b/components/canvas/__tests__/use-canvas-sync-engine-hook.test.tsx @@ -90,17 +90,9 @@ const latestHookValueRef: { (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; -function HookHarness({ - canvasId, - initialNodes = [], - initialEdges = [], -}: { - canvasId: Id<"canvases">; - initialNodes?: RFNode[]; - initialEdges?: RFEdge[]; -}) { - const [nodes, setNodes] = useState(initialNodes); - const [edges, setEdges] = useState(initialEdges); +function HookHarness({ canvasId }: { canvasId: Id<"canvases"> }) { + const [, setNodes] = useState([]); + const [edges, setEdges] = useState([]); const edgesRef = useRef(edges); const deletingNodeIds = useRef(new Set()); const [, setAssetBrowserTargetNodeId] = useState(null); @@ -128,15 +120,10 @@ function HookHarness({ latestEdgesRef.current = edges; }, [edges]); - useEffect(() => { - latestNodesRef.current = nodes; - }, [nodes]); - return null; } const latestEdgesRef: { current: RFEdge[] } = { current: [] }; -const latestNodesRef: { current: RFNode[] } = { current: [] }; function setNavigatorOnline(online: boolean) { Object.defineProperty(window.navigator, "onLine", { @@ -152,7 +139,6 @@ describe("useCanvasSyncEngine hook wiring", () => { afterEach(async () => { latestHookValueRef.current = null; latestEdgesRef.current = []; - latestNodesRef.current = []; setNavigatorOnline(true); mocks.mutationMocks.clear(); vi.clearAllMocks(); @@ -295,97 +281,4 @@ describe("useCanvasSyncEngine hook wiring", () => { latestEdgesRef.current.some((edge) => edge.id === "optimistic_edge_req-2"), ).toBe(false); }); - - it("adds optimistic split node and edges immediately in online mode before mutation resolves", async () => { - let createSplitResolver!: (value: string) => void; - const createSplitPromise = new Promise((resolve) => { - createSplitResolver = resolve; - }); - const createSplitMutation = vi.fn(() => createSplitPromise); - Object.assign(createSplitMutation, { - withOptimisticUpdate: () => createSplitMutation, - }); - mocks.mutationMocks.set("nodes.createWithEdgeSplit", createSplitMutation); - - container = document.createElement("div"); - document.body.appendChild(container); - root = createRoot(container); - - await act(async () => { - root?.render( - , - ); - }); - - let pendingCreateSplit: Promise> | undefined; - await act(async () => { - pendingCreateSplit = latestHookValueRef.current?.actions.createNodeWithEdgeSplit({ - canvasId: asCanvasId("canvas-1"), - type: "note", - positionX: 180, - positionY: 0, - width: 140, - height: 100, - data: {}, - splitEdgeId: "edge-base" as Id<"edges">, - splitSourceHandle: "source-handle", - splitTargetHandle: "target-handle", - newNodeSourceHandle: "new-source-handle", - newNodeTargetHandle: "new-target-handle", - clientRequestId: "split-1", - }); - await Promise.resolve(); - }); - - expect( - latestNodesRef.current.find((node) => node.id === "optimistic_split-1"), - ).toMatchObject({ - className: "canvas-edge-insert-enter", - }); - expect( - latestEdgesRef.current.some((edge) => edge.id === "edge-base"), - ).toBe(false); - expect( - latestEdgesRef.current.some( - (edge) => edge.id === "optimistic_edge_split-1_split_a", - ), - ).toBe(true); - expect( - latestEdgesRef.current.some( - (edge) => edge.id === "optimistic_edge_split-1_split_b", - ), - ).toBe(true); - - createSplitResolver("node-real-split-1"); - await act(async () => { - await pendingCreateSplit; - }); - }); }); diff --git a/components/canvas/canvas.tsx b/components/canvas/canvas.tsx index b5ac195..67c0982 100644 --- a/components/canvas/canvas.tsx +++ b/components/canvas/canvas.tsx @@ -82,7 +82,7 @@ interface CanvasInnerProps { canvasId: Id<"canvases">; } -const EDGE_INSERT_REFLOW_SETTLE_MS = 1297; +const EDGE_INSERT_REFLOW_SETTLE_MS = 997; function CanvasInner({ canvasId }: CanvasInnerProps) { const t = useTranslations('toasts'); diff --git a/components/canvas/use-canvas-edge-insertions.ts b/components/canvas/use-canvas-edge-insertions.ts index 2b935ba..45f5119 100644 --- a/components/canvas/use-canvas-edge-insertions.ts +++ b/components/canvas/use-canvas-edge-insertions.ts @@ -27,7 +27,7 @@ export type EdgeInsertMenuState = { }; const EDGE_INSERT_GAP_PX = 10; -const DEFAULT_REFLOW_SETTLE_MS = 1297; +const DEFAULT_REFLOW_SETTLE_MS = 997; function waitForReflowSettle(ms: number): Promise { return new Promise((resolve) => { diff --git a/components/canvas/use-canvas-sync-engine.ts b/components/canvas/use-canvas-sync-engine.ts index 35057ce..b2f5460 100644 --- a/components/canvas/use-canvas-sync-engine.ts +++ b/components/canvas/use-canvas-sync-engine.ts @@ -746,6 +746,8 @@ export function useCanvasSyncEngine({ ]); }); + const createNodeWithEdgeSplitMut = useMutation(api.nodes.createWithEdgeSplit); + const createEdge = useMutation(api.edges.create).withOptimisticUpdate( (localStore, args) => { const edgeList = localStore.getQuery(api.edges.list, { @@ -847,10 +849,7 @@ export function useCanvasSyncEngine({ const addOptimisticNodeLocally = useCallback( ( - args: Parameters[0] & { - clientRequestId: string; - className?: string; - }, + args: Parameters[0] & { clientRequestId: string }, ): Id<"nodes"> => { const optimisticNodeId = `${OPTIMISTIC_NODE_PREFIX}${args.clientRequestId}`; setNodes((current) => { @@ -867,7 +866,6 @@ export function useCanvasSyncEngine({ style: { width: args.width, height: args.height }, parentId: args.parentId as string | undefined, zIndex: args.zIndex, - className: args.className, selected: false, }, ]; @@ -1392,24 +1390,15 @@ export function useCanvasSyncEngine({ ); const runCreateNodeWithEdgeSplitOnlineOnly = useCallback( - async (args: Parameters[0]) => { + async (args: Parameters[0]) => { const clientRequestId = args.clientRequestId ?? crypto.randomUUID(); const payload = { ...args, clientRequestId }; - const splitEdgeId = payload.splitEdgeId as string; - controller.pendingConnectionCreatesRef.current.add(clientRequestId); + if (isSyncOnline) { + return await createNodeWithEdgeSplitMut(payload); + } - const originalSplitEdge = edgesRef.current.find( - (edge) => - edge.id === splitEdgeId && - edge.className !== "temp" && - !isOptimisticEdgeId(edge.id), - ); - - const optimisticNodeId = addOptimisticNodeLocally({ - ...payload, - className: "canvas-edge-insert-enter", - }); + const optimisticNodeId = addOptimisticNodeLocally(payload); const splitApplied = applyEdgeSplitLocally({ clientRequestId, splitEdgeId: payload.splitEdgeId, @@ -1422,34 +1411,6 @@ export function useCanvasSyncEngine({ positionY: payload.positionY, }); - if (isSyncOnline) { - try { - const realId = await trackPendingNodeCreate( - clientRequestId, - createNodeWithEdgeSplitRaw({ ...payload }), - ); - await remapOptimisticNodeLocally(clientRequestId, realId); - return realId; - } catch (error) { - removeOptimisticCreateLocally({ - clientRequestId, - removeNode: true, - removeEdge: true, - }); - - if (splitApplied && originalSplitEdge) { - setEdges((current) => { - if (current.some((edge) => edge.id === originalSplitEdge.id)) { - return current; - } - return [...current, originalSplitEdge]; - }); - } - - throw error; - } - } - if (splitApplied) { await enqueueSyncMutation("createNodeWithEdgeSplit", payload); } else { @@ -1469,19 +1430,7 @@ export function useCanvasSyncEngine({ return optimisticNodeId; }, - [ - addOptimisticNodeLocally, - applyEdgeSplitLocally, - controller.pendingConnectionCreatesRef, - createNodeWithEdgeSplitRaw, - edgesRef, - enqueueSyncMutation, - isSyncOnline, - remapOptimisticNodeLocally, - removeOptimisticCreateLocally, - setEdges, - trackPendingNodeCreate, - ], + [addOptimisticNodeLocally, applyEdgeSplitLocally, createNodeWithEdgeSplitMut, enqueueSyncMutation, isSyncOnline], ); const runBatchRemoveNodesMutation = useCallback(