This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import type { ChangeEventHandler, HTMLAttributes } from "react";
|
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";
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -30,6 +30,7 @@ const AutoResizingTextarea = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
forwardedRef,
|
forwardedRef,
|
||||||
) => {
|
) => {
|
||||||
const innerRef = useRef<HTMLTextAreaElement | null>(null);
|
const innerRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
const lastHeightRef = useRef<string>("");
|
||||||
const getTextarea = useCallback(() => innerRef.current, []);
|
const getTextarea = useCallback(() => innerRef.current, []);
|
||||||
|
|
||||||
const setRefs = (element: HTMLTextAreaElement | null) => {
|
const setRefs = (element: HTMLTextAreaElement | null) => {
|
||||||
@@ -43,13 +44,25 @@ const AutoResizingTextarea = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const textareaElement = getTextarea();
|
const textareaElement = getTextarea();
|
||||||
if (!textareaElement) {
|
if (!textareaElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Temporarily set to auto to measure scrollHeight accurately
|
||||||
textareaElement.style.height = "auto";
|
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]);
|
}, [getTextarea, value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { useBlocker, useParams } from "react-router-dom";
|
import { useBlocker, useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
AWSSecretParameter,
|
AWSSecretParameter,
|
||||||
@@ -321,6 +322,9 @@ function FlowRenderer({
|
|||||||
// Track if this is the initial load to prevent false "unsaved changes" detection
|
// Track if this is the initial load to prevent false "unsaved changes" detection
|
||||||
const isInitialLoadRef = useRef(true);
|
const isInitialLoadRef = useRef(true);
|
||||||
|
|
||||||
|
// Track if we're currently in a layout operation to prevent infinite loops
|
||||||
|
const isLayoutingRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nodesInitialized) {
|
if (nodesInitialized) {
|
||||||
setShouldConstrainPan(true);
|
setShouldConstrainPan(true);
|
||||||
@@ -364,6 +368,27 @@ function FlowRenderer({
|
|||||||
[setNodes, setEdges, targettedBlockLabel],
|
[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(() => {
|
useEffect(() => {
|
||||||
if (nodesInitialized) {
|
if (nodesInitialized) {
|
||||||
doLayout(nodes, edges);
|
doLayout(nodes, edges);
|
||||||
@@ -850,21 +875,37 @@ function FlowRenderer({
|
|||||||
const dimensionChanges = changes.filter(
|
const dimensionChanges = changes.filter(
|
||||||
(change) => change.type === "dimensions",
|
(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) {
|
// Only process dimension changes if we're not already in a layout operation
|
||||||
doLayout(tempNodes, edges);
|
// 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
|
// Only track changes after initial load is complete and not during internal updates
|
||||||
|
|||||||
Reference in New Issue
Block a user