refactor(canvas): remove unused animation and optimize edge insertion timing

- Removed the unused CSS animation for edge insertion to streamline the codebase.
- Updated the edge insertion reflow duration from 1297ms to 997ms for improved performance.
- Refactored transition timing function for edge insertion to enhance animation smoothness.
- Cleaned up related test cases to reflect the changes in edge insertion logic.
This commit is contained in:
2026-04-06 21:30:56 +02:00
parent b7771764d8
commit 36e8b7d3db
5 changed files with 57 additions and 199 deletions

View File

@@ -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<RFNode[]>(initialNodes);
const [edges, setEdges] = useState<RFEdge[]>(initialEdges);
function HookHarness({ canvasId }: { canvasId: Id<"canvases"> }) {
const [, setNodes] = useState<RFNode[]>([]);
const [edges, setEdges] = useState<RFEdge[]>([]);
const edgesRef = useRef<RFEdge[]>(edges);
const deletingNodeIds = useRef(new Set<string>());
const [, setAssetBrowserTargetNodeId] = useState<string | null>(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<string>((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(
<HookHarness
canvasId={asCanvasId("canvas-1")}
initialNodes={[
{
id: "node-source",
type: "note",
position: { x: 0, y: 0 },
data: {},
style: { width: 120, height: 80 },
},
{
id: "node-target",
type: "note",
position: { x: 360, y: 0 },
data: {},
style: { width: 120, height: 80 },
},
]}
initialEdges={[
{
id: "edge-base",
source: "node-source",
target: "node-target",
sourceHandle: "source-handle",
targetHandle: "target-handle",
},
]}
/>,
);
});
let pendingCreateSplit: Promise<Id<"nodes">> | 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;
});
});
});

View File

@@ -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');

View File

@@ -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<void> {
return new Promise((resolve) => {

View File

@@ -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<typeof createNode>[0] & {
clientRequestId: string;
className?: string;
},
args: Parameters<typeof createNode>[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<typeof createNodeWithEdgeSplitRaw>[0]) => {
async (args: Parameters<typeof createNodeWithEdgeSplitMut>[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<RunBatchRemoveNodesMutation>(