fix: insert parameters at cursor position (#2867)

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Suchintan <suchintan@users.noreply.github.com>
Co-authored-by: Jonathan Dobson <jon.m.dobson@gmail.com>
This commit is contained in:
Nate
2025-07-09 18:39:52 -04:00
committed by GitHub
parent af7b862e02
commit ae816d5227
2 changed files with 116 additions and 39 deletions

View File

@@ -1,5 +1,12 @@
import { ChangeEventHandler, useEffect, useLayoutEffect, useRef } from "react";
import { Textarea } from "@/components/ui/textarea";
import type { ChangeEventHandler, HTMLAttributes } from "react";
import {
forwardRef,
useEffect,
useLayoutEffect,
useRef,
useCallback,
} from "react";
import { cn } from "@/util/utils";
type Props = {
@@ -8,44 +15,73 @@ type Props = {
className?: string;
readOnly?: boolean;
placeholder?: string;
};
onClick?: React.MouseEventHandler<HTMLTextAreaElement>;
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>;
onSelect?: React.ReactEventHandler<HTMLTextAreaElement>;
} & Omit<HTMLAttributes<HTMLTextAreaElement>, "onChange" | "value">;
function AutoResizingTextarea({
value,
onChange,
className,
readOnly,
placeholder,
}: Props) {
const ref = useRef<HTMLTextAreaElement>(null);
const AutoResizingTextarea = forwardRef<HTMLTextAreaElement, Props>(
(
{
value,
onChange,
className,
readOnly,
placeholder,
onClick,
onKeyUp,
onSelect,
...restProps
},
forwardedRef,
) => {
const innerRef = useRef<HTMLTextAreaElement | null>(null);
const getTextarea = useCallback(() => innerRef.current, []);
useLayoutEffect(() => {
// size the textarea correctly on first render
if (!ref.current) {
return;
}
ref.current.style.height = `${ref.current.scrollHeight + 2}px`;
}, []);
const setRefs = (element: HTMLTextAreaElement | null) => {
innerRef.current = element;
useEffect(() => {
if (!ref.current) {
return;
}
ref.current.style.height = "auto";
ref.current.style.height = `${ref.current.scrollHeight + 2}px`;
}, [value]);
// Forward to external ref
if (typeof forwardedRef === "function") {
forwardedRef(element);
} else if (forwardedRef) {
forwardedRef.current = element;
}
};
return (
<Textarea
value={value}
onChange={onChange}
readOnly={readOnly}
placeholder={placeholder}
ref={ref}
rows={1}
className={cn("min-h-0 resize-none overflow-y-hidden", className)}
/>
);
}
useLayoutEffect(() => {
const textareaElement = getTextarea();
if (!textareaElement) {
return;
}
textareaElement.style.height = `${textareaElement.scrollHeight + 2}px`;
}, [getTextarea]);
useEffect(() => {
const textareaElement = getTextarea();
if (!textareaElement) {
return;
}
textareaElement.style.height = "auto";
textareaElement.style.height = `${textareaElement.scrollHeight + 2}px`;
}, [getTextarea, value]);
return (
<Textarea
value={value}
onChange={onChange}
readOnly={readOnly}
placeholder={placeholder}
onClick={onClick}
onKeyUp={onKeyUp}
onSelect={onSelect}
ref={setRefs}
rows={1}
className={cn("min-h-0 resize-none overflow-y-hidden", className)}
{...restProps}
/>
);
},
);
export { AutoResizingTextarea };

View File

@@ -3,6 +3,7 @@ import { cn } from "@/util/utils";
import { AutoResizingTextarea } from "./AutoResizingTextarea/AutoResizingTextarea";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { WorkflowBlockParameterSelect } from "@/routes/workflows/editor/nodes/WorkflowBlockParameterSelect";
import { useRef, useState } from "react";
type Props = Omit<
React.ComponentProps<typeof AutoResizingTextarea>,
@@ -14,14 +15,56 @@ type Props = Omit<
function WorkflowBlockInputTextarea(props: Props) {
const { nodeId, onChange, ...textAreaProps } = props;
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [cursorPosition, setCursorPosition] = useState<{
start: number;
end: number;
} | null>(null);
const handleTextareaSelect = () => {
if (textareaRef.current) {
setCursorPosition({
start: textareaRef.current.selectionStart,
end: textareaRef.current.selectionEnd,
});
}
};
const insertParameterAtCursor = (parameterKey: string) => {
const value = props.value ?? "";
const parameterText = `{{${parameterKey}}}`;
if (cursorPosition && textareaRef.current) {
const { start, end } = cursorPosition;
const newValue =
value.substring(0, start) + parameterText + value.substring(end);
onChange(newValue);
setTimeout(() => {
if (textareaRef.current) {
const newPosition = start + parameterText.length;
textareaRef.current.focus();
textareaRef.current.setSelectionRange(newPosition, newPosition);
}
}, 0);
} else {
onChange(`${value}${parameterText}`);
}
};
return (
<div className="relative">
<AutoResizingTextarea
{...textAreaProps}
ref={textareaRef}
onChange={(event) => {
onChange(event.target.value);
handleTextareaSelect();
}}
onClick={handleTextareaSelect}
onKeyUp={handleTextareaSelect}
onSelect={handleTextareaSelect}
className={cn("pr-9", props.className)}
/>
<div className="absolute right-0 top-0 flex size-9 cursor-pointer items-center justify-center">
@@ -34,9 +77,7 @@ function WorkflowBlockInputTextarea(props: Props) {
<PopoverContent className="w-[22rem]">
<WorkflowBlockParameterSelect
nodeId={nodeId}
onAdd={(parameterKey) => {
onChange(`${props.value ?? ""}{{${parameterKey}}}`);
}}
onAdd={insertParameterAtCursor}
/>
</PopoverContent>
</Popover>