Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -539,34 +540,40 @@ export const Code = memo(function Code({
* @param newValue - The new code value with the selected tag inserted
*/
const handleTagSelect = (newValue: string) => {
const textarea = editorRef.current?.querySelector('textarea') as HTMLTextAreaElement | null
const liveCursor = textarea?.selectionStart ?? cursorPosition
const liveValue = textarea?.value ?? code

if (!isPreview && !readOnly) {
setCode(newValue)
emitTagSelection(newValue)
recordChange(newValue)
restoreCursorAfterInsertion(textarea, liveValue, liveCursor, newValue, 'tag')
} 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
*/
const handleEnvVarSelect = (newValue: string) => {
const textarea = editorRef.current?.querySelector('textarea') as HTMLTextAreaElement | null
const liveCursor = textarea?.selectionStart ?? cursorPosition
const liveValue = textarea?.value ?? code

if (!isPreview && !readOnly) {
setCode(newValue)
emitTagSelection(newValue)
recordChange(newValue)
restoreCursorAfterInsertion(textarea, liveValue, liveCursor, newValue, 'envVar')
} 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 @@ -557,6 +558,13 @@ export function ConditionInput({
const handleTagSelectImmediate = (blockId: string, newValue: string) => {
if (isPreview || disabled) return

const textarea = containerRef.current?.querySelector(
`[data-block-id="${CSS.escape(blockId)}"] textarea`
) as HTMLTextAreaElement | null
Comment thread
waleedlatif1 marked this conversation as resolved.
const blockValue = conditionalBlocks.find((b) => b.id === blockId)?.value ?? ''
const liveCursor = textarea?.selectionStart ?? 0
const liveValue = textarea?.value ?? blockValue

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

restoreCursorAfterInsertion(textarea, liveValue, liveCursor, newValue, 'tag')
}

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

const textarea = containerRef.current?.querySelector(
`[data-block-id="${CSS.escape(blockId)}"] textarea`
) as HTMLTextAreaElement | null
const blockValue = conditionalBlocks.find((b) => b.id === blockId)?.value ?? ''
const liveCursor = textarea?.selectionStart ?? 0
const liveValue = textarea?.value ?? blockValue

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

restoreCursorAfterInsertion(textarea, liveValue, liveCursor, newValue, 'envVar')
}

/**
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,6 +61,7 @@ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId

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

const [tempInputValue, setTempInputValue] = useState<string | null>(null)
const [showTagDropdown, setShowTagDropdown] = useState(false)
Expand Down Expand Up @@ -291,21 +293,27 @@ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId
const handleSubflowTagSelect = useCallback(
(newValue: string) => {
if (!currentBlockId || !isSubflow || !currentBlock) return

const textarea = textareaRef.current
const liveCursor = textarea?.selectionStart ?? cursorPosition
const liveValue = textarea?.value ?? editorValueRef.current

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

setTimeout(() => {
const textarea = textareaRef.current
if (textarea) {
textarea.focus()
}
}, 0)
restoreCursorAfterInsertion(textarea, liveValue, liveCursor, newValue, 'tag')
},
[currentBlockId, isSubflow, currentBlock, collaborativeUpdateIterationCollection]
[
currentBlockId,
isSubflow,
currentBlock,
collaborativeUpdateIterationCollection,
cursorPosition,
]
)
Comment thread
waleedlatif1 marked this conversation as resolved.
Comment thread
waleedlatif1 marked this conversation as resolved.

// Compute derived values
Expand Down Expand Up @@ -352,6 +360,7 @@ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId

const inputValue = tempInputValue ?? iterations.toString()
const editorValue = isConditionMode ? conditionString : collectionString
editorValueRef.current = editorValue

// Type options for combobox
const typeOptions =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Type of dropdown insertion that determines the delimiter pair used
* for cursor position computation.
*/
export type InsertionType = 'tag' | 'envVar'

/**
* Restores the cursor position in a textarea after a tag or env-var
* dropdown insertion. Computes where the inserted token ends in the
* new value and places the cursor right after it.
*
Comment thread
waleedlatif1 marked this conversation as resolved.
* @param textarea - The textarea element to restore cursor in
* @param liveValue - The textarea value before the insertion
* @param liveCursor - The cursor position before the insertion
* @param newValue - The full new value after the insertion
* @param type - The type of insertion ('tag' for `<>`, 'envVar' for `{{}}`)
*/
export function restoreCursorAfterInsertion(
textarea: HTMLTextAreaElement | null,
liveValue: string,
liveCursor: number,
newValue: string,
type: InsertionType
): void {
const [openDelim, closeDelim, closeLen] =
type === 'tag' ? (['<', '>', 1] as const) : (['{{', '}}', 2] as const)

// insertPos indexes into liveValue, but is reused to search newValue.
// This is valid because text before the trigger character is identical
// in both strings — the insertion only mutates text at/after the delimiter.
const insertPos = liveValue.slice(0, liveCursor).lastIndexOf(openDelim)
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
const searchFrom = insertPos !== -1 ? insertPos : liveCursor
const closingPos = newValue.indexOf(closeDelim, searchFrom)
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
const newCursorPos = closingPos !== -1 ? closingPos + closeLen : newValue.length

setTimeout(() => {
if (textarea) {
textarea.focus()
textarea.selectionStart = newCursorPos
textarea.selectionEnd = newCursorPos
}
}, 0)
}