fix(sockets): add sockets event for tag / env var dropdown selections (#844)

* fix(sockets): add sockets event for tag / env var dropdown selections to be unit op

* do not bypass op queue for tag selections

* Update apps/sim/socket-server/handlers/subblocks.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* prevent race cond between subblock update event and tag selection

* refactor

* reduce debounce time to 50ms

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Vikhyath Mondreti
2025-08-01 15:22:56 -07:00
committed by GitHub
parent 2e2be9bf38
commit 3bd7a6c402
10 changed files with 222 additions and 47 deletions

View File

@@ -14,6 +14,7 @@ import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
import type { GenerationType } from '@/blocks/types'
import { useTagSelection } from '@/hooks/use-tag-selection'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
const logger = createLogger('Code')
@@ -164,6 +165,8 @@ export function Code({
},
})
const emitTagSelection = useTagSelection(blockId, subBlockId)
// Use preview value when in preview mode, otherwise use store value or prop value
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
@@ -306,7 +309,7 @@ export function Code({
const handleTagSelect = (newValue: string) => {
if (!isPreview) {
setCode(newValue)
setStoreValue(newValue)
emitTagSelection(newValue)
}
setShowTags(false)
setActiveSourceBlockId(null)
@@ -319,7 +322,7 @@ export function Code({
const handleEnvVarSelect = (newValue: string) => {
if (!isPreview) {
setCode(newValue)
setStoreValue(newValue)
emitTagSelection(newValue)
}
setShowEnvVars(false)

View File

@@ -10,6 +10,7 @@ import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import type { SubBlockConfig } from '@/blocks/types'
import { useTagSelection } from '@/hooks/use-tag-selection'
const logger = createLogger('ComboBox')
@@ -53,6 +54,8 @@ export function ComboBox({
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
const [highlightedIndex, setHighlightedIndex] = useState(-1)
const emitTagSelection = useTagSelection(blockId, subBlockId)
const inputRef = useRef<HTMLInputElement>(null)
const overlayRef = useRef<HTMLDivElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
@@ -330,7 +333,7 @@ export function ComboBox({
// Environment variable and tag selection handler
const handleEnvVarSelect = (newValue: string) => {
if (!isPreview) {
setStoreValue(newValue)
emitTagSelection(newValue)
}
}

View File

@@ -14,6 +14,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import { useTagSelection } from '@/hooks/use-tag-selection'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('ConditionInput')
@@ -52,6 +53,9 @@ export function ConditionInput({
disabled = false,
}: ConditionInputProps) {
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const emitTagSelection = useTagSelection(blockId, subBlockId)
const containerRef = useRef<HTMLDivElement>(null)
const [visualLineHeights, setVisualLineHeights] = useState<{
[key: string]: number[]
@@ -400,6 +404,64 @@ export function ConditionInput({
)
}
const handleTagSelectImmediate = (blockId: string, newValue: string) => {
if (isPreview || disabled) return
setConditionalBlocks((blocks) =>
blocks.map((block) =>
block.id === blockId
? {
...block,
value: newValue,
showTags: false,
activeSourceBlockId: null,
}
: block
)
)
const updatedBlocks = conditionalBlocks.map((block) =>
block.id === blockId
? {
...block,
value: newValue,
showTags: false,
activeSourceBlockId: null,
}
: block
)
emitTagSelection(JSON.stringify(updatedBlocks))
}
const handleEnvVarSelectImmediate = (blockId: string, newValue: string) => {
if (isPreview || disabled) return
setConditionalBlocks((blocks) =>
blocks.map((block) =>
block.id === blockId
? {
...block,
value: newValue,
showEnvVars: false,
searchTerm: '',
}
: block
)
)
const updatedBlocks = conditionalBlocks.map((block) =>
block.id === blockId
? {
...block,
value: newValue,
showEnvVars: false,
searchTerm: '',
}
: block
)
emitTagSelection(JSON.stringify(updatedBlocks))
}
// Update block titles based on position
const updateBlockTitles = (blocks: ConditionalBlock[]): ConditionalBlock[] => {
return blocks.map((block, index) => ({
@@ -706,7 +768,7 @@ export function ConditionInput({
{block.showEnvVars && (
<EnvVarDropdown
visible={block.showEnvVars}
onSelect={(newValue) => handleEnvVarSelect(block.id, newValue)}
onSelect={(newValue) => handleEnvVarSelectImmediate(block.id, newValue)}
searchTerm={block.searchTerm}
inputValue={block.value}
cursorPosition={block.cursorPosition}
@@ -723,7 +785,7 @@ export function ConditionInput({
{block.showTags && (
<TagDropdown
visible={block.showTags}
onSelect={(newValue) => handleTagSelect(block.id, newValue)}
onSelect={(newValue) => handleTagSelectImmediate(block.id, newValue)}
blockId={blockId}
activeSourceBlockId={block.activeSourceBlockId}
inputValue={block.value}

View File

@@ -11,6 +11,7 @@ import { cn } from '@/lib/utils'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import type { SubBlockConfig } from '@/blocks/types'
import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions'
import { useTagSelection } from '@/hooks/use-tag-selection'
interface DocumentTagRow {
id: string
@@ -47,6 +48,8 @@ export function DocumentTagEntry({
// Use KB tag definitions hook to get available tags
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
const emitTagSelection = useTagSelection(blockId, subBlock.id)
// State for dropdown visibility - one for each row
const [dropdownStates, setDropdownStates] = useState<Record<number, boolean>>({})
// State for type dropdown visibility - one for each row
@@ -128,6 +131,41 @@ export function DocumentTagEntry({
setStoreValue(jsonString)
}
// Shared helper function for updating rows and generating JSON
const updateRowsAndGenerateJson = (rowIndex: number, column: string, value: string) => {
const updatedRows = [...rows].map((row, idx) => {
if (idx === rowIndex) {
const newCells = { ...row.cells, [column]: value }
// Auto-select type when existing tag is selected
if (column === 'tagName' && value) {
const tagDef = tagDefinitions.find(
(def) => def.displayName.toLowerCase() === value.toLowerCase()
)
if (tagDef) {
newCells.type = tagDef.fieldType
}
}
return {
...row,
cells: newCells,
}
}
return row
})
// Store all rows including empty ones - don't auto-remove
const dataToStore = updatedRows.map((row) => ({
id: row.id,
tagName: row.cells.tagName || '',
fieldType: row.cells.type || 'text',
value: row.cells.value || '',
}))
return dataToStore.length > 0 ? JSON.stringify(dataToStore) : ''
}
const handleCellChange = (rowIndex: number, column: string, value: string) => {
if (isPreview || disabled) return
@@ -155,42 +193,17 @@ export function DocumentTagEntry({
}
}
const updatedRows = [...rows].map((row, idx) => {
if (idx === rowIndex) {
const newCells = { ...row.cells, [column]: value }
// Auto-select type when existing tag is selected
if (column === 'tagName' && value) {
const tagDef = tagDefinitions.find(
(def) => def.displayName.toLowerCase() === value.toLowerCase()
)
if (tagDef) {
newCells.type = tagDef.fieldType
}
}
return {
...row,
cells: newCells,
}
}
return row
})
// No auto-add rows - user will manually add them with plus button
// Store all rows including empty ones - don't auto-remove
const dataToStore = updatedRows.map((row) => ({
id: row.id,
tagName: row.cells.tagName || '',
fieldType: row.cells.type || 'text',
value: row.cells.value || '',
}))
const jsonString = dataToStore.length > 0 ? JSON.stringify(dataToStore) : ''
const jsonString = updateRowsAndGenerateJson(rowIndex, column, value)
setStoreValue(jsonString)
}
const handleTagDropdownSelection = (rowIndex: number, column: string, value: string) => {
if (isPreview || disabled) return
const jsonString = updateRowsAndGenerateJson(rowIndex, column, value)
emitTagSelection(jsonString)
}
const handleAddRow = () => {
if (isPreview || disabled) return
@@ -520,7 +533,8 @@ export function DocumentTagEntry({
<TagDropdown
visible={activeTagDropdown.showTags}
onSelect={(newValue) => {
handleCellChange(activeTagDropdown.rowIndex, 'value', newValue)
// Use immediate emission for tag dropdown selections
handleTagDropdownSelection(activeTagDropdown.rowIndex, 'value', newValue)
setActiveTagDropdown(null)
}}
blockId={blockId}

View File

@@ -9,6 +9,7 @@ import { Label } from '@/components/ui/label'
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import type { SubBlockConfig } from '@/blocks/types'
import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions'
import { useTagSelection } from '@/hooks/use-tag-selection'
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
interface TagFilter {
@@ -44,6 +45,9 @@ export function KnowledgeTagFilters({
}: KnowledgeTagFiltersProps) {
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
// Hook for immediate tag/dropdown selections
const emitTagSelection = useTagSelection(blockId, subBlock.id)
// Get the knowledge base ID from other sub-blocks
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
const knowledgeBaseId = knowledgeBaseIdValue || null
@@ -122,6 +126,30 @@ export function KnowledgeTagFilters({
updateFilters(updatedFilters)
}
const handleTagDropdownSelection = (rowIndex: number, column: string, value: string) => {
if (isPreview || disabled) return
const updatedRows = [...rows].map((row, idx) => {
if (idx === rowIndex) {
return {
...row,
cells: { ...row.cells, [column]: value },
}
}
return row
})
// Convert back to TagFilter format - keep all rows, even empty ones
const updatedFilters = updatedRows.map((row) => ({
id: row.id,
tagName: row.cells.tagName || '',
tagValue: row.cells.value || '',
}))
const jsonValue = updatedFilters.length > 0 ? JSON.stringify(updatedFilters) : null
emitTagSelection(jsonValue)
}
const handleAddRow = () => {
if (isPreview || disabled) return
@@ -336,7 +364,8 @@ export function KnowledgeTagFilters({
<TagDropdown
visible={activeTagDropdown.showTags}
onSelect={(newValue) => {
handleCellChange(activeTagDropdown.rowIndex, 'value', newValue)
// Use immediate emission for tag dropdown selections
handleTagDropdownSelection(activeTagDropdown.rowIndex, 'value', newValue)
setActiveTagDropdown(null)
}}
blockId={blockId}

View File

@@ -12,6 +12,7 @@ import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
import type { SubBlockConfig } from '@/blocks/types'
import { useTagSelection } from '@/hooks/use-tag-selection'
const logger = createLogger('LongInput')
@@ -79,6 +80,8 @@ export function LongInput({
},
})
const emitTagSelection = useTagSelection(blockId, subBlockId)
const [showEnvVars, setShowEnvVars] = useState(false)
const [showTags, setShowTags] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
@@ -428,7 +431,7 @@ export function LongInput({
if (onChange) {
onChange(newValue)
} else if (!isPreview) {
setStoreValue(newValue)
emitTagSelection(newValue)
}
}}
searchTerm={searchTerm}
@@ -445,7 +448,7 @@ export function LongInput({
if (onChange) {
onChange(newValue)
} else if (!isPreview) {
setStoreValue(newValue)
emitTagSelection(newValue)
}
}}
blockId={blockId}

View File

@@ -8,6 +8,7 @@ import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import type { SubBlockConfig } from '@/blocks/types'
import { useTagSelection } from '@/hooks/use-tag-selection'
const logger = createLogger('ShortInput')
@@ -57,6 +58,8 @@ export function ShortInput({
const overlayRef = useRef<HTMLDivElement>(null)
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
const emitTagSelection = useTagSelection(blockId, subBlockId)
// Get ReactFlow instance for zoom control
const reactFlowInstance = useReactFlow()
@@ -288,8 +291,7 @@ export function ShortInput({
if (onChange) {
onChange(newValue)
} else if (!isPreview) {
// Only update store when not in preview mode
setStoreValue(newValue)
emitTagSelection(newValue)
}
}

View File

@@ -723,6 +723,42 @@ export function useCollaborativeWorkflow() {
]
)
// Immediate tag selection (uses queue but processes immediately, no debouncing)
const collaborativeSetTagSelection = useCallback(
(blockId: string, subblockId: string, value: any) => {
if (isApplyingRemoteChange.current) return
if (!currentWorkflowId || activeWorkflowId !== currentWorkflowId) {
logger.debug('Skipping tag selection - not in active workflow', {
currentWorkflowId,
activeWorkflowId,
blockId,
subblockId,
})
return
}
// Apply locally first (immediate UI feedback)
subBlockStore.setValue(blockId, subblockId, value)
// Use the operation queue but with immediate processing (no debouncing)
const operationId = crypto.randomUUID()
addToQueue({
id: operationId,
operation: {
operation: 'subblock-update',
target: 'subblock',
payload: { blockId, subblockId, value },
},
workflowId: activeWorkflowId,
userId: session?.user?.id || 'unknown',
immediate: true,
})
},
[subBlockStore, addToQueue, currentWorkflowId, activeWorkflowId, session?.user?.id]
)
const collaborativeDuplicateBlock = useCallback(
(sourceId: string) => {
const sourceBlock = workflowStore.blocks[sourceId]
@@ -1019,6 +1055,7 @@ export function useCollaborativeWorkflow() {
collaborativeAddEdge,
collaborativeRemoveEdge,
collaborativeSetSubblockValue,
collaborativeSetTagSelection,
// Collaborative loop/parallel operations
collaborativeUpdateLoopCount,

View File

@@ -0,0 +1,20 @@
import { useCallback } from 'react'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
/**
* Hook for handling immediate tag dropdown selections
* Uses the collaborative workflow system but with immediate processing
*/
export function useTagSelection(blockId: string, subblockId: string) {
const { collaborativeSetTagSelection } = useCollaborativeWorkflow()
const emitTagSelectionValue = useCallback(
(value: any) => {
// Use the collaborative system with immediate processing (no debouncing)
collaborativeSetTagSelection(blockId, subblockId, value)
},
[blockId, subblockId, collaborativeSetTagSelection]
)
return emitTagSelectionValue
}

View File

@@ -15,6 +15,7 @@ export interface QueuedOperation {
retryCount: number
status: 'pending' | 'processing' | 'confirmed' | 'failed'
userId: string
immediate?: boolean // Flag for immediate processing (skips debouncing)
}
interface OperationQueueState {
@@ -59,10 +60,11 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
hasOperationError: false,
addToQueue: (operation) => {
// Handle debouncing for subblock operations
// Handle debouncing for regular subblock operations (but not immediate ones like tag selections)
if (
operation.operation.operation === 'subblock-update' &&
operation.operation.target === 'subblock'
operation.operation.target === 'subblock' &&
!operation.immediate
) {
const { blockId, subblockId } = operation.operation.payload
const debounceKey = `${blockId}-${subblockId}`
@@ -100,7 +102,7 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
}))
get().processNextOperation()
}, 100) // 100ms debounce for subblock operations
}, 50) // 50ms debounce for subblock operations - optimized for collaborative editing
subblockDebounceTimeouts.set(debounceKey, timeoutId)
return