mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
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:
committed by
GitHub
parent
2e2be9bf38
commit
3bd7a6c402
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
20
apps/sim/hooks/use-tag-selection.ts
Normal file
20
apps/sim/hooks/use-tag-selection.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user