diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/components/popover/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/components/popover/index.tsx index 4613275dd..ac1b30a5d 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/components/popover/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/components/popover/index.tsx @@ -1,7 +1,7 @@ import { PopoverAnchor } from "@radix-ui/react-popover"; -import { uniqueId } from "lodash"; + import { X } from "lucide-react"; -import { type ReactNode, useMemo, useState } from "react"; +import { type ReactNode, useEffect, useMemo, useState } from "react"; import ForwardedIconComponent from "@/components/common/genericIconComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent"; import { Badge } from "@/components/ui/badge"; @@ -189,12 +189,20 @@ const CustomInputPopover = ({ hasRefreshButton, }) => { const [isFocused, setIsFocused] = useState(false); + const [cursor, setCursor] = useState(null); const memoizedOptions = useMemo(() => new Set(options), [options]); const PopoverContentInput = editNode ? PopoverContent : PopoverContentWithoutPortal; + // Restore cursor position after value changes + useEffect(() => { + if (cursor !== null && refInput.current) { + refInput.current.setSelectionRange(cursor, cursor); + } + }, [cursor, value]); + const handleRemoveOption = ( optionToRemove: string, e: React.MouseEvent, @@ -270,7 +278,7 @@ const CustomInputPopover = ({ autoComplete="off" onFocus={() => setIsFocused(true)} autoFocus={autoFocus} - id={id + uniqueId()} + id={id} ref={refInput} type={!pwdVisible && password ? "password" : "text"} onBlur={() => { @@ -292,7 +300,10 @@ const CustomInputPopover = ({ ? "" : placeholder } - onChange={(e) => onChange?.(e.target.value)} + onChange={(e) => { + setCursor(e.target.selectionStart); + onChange?.(e.target.value); + }} onKeyDown={(e) => { handleKeyDown?.(e); if (blurOnEnter && e.key === "Enter") refInput.current?.blur(); diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/components/popoverObject/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/components/popoverObject/index.tsx index 191c76085..10b44aa76 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/components/popoverObject/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/components/popoverObject/index.tsx @@ -1,4 +1,5 @@ import { PopoverAnchor } from "@radix-ui/react-popover"; +import { useEffect, useState } from "react"; import ForwardedIconComponent from "@/components/common/genericIconComponent"; import { Command, @@ -40,11 +41,21 @@ const CustomInputPopoverObject = ({ handleKeyDown, showOptions, }) => { + const [cursor, setCursor] = useState(null); + const PopoverContentInput = editNode ? PopoverContent : PopoverContentWithoutPortal; + // Restore cursor position after value changes + useEffect(() => { + if (cursor !== null && refInput.current) { + refInput.current.setSelectionRange(cursor, cursor); + } + }, [cursor, value]); + const handleInputChange = (e) => { + setCursor(e.target.selectionStart); onChange && onChange(e.target.value); }; diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/index.tsx index 11889f12d..11ca476fd 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/inputComponent/index.tsx @@ -45,6 +45,7 @@ export default function InputComponent({ hasRefreshButton = false, }: InputComponentType): JSX.Element { const [pwdVisible, setPwdVisible] = useState(false); + const [cursor, setCursor] = useState(null); const refInput = useRef(null); const [showOptions, setShowOptions] = useState(false); @@ -54,6 +55,13 @@ export default function InputComponent({ } }, [disabled]); + // Restore cursor position after value changes + useEffect(() => { + if (cursor !== null && refInput.current) { + refInput.current.setSelectionRange(cursor, cursor); + } + }, [cursor, value]); + function onInputLostFocus(event): void { if (onBlur) onBlur(event); } @@ -83,6 +91,7 @@ export default function InputComponent({ )} placeholder={password && editNode ? "Key" : placeholder} onChange={(e) => { + setCursor(e.target.selectionStart); if (onChangeFolderName) { return onChangeFolderName(e); } diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/textAreaComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/textAreaComponent/index.tsx index 2a844ac59..07a1726af 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/textAreaComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/textAreaComponent/index.tsx @@ -74,6 +74,7 @@ export default function TextAreaComponent({ const inputRef = useRef(null); const [isFocused, setIsFocused] = useState(false); const [passwordVisible, setPasswordVisible] = useState(false); + const [cursor, setCursor] = useState(null); const isWebhook = useMemo( () => nodeInformationMetadata?.nodeType === "webhook", @@ -100,6 +101,13 @@ export default function TextAreaComponent({ } }, [isWebhook, value, nodeInformationMetadata, handleOnNewValue]); + // Restore cursor position after value changes + useEffect(() => { + if (cursor !== null && inputRef.current) { + inputRef.current.setSelectionRange(cursor, cursor); + } + }, [cursor, value]); + const getInputClassName = () => { return cn( inputClasses.base({ isFocused, password: password! }), @@ -111,6 +119,7 @@ export default function TextAreaComponent({ }; const handleInputChange = (e: React.ChangeEvent) => { + setCursor(e.target.selectionStart); handleOnNewValue({ value: e.target.value }); }; diff --git a/src/frontend/tests/core/unit/inputComponent.spec.ts b/src/frontend/tests/core/unit/inputComponent.spec.ts index 08d26a4f8..fb3d985c0 100644 --- a/src/frontend/tests/core/unit/inputComponent.spec.ts +++ b/src/frontend/tests/core/unit/inputComponent.spec.ts @@ -38,6 +38,19 @@ test( expect(false).toBeTruthy(); } + // Test cursor position preservation + const input = page.getByTestId("popover-anchor-input-collection_name"); + await input.click(); + await input.press("Home"); // Move cursor to start + await input.press("ArrowRight"); // Move cursor to position 1 + await input.press("ArrowRight"); // Move cursor to position 2 + await input.pressSequentially("X", { delay: 100 }); // Type at position 2 + const cursorValue = await input.inputValue(); + if (!cursorValue.startsWith("coX")) { + expect(false).toBeTruthy(); + } + await input.fill("collection_name_test_123123123!@#$&*(&%$@"); + await page.getByTestId("div-generic-node").click(); await page.getByTestId("edit-button-modal").last().click(); diff --git a/src/frontend/tests/core/unit/textAreaModalComponent.spec.ts b/src/frontend/tests/core/unit/textAreaModalComponent.spec.ts index 2621a49c0..2a6218564 100644 --- a/src/frontend/tests/core/unit/textAreaModalComponent.spec.ts +++ b/src/frontend/tests/core/unit/textAreaModalComponent.spec.ts @@ -44,6 +44,21 @@ test( "test test test test test test test test test test test !@#%*)( 123456789101010101010101111111111 !!!!!!!!!!", ); + // Test cursor position preservation + const textInput = page.getByTestId("textarea_str_text"); + await textInput.click(); + await textInput.press("Home"); // Move cursor to start + await textInput.press("ArrowRight"); // Move cursor to position 1 + await textInput.press("ArrowRight"); // Move cursor to position 2 + await textInput.pressSequentially("Y", { delay: 100 }); // Type at position 2 + const cursorValue = await textInput.inputValue(); + if (!cursorValue.startsWith("teY")) { + expect(false).toBeTruthy(); + } + await textInput.fill( + "test test test test test test test test test test test !@#%*)( 123456789101010101010101111111111 !!!!!!!!!!", + ); + await page .getByTestId("button_open_text_area_modal_textarea_str_text") .click();