Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import { restoreCursorAfterInsertion } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
Expand Down Expand Up @@ -537,36 +538,40 @@ export const Code = memo(function Code({
/**
* Handles selection of a tag from the tag dropdown.
* @param newValue - The new code value with the selected tag inserted
* @param newCursorPosition - The cursor position after the inserted tag
*/
const handleTagSelect = (newValue: string) => {
const handleTagSelect = (newValue: string, newCursorPosition: number) => {
const textarea = editorRef.current?.querySelector('textarea') as HTMLTextAreaElement | null

if (!isPreview && !readOnly) {
setCode(newValue)
emitTagSelection(newValue)
recordChange(newValue)
restoreCursorAfterInsertion(textarea, newCursorPosition)
} else {
setTimeout(() => textarea?.focus(), 0)
}
setShowTags(false)
setActiveSourceBlockId(null)

setTimeout(() => {
editorRef.current?.querySelector('textarea')?.focus()
}, 0)
}

/**
* Handles selection of an environment variable from the dropdown.
* @param newValue - The new code value with the selected env var inserted
* @param newCursorPosition - The cursor position after the inserted env var
*/
const handleEnvVarSelect = (newValue: string) => {
const handleEnvVarSelect = (newValue: string, newCursorPosition: number) => {
const textarea = editorRef.current?.querySelector('textarea') as HTMLTextAreaElement | null

if (!isPreview && !readOnly) {
setCode(newValue)
emitTagSelection(newValue)
recordChange(newValue)
restoreCursorAfterInsertion(textarea, newCursorPosition)
} else {
setTimeout(() => textarea?.focus(), 0)
}
setShowEnvVars(false)

setTimeout(() => {
editorRef.current?.querySelector('textarea')?.focus()
}, 0)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
TagDropdown,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { restoreCursorAfterInsertion } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { normalizeName } from '@/executor/constants'
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
Expand Down Expand Up @@ -554,9 +555,17 @@ export function ConditionInput({
)
}

const handleTagSelectImmediate = (blockId: string, newValue: string) => {
const handleTagSelectImmediate = (
blockId: string,
newValue: string,
newCursorPosition: number
) => {
if (isPreview || disabled) return

const textarea = containerRef.current?.querySelector(
`[data-block-id="${CSS.escape(blockId)}"] textarea`
) as HTMLTextAreaElement | null

shouldPersistRef.current = true
setConditionalBlocks((blocks) =>
blocks.map((block) =>
Expand All @@ -582,11 +591,21 @@ export function ConditionInput({
: block
)
emitTagSelection(JSON.stringify(updatedBlocks))

restoreCursorAfterInsertion(textarea, newCursorPosition)
}

const handleEnvVarSelectImmediate = (blockId: string, newValue: string) => {
const handleEnvVarSelectImmediate = (
blockId: string,
newValue: string,
newCursorPosition: number
) => {
if (isPreview || disabled) return

const textarea = containerRef.current?.querySelector(
`[data-block-id="${CSS.escape(blockId)}"] textarea`
) as HTMLTextAreaElement | null

shouldPersistRef.current = true
setConditionalBlocks((blocks) =>
blocks.map((block) =>
Expand All @@ -612,6 +631,8 @@ export function ConditionInput({
: block
)
emitTagSelection(JSON.stringify(updatedBlocks))

restoreCursorAfterInsertion(textarea, newCursorPosition)
}

/**
Expand Down Expand Up @@ -999,7 +1020,9 @@ export function ConditionInput({
{block.showEnvVars && (
<EnvVarDropdown
visible={block.showEnvVars}
onSelect={(newValue) => handleEnvVarSelectImmediate(block.id, newValue)}
onSelect={(newValue, newCursorPosition) =>
handleEnvVarSelectImmediate(block.id, newValue, newCursorPosition)
}
searchTerm={block.searchTerm}
inputValue={block.value}
cursorPosition={block.cursorPosition}
Expand All @@ -1023,7 +1046,9 @@ export function ConditionInput({
{block.showTags && (
<TagDropdown
visible={block.showTags}
onSelect={(newValue) => handleTagSelectImmediate(block.id, newValue)}
onSelect={(newValue, newCursorPosition) =>
handleTagSelectImmediate(block.id, newValue, newCursorPosition)
}
blockId={blockId}
activeSourceBlockId={block.activeSourceBlockId}
inputValue={block.value}
Expand Down Expand Up @@ -1207,7 +1232,9 @@ export function ConditionInput({
{block.showEnvVars && (
<EnvVarDropdown
visible={block.showEnvVars}
onSelect={(newValue) => handleEnvVarSelectImmediate(block.id, newValue)}
onSelect={(newValue, newCursorPosition) =>
handleEnvVarSelectImmediate(block.id, newValue, newCursorPosition)
}
searchTerm={block.searchTerm}
inputValue={block.value}
cursorPosition={block.cursorPosition}
Expand All @@ -1225,7 +1252,9 @@ export function ConditionInput({
{block.showTags && (
<TagDropdown
visible={block.showTags}
onSelect={(newValue) => handleTagSelectImmediate(block.id, newValue)}
onSelect={(newValue, newCursorPosition) =>
handleTagSelectImmediate(block.id, newValue, newCursorPosition)
}
blockId={blockId}
activeSourceBlockId={block.activeSourceBlockId}
inputValue={block.value}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface EnvVarDropdownProps {
/** Whether the dropdown is visible */
visible: boolean
/** Callback when an environment variable is selected */
onSelect: (newValue: string) => void
onSelect: (newValue: string, newCursorPosition: number) => void
/** Search term to filter environment variables */
searchTerm?: string
/** Additional CSS class names */
Expand Down Expand Up @@ -189,20 +189,19 @@ export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({

const isStandardEnvVarContext = lastOpenBraces !== -1

const tagLength = 2 + envVar.length + 2

if (isStandardEnvVarContext) {
const startText = textBeforeCursor.slice(0, lastOpenBraces)

const closeIndex = textAfterCursor.indexOf('}}')
const endText = closeIndex !== -1 ? textAfterCursor.slice(closeIndex + 2) : textAfterCursor

const newValue = `${startText}{{${envVar}}}${endText}`
onSelect(newValue)
onSelect(newValue, lastOpenBraces + tagLength)
} else {
if (inputValue.trim() !== '') {
onSelect(`{{${envVar}}}`)
} else {
onSelect(`{{${envVar}}}`)
}
const newValue = `{{${envVar}}}`
onSelect(newValue, tagLength)
}

onClose?.()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ interface TagDropdownProps {
/** Whether the dropdown is visible */
visible: boolean
/** Callback when a tag is selected */
onSelect: (newValue: string) => void
onSelect: (newValue: string, newCursorPosition: number) => void
/** ID of the block that owns the input field */
blockId: string
/** ID of the specific source block being referenced, if any */
Expand Down Expand Up @@ -1588,10 +1588,12 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}

let newValue: string
let insertStart: number

if (lastOpenBracket === -1) {
// No '<' found - insert the full tag at cursor position
newValue = `${textBeforeCursor}<${processedTag}>${textAfterCursor}`
insertStart = liveCursor
} else {
// '<' found - replace from '<' to cursor (and consume trailing '>' if present)
const nextCloseBracket = textAfterCursor.indexOf('>')
Expand All @@ -1605,9 +1607,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}

newValue = `${textBeforeCursor.slice(0, lastOpenBracket)}<${processedTag}>${remainingTextAfterCursor}`
insertStart = lastOpenBracket
}

onSelect(newValue)
const newCursorPos = insertStart + 1 + processedTag.length + 1
onSelect(newValue, newCursorPos)
onClose?.()
},
[workflowVariables, onSelect, onClose, getMergedSubBlocks]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
splitReferenceSegment,
} from '@/lib/workflows/sanitization/references'
import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { restoreCursorAfterInsertion } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { normalizeName, REFERENCE } from '@/executor/constants'
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
Expand Down Expand Up @@ -60,7 +61,6 @@ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId

const textareaRef = useRef<HTMLTextAreaElement | null>(null)
const editorContainerRef = useRef<HTMLDivElement>(null)

const [tempInputValue, setTempInputValue] = useState<string | null>(null)
const [showTagDropdown, setShowTagDropdown] = useState(false)
const [cursorPosition, setCursorPosition] = useState(0)
Expand Down Expand Up @@ -289,21 +289,17 @@ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId
* Handle tag selection from dropdown
*/
const handleSubflowTagSelect = useCallback(
(newValue: string) => {
(newValue: string, newCursorPosition: number) => {
if (!currentBlockId || !isSubflow || !currentBlock) return

collaborativeUpdateIterationCollection(
currentBlockId,
currentBlock.type as 'loop' | 'parallel',
newValue
)
setShowTagDropdown(false)

setTimeout(() => {
const textarea = textareaRef.current
if (textarea) {
textarea.focus()
}
}, 0)
restoreCursorAfterInsertion(textareaRef.current, newCursorPosition)
},
[currentBlockId, isSubflow, currentBlock, collaborativeUpdateIterationCollection]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Restores the cursor position in a textarea after a dropdown insertion.
* Schedules a macrotask (via setTimeout) that runs after React's controlled-component commit
* so that the cursor position sticks.
*
* @param textarea - The textarea element to restore cursor in (may be null)
* @param newCursorPosition - The exact position to place the cursor at
*/
export function restoreCursorAfterInsertion(
textarea: HTMLTextAreaElement | null,
newCursorPosition: number
): void {
setTimeout(() => {
if (textarea) {
textarea.focus()
textarea.selectionStart = newCursorPosition
textarea.selectionEnd = newCursorPosition
}
}, 0)
}