diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx index 85c9e08ddf..3d1fb63be3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { Check, ChevronDown, FileText } from 'lucide-react' import { Button } from '@/components/ui/button' import { @@ -13,8 +13,8 @@ import { } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import type { SubBlockConfig } from '@/blocks/types' -import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useSubBlockValue } from '../../hooks/use-sub-block-value' interface DocumentData { id: string @@ -52,38 +52,23 @@ export function DocumentSelector({ previewValue, }: DocumentSelectorProps) { const { getValue } = useSubBlockStore() - const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const [documents, setDocuments] = useState([]) const [error, setError] = useState(null) const [open, setOpen] = useState(false) const [selectedDocument, setSelectedDocument] = useState(null) const [initialFetchDone, setInitialFetchDone] = useState(false) - const [selectedId, setSelectedId] = useState('') - // Get the current value from the store - const storeValue = getValue(blockId, subBlock.id) + // Use the proper hook to get the current value and setter + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) - // Get the knowledge base ID from the same block's knowledgeBaseId subblock - const knowledgeBaseId = getValue(blockId, 'knowledgeBaseId') + // Get the knowledge base ID from the same block's knowledgeBaseId subblock - memoize to prevent re-renders + const knowledgeBaseId = useMemo(() => getValue(blockId, 'knowledgeBaseId'), [getValue, blockId]) // Use preview value when in preview mode, otherwise use store value const value = isPreview ? previewValue : storeValue - // Initialize selectedId with the effective value - useEffect(() => { - if (isPreview && previewValue !== undefined) { - setSelectedId(previewValue || '') - } else { - setSelectedId(value || '') - } - }, [value, isPreview, previewValue]) - // Update local state when external value changes - useEffect(() => { - const currentValue = isPreview ? previewValue : value - setSelectedId(currentValue || '') - }, [value, isPreview, previewValue]) // Fetch documents for the selected knowledge base const fetchDocuments = useCallback(async () => { @@ -112,46 +97,12 @@ export function DocumentSelector({ const fetchedDocuments = result.data || [] setDocuments(fetchedDocuments) setInitialFetchDone(true) - - // Auto-selection logic: if we have a valid selection, keep it - // If there's only one document, select it - // If we have a value but it's not in the documents, reset it - if (selectedId && !fetchedDocuments.some((doc: DocumentData) => doc.id === selectedId)) { - setSelectedId('') - if (!isPreview) { - collaborativeSetSubblockValue(blockId, subBlock.id, '') - } - } - - if ( - (!selectedId || !fetchedDocuments.some((doc: DocumentData) => doc.id === selectedId)) && - fetchedDocuments.length > 0 - ) { - if (fetchedDocuments.length === 1) { - // If only one document, auto-select it - const singleDoc = fetchedDocuments[0] - setSelectedId(singleDoc.id) - setSelectedDocument(singleDoc) - if (!isPreview) { - collaborativeSetSubblockValue(blockId, subBlock.id, singleDoc.id) - } - onDocumentSelect?.(singleDoc.id) - } - } } catch (err) { if ((err as Error).name === 'AbortError') return setError((err as Error).message) setDocuments([]) } - }, [ - knowledgeBaseId, - selectedId, - collaborativeSetSubblockValue, - blockId, - subBlock.id, - isPreview, - onDocumentSelect, - ]) + }, [knowledgeBaseId]) // Handle dropdown open/close - fetch documents when opening const handleOpenChange = (isOpen: boolean) => { @@ -170,50 +121,35 @@ export function DocumentSelector({ if (isPreview) return setSelectedDocument(document) - setSelectedId(document.id) - - if (!isPreview) { - collaborativeSetSubblockValue(blockId, subBlock.id, document.id) - } - + setStoreValue(document.id) onDocumentSelect?.(document.id) setOpen(false) } // Sync selected document with value prop useEffect(() => { - if (selectedId && documents.length > 0) { - const docInfo = documents.find((doc) => doc.id === selectedId) - if (docInfo) { - setSelectedDocument(docInfo) - } else { - setSelectedDocument(null) - } - } else if (!selectedId) { + if (value && documents.length > 0) { + const docInfo = documents.find((doc) => doc.id === value) + setSelectedDocument(docInfo || null) + } else { setSelectedDocument(null) } - }, [selectedId, documents]) + }, [value, documents]) // Reset documents when knowledge base changes useEffect(() => { - if (knowledgeBaseId) { - setDocuments([]) - setSelectedDocument(null) - setSelectedId('') - setInitialFetchDone(false) - setError(null) - if (!isPreview) { - collaborativeSetSubblockValue(blockId, subBlock.id, '') - } - } - }, [knowledgeBaseId, blockId, subBlock.id, collaborativeSetSubblockValue, isPreview]) + setDocuments([]) + setSelectedDocument(null) + setInitialFetchDone(false) + setError(null) + }, [knowledgeBaseId]) // Fetch documents when knowledge base is available and we haven't fetched yet useEffect(() => { if (knowledgeBaseId && !initialFetchDone && !isPreview) { fetchDocuments() } - }, [knowledgeBaseId, initialFetchDone, fetchDocuments, isPreview]) + }, [knowledgeBaseId, initialFetchDone, isPreview]) const formatDocumentName = (document: DocumentData) => { return document.filename @@ -307,7 +243,7 @@ export function DocumentSelector({ - {document.id === selectedId && } + {document.id === value && } ))} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx index 475f7b5e5d..654b4638bf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx @@ -7,6 +7,7 @@ import type { SubBlockConfig } from '@/blocks/types' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useSubBlockValue } from '../../hooks/use-sub-block-value' import type { ConfluenceFileInfo } from './components/confluence-file-selector' import { ConfluenceFileSelector } from './components/confluence-file-selector' import type { DiscordChannelInfo } from './components/discord-channel-selector' @@ -40,6 +41,9 @@ export function FileSelectorInput({ const { getValue } = useSubBlockStore() const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const { activeWorkflowId } = useWorkflowRegistry() + + // Use the proper hook to get the current value and setter + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) const [selectedFileId, setSelectedFileId] = useState('') const [_fileInfo, setFileInfo] = useState(null) const [selectedIssueId, setSelectedIssueId] = useState('') @@ -66,7 +70,7 @@ export function FileSelectorInput({ const serverId = isDiscord ? (getValue(blockId, 'serverId') as string) || '' : '' // Use preview value when in preview mode, otherwise use store value - const value = isPreview ? previewValue : getValue(blockId, subBlock.id) + const value = isPreview ? previewValue : storeValue // Get the current value from the store or prop value if in preview mode useEffect(() => { @@ -117,14 +121,14 @@ export function FileSelectorInput({ const handleFileChange = (fileId: string, info?: any) => { setSelectedFileId(fileId) setFileInfo(info || null) - collaborativeSetSubblockValue(blockId, subBlock.id, fileId) + setStoreValue(fileId) } // Handle issue selection const handleIssueChange = (issueKey: string, info?: JiraIssueInfo) => { setSelectedIssueId(issueKey) setIssueInfo(info || null) - collaborativeSetSubblockValue(blockId, subBlock.id, issueKey) + setStoreValue(issueKey) // Clear the fields when a new issue is selected if (isJira) { @@ -137,14 +141,14 @@ export function FileSelectorInput({ const handleChannelChange = (channelId: string, info?: DiscordChannelInfo) => { setSelectedChannelId(channelId) setChannelInfo(info || null) - collaborativeSetSubblockValue(blockId, subBlock.id, channelId) + setStoreValue(channelId) } // Handle calendar selection const handleCalendarChange = (calendarId: string, info?: GoogleCalendarInfo) => { setSelectedCalendarId(calendarId) setCalendarInfo(info || null) - collaborativeSetSubblockValue(blockId, subBlock.id, calendarId) + setStoreValue(calendarId) } // For Google Drive diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx index e77746e32f..4cc457e08c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx @@ -4,7 +4,6 @@ import { useRef, useState } from 'react' import { X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Progress } from '@/components/ui/progress' -import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useNotificationStore } from '@/stores/notifications/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -58,7 +57,6 @@ export function FileUpload({ // Stores const { addNotification } = useNotificationStore() const { activeWorkflowId } = useWorkflowRegistry() - const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() // Use preview value when in preview mode, otherwise use store value const value = isPreview ? previewValue : storeValue @@ -298,16 +296,10 @@ export function FileUpload({ const newFiles = Array.from(uniqueFiles.values()) setStoreValue(newFiles) - - // Use collaborative update for persistence - collaborativeSetSubblockValue(blockId, subBlockId, newFiles) useWorkflowStore.getState().triggerUpdate() } else { // For single file: Replace with last uploaded file setStoreValue(uploadedFiles[0] || null) - - // Use collaborative update for persistence - collaborativeSetSubblockValue(blockId, subBlockId, uploadedFiles[0] || null) useWorkflowStore.getState().triggerUpdate() } } catch (error) { @@ -363,19 +355,9 @@ export function FileUpload({ const filesArray = Array.isArray(value) ? value : value ? [value] : [] const updatedFiles = filesArray.filter((f) => f.path !== file.path) setStoreValue(updatedFiles.length > 0 ? updatedFiles : null) - - // Use collaborative update for persistence - collaborativeSetSubblockValue( - blockId, - subBlockId, - updatedFiles.length > 0 ? updatedFiles : null - ) } else { // For single file: Clear the value setStoreValue(null) - - // Use collaborative update for persistence - collaborativeSetSubblockValue(blockId, subBlockId, null) } useWorkflowStore.getState().triggerUpdate() @@ -416,7 +398,6 @@ export function FileUpload({ // Clear input state immediately for better UX setStoreValue(null) - collaborativeSetSubblockValue(blockId, subBlockId, null) useWorkflowStore.getState().triggerUpdate() if (fileInputRef.current) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx index 2085c28395..5e723799c1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { Check, ChevronDown, RefreshCw, X } from 'lucide-react' import { PackageSearchIcon } from '@/components/icons' import { Button } from '@/components/ui/button' @@ -14,9 +14,8 @@ import { } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import type { SubBlockConfig } from '@/blocks/types' -import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { type KnowledgeBaseData, useKnowledgeStore } from '@/stores/knowledge/store' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useSubBlockValue } from '../../../sub-block/hooks/use-sub-block-value' interface KnowledgeBaseSelectorProps { blockId: string @@ -37,24 +36,39 @@ export function KnowledgeBaseSelector({ }: KnowledgeBaseSelectorProps) { const { getKnowledgeBasesList, knowledgeBasesList, loadingKnowledgeBasesList } = useKnowledgeStore() - const { getValue } = useSubBlockStore() - const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const [knowledgeBases, setKnowledgeBases] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [open, setOpen] = useState(false) - const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState([]) const [initialFetchDone, setInitialFetchDone] = useState(false) - // Get the current value from the store - const storeValue = getValue(blockId, subBlock.id) + // Use the proper hook to get the current value and setter - this prevents infinite loops + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) // Use preview value when in preview mode, otherwise use store value const value = isPreview ? previewValue : storeValue const isMultiSelect = subBlock.multiSelect === true + // Compute selected knowledge bases directly from value - no local state to avoid loops + const selectedKnowledgeBases = useMemo(() => { + if (value && knowledgeBases.length > 0) { + const selectedIds = + typeof value === 'string' + ? value.includes(',') + ? value + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) + : [value] + : [] + + return knowledgeBases.filter((kb) => selectedIds.includes(kb.id)) + } + return [] + }, [value, knowledgeBases]) + // Fetch knowledge bases const fetchKnowledgeBases = useCallback(async () => { setLoading(true) @@ -89,12 +103,8 @@ export function KnowledgeBaseSelector({ const handleSelectSingleKnowledgeBase = (knowledgeBase: KnowledgeBaseData) => { if (isPreview) return - setSelectedKnowledgeBases([knowledgeBase]) - - if (!isPreview) { - // Use collaborative update for both local store and persistence - collaborativeSetSubblockValue(blockId, subBlock.id, knowledgeBase.id) - } + // Use the hook's setter which handles collaborative updates + setStoreValue(knowledgeBase.id) onKnowledgeBaseSelect?.(knowledgeBase.id) setOpen(false) @@ -115,16 +125,13 @@ export function KnowledgeBaseSelector({ newSelected = [...selectedKnowledgeBases, knowledgeBase] } - setSelectedKnowledgeBases(newSelected) + const selectedIds = newSelected.map((kb) => kb.id) + const valueToStore = selectedIds.length === 1 ? selectedIds[0] : selectedIds.join(',') - if (!isPreview) { - const selectedIds = newSelected.map((kb) => kb.id) - const valueToStore = selectedIds.length === 1 ? selectedIds[0] : selectedIds.join(',') - // Use collaborative update for both local store and persistence - collaborativeSetSubblockValue(blockId, subBlock.id, valueToStore) - } + // Use the hook's setter which handles collaborative updates + setStoreValue(valueToStore) - onKnowledgeBaseSelect?.(newSelected.map((kb) => kb.id)) + onKnowledgeBaseSelect?.(selectedIds) } // Remove selected knowledge base (for multi-select tags) @@ -132,37 +139,16 @@ export function KnowledgeBaseSelector({ if (isPreview) return const newSelected = selectedKnowledgeBases.filter((kb) => kb.id !== knowledgeBaseId) - setSelectedKnowledgeBases(newSelected) + const selectedIds = newSelected.map((kb) => kb.id) + const valueToStore = selectedIds.length === 1 ? selectedIds[0] : selectedIds.join(',') - if (!isPreview) { - const selectedIds = newSelected.map((kb) => kb.id) - const valueToStore = selectedIds.length === 1 ? selectedIds[0] : selectedIds.join(',') - // Use collaborative update for both local store and persistence - collaborativeSetSubblockValue(blockId, subBlock.id, valueToStore) - } + // Use the hook's setter which handles collaborative updates + setStoreValue(valueToStore) - onKnowledgeBaseSelect?.(newSelected.map((kb) => kb.id)) + onKnowledgeBaseSelect?.(selectedIds) } - // Sync selected knowledge bases with value prop - useEffect(() => { - if (value && knowledgeBases.length > 0) { - const selectedIds = - typeof value === 'string' - ? value.includes(',') - ? value - .split(',') - .map((id) => id.trim()) - .filter((id) => id.length > 0) - : [value] - : [] - const selectedKbs = knowledgeBases.filter((kb) => selectedIds.includes(kb.id)) - setSelectedKnowledgeBases(selectedKbs) - } else if (!value) { - setSelectedKnowledgeBases([]) - } - }, [value, knowledgeBases]) // Use cached data if available useEffect(() => { @@ -177,6 +163,7 @@ export function KnowledgeBaseSelector({ if ( value && selectedKnowledgeBases.length === 0 && + knowledgeBases.length === 0 && !loading && !initialFetchDone && !isPreview @@ -186,6 +173,7 @@ export function KnowledgeBaseSelector({ }, [ value, selectedKnowledgeBases.length, + knowledgeBases.length, loading, initialFetchDone, fetchKnowledgeBases, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx index 783720c516..182048bfaf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx @@ -5,6 +5,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp import type { SubBlockConfig } from '@/blocks/types' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useSubBlockValue } from '../../hooks/use-sub-block-value' import { type DiscordServerInfo, DiscordServerSelector } from './components/discord-server-selector' import { type JiraProjectInfo, JiraProjectSelector } from './components/jira-project-selector' import { type LinearProjectInfo, LinearProjectSelector } from './components/linear-project-selector' @@ -32,6 +33,9 @@ export function ProjectSelectorInput({ const [selectedProjectId, setSelectedProjectId] = useState('') const [_projectInfo, setProjectInfo] = useState(null) + // Use the proper hook to get the current value and setter + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) + // Get provider-specific values const provider = subBlock.provider || 'jira' const isDiscord = provider === 'discord' @@ -60,7 +64,7 @@ export function ProjectSelectorInput({ ) => { setSelectedProjectId(projectId) setProjectInfo(info || null) - collaborativeSetSubblockValue(blockId, subBlock.id, projectId) + setStoreValue(projectId) // Clear the issue-related fields when a new project is selected if (provider === 'jira') { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts index 3da6d0b7ea..2811c40b14 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts @@ -4,6 +4,7 @@ import { getProviderFromModel } from '@/providers/utils' import { useGeneralStore } from '@/stores/settings/general/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' // Helper function to dispatch collaborative subblock updates const dispatchSubblockUpdate = (blockId: string, subBlockId: string, value: any) => { @@ -167,6 +168,8 @@ export function useSubBlockValue( subBlockId: string, triggerWorkflowUpdate = false ): readonly [T | null, (value: T) => void] { + const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() + const blockType = useWorkflowStore( useCallback((state) => state.blocks?.[blockId]?.type, [blockId]) ) @@ -228,25 +231,15 @@ export function useSubBlockValue( storeApiKeyValue(blockId, blockType, modelValue, newValue, storeValue) } - // Update the subblock store directly - useSubBlockStore.getState().setValue(blockId, subBlockId, valueCopy) - - // Dispatch event to trigger socket emission only (not store update) - const event = new CustomEvent('update-subblock-value', { - detail: { - blockId, - subBlockId, - value: valueCopy, - }, - }) - window.dispatchEvent(event) + // Use collaborative function which handles both local store update and socket emission + collaborativeSetSubblockValue(blockId, subBlockId, valueCopy) if (triggerWorkflowUpdate) { useWorkflowStore.getState().triggerUpdate() } } }, - [blockId, subBlockId, blockType, isApiKey, storeValue, triggerWorkflowUpdate, modelValue] + [blockId, subBlockId, blockType, isApiKey, storeValue, triggerWorkflowUpdate, modelValue, collaborativeSetSubblockValue] ) // Initialize valueRef on first render