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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>(
|
||||
|
||||
Reference in New Issue
Block a user