This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { ChangeEventHandler, HTMLAttributes } from "react";
|
||||
import { forwardRef, useEffect, useRef, useCallback } from "react";
|
||||
import { forwardRef, useRef, useCallback, useLayoutEffect } from "react";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
type Props = {
|
||||
@@ -30,6 +30,7 @@ const AutoResizingTextarea = forwardRef<HTMLTextAreaElement, Props>(
|
||||
forwardedRef,
|
||||
) => {
|
||||
const innerRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const lastHeightRef = useRef<string>("");
|
||||
const getTextarea = useCallback(() => innerRef.current, []);
|
||||
|
||||
const setRefs = (element: HTMLTextAreaElement | null) => {
|
||||
@@ -43,13 +44,25 @@ const AutoResizingTextarea = forwardRef<HTMLTextAreaElement, Props>(
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
const textareaElement = getTextarea();
|
||||
if (!textareaElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily set to auto to measure scrollHeight accurately
|
||||
textareaElement.style.height = "auto";
|
||||
textareaElement.style.height = `${textareaElement.scrollHeight + 2}px`;
|
||||
const newHeight = `${textareaElement.scrollHeight + 2}px`;
|
||||
|
||||
// Only apply the final height if it differs from the last applied height
|
||||
// This prevents unnecessary dimension change events in React Flow
|
||||
if (lastHeightRef.current !== newHeight) {
|
||||
lastHeightRef.current = newHeight;
|
||||
textareaElement.style.height = newHeight;
|
||||
} else {
|
||||
// Restore the previous height since nothing changed
|
||||
textareaElement.style.height = lastHeightRef.current;
|
||||
}
|
||||
}, [getTextarea, value]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { useBlocker, useParams } from "react-router-dom";
|
||||
import {
|
||||
AWSSecretParameter,
|
||||
@@ -321,6 +322,9 @@ function FlowRenderer({
|
||||
// Track if this is the initial load to prevent false "unsaved changes" detection
|
||||
const isInitialLoadRef = useRef(true);
|
||||
|
||||
// Track if we're currently in a layout operation to prevent infinite loops
|
||||
const isLayoutingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesInitialized) {
|
||||
setShouldConstrainPan(true);
|
||||
@@ -364,6 +368,27 @@ function FlowRenderer({
|
||||
[setNodes, setEdges, targettedBlockLabel],
|
||||
);
|
||||
|
||||
// Debounced layout for dimension changes to prevent infinite loops (React error #185)
|
||||
// when copy-pasting triggers rapid successive dimension changes
|
||||
const debouncedLayoutForDimensions = useDebouncedCallback(
|
||||
(tempNodes: Array<AppNode>, currentEdges: Array<Edge>) => {
|
||||
if (isLayoutingRef.current) {
|
||||
return;
|
||||
}
|
||||
isLayoutingRef.current = true;
|
||||
try {
|
||||
doLayout(tempNodes, currentEdges);
|
||||
} finally {
|
||||
// Reset the flag after a short delay to allow React to flush updates
|
||||
requestAnimationFrame(() => {
|
||||
isLayoutingRef.current = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
50,
|
||||
{ leading: true, trailing: true, maxWait: 200 },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesInitialized) {
|
||||
doLayout(nodes, edges);
|
||||
@@ -850,21 +875,37 @@ function FlowRenderer({
|
||||
const dimensionChanges = changes.filter(
|
||||
(change) => change.type === "dimensions",
|
||||
);
|
||||
const tempNodes = [...nodes];
|
||||
dimensionChanges.forEach((change) => {
|
||||
const node = tempNodes.find((node) => node.id === change.id);
|
||||
if (node) {
|
||||
if (node.measured?.width) {
|
||||
node.measured.width = change.dimensions?.width;
|
||||
}
|
||||
if (node.measured?.height) {
|
||||
node.measured.height = change.dimensions?.height;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (dimensionChanges.length > 0) {
|
||||
doLayout(tempNodes, edges);
|
||||
// Only process dimension changes if we're not already in a layout operation
|
||||
// This prevents infinite loops (React error #185) during copy-paste
|
||||
if (dimensionChanges.length > 0 && !isLayoutingRef.current) {
|
||||
const tempNodes = [...nodes];
|
||||
let hasActualChanges = false;
|
||||
|
||||
dimensionChanges.forEach((change) => {
|
||||
const node = tempNodes.find((node) => node.id === change.id);
|
||||
if (node) {
|
||||
const newWidth = change.dimensions?.width;
|
||||
const newHeight = change.dimensions?.height;
|
||||
|
||||
// Only update if dimensions actually changed
|
||||
if (
|
||||
node.measured?.width !== newWidth ||
|
||||
node.measured?.height !== newHeight
|
||||
) {
|
||||
hasActualChanges = true;
|
||||
if (node.measured) {
|
||||
node.measured.width = newWidth;
|
||||
node.measured.height = newHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Only trigger layout if there were actual dimension changes
|
||||
if (hasActualChanges) {
|
||||
debouncedLayoutForDimensions(tempNodes, edges);
|
||||
}
|
||||
}
|
||||
|
||||
// Only track changes after initial load is complete and not during internal updates
|
||||
|
||||
Reference in New Issue
Block a user