From 4fec9f0de1b1d4c715a5c3f4090a80388028d2d5 Mon Sep 17 00:00:00 2001 From: Diego Costa Date: Fri, 24 Oct 2025 13:55:55 -0300 Subject: [PATCH 1/7] fix: resolve credential modal issues in agentflow canvas --- packages/ui/src/hooks/useFlowCredentials.js | 158 ++++++++++++++++++ .../ui/src/utils/flowCredentialsHelper.js | 148 +++++++++++++++- packages/ui/src/views/agentflowsv2/Canvas.jsx | 30 ++++ .../views/agentflowsv2/MarketplaceCanvas.jsx | 27 ++- packages/ui/src/views/canvas/index.jsx | 26 +-- .../views/marketplaces/MarketplaceCanvas.jsx | 27 ++- 6 files changed, 386 insertions(+), 30 deletions(-) create mode 100644 packages/ui/src/hooks/useFlowCredentials.js diff --git a/packages/ui/src/hooks/useFlowCredentials.js b/packages/ui/src/hooks/useFlowCredentials.js new file mode 100644 index 00000000000..dbddaeb4f55 --- /dev/null +++ b/packages/ui/src/hooks/useFlowCredentials.js @@ -0,0 +1,158 @@ +import { useCallback, useRef, useState } from 'react' +import nodesApi from '@/api/nodes' +import { collectFlowCredentials, updateFlowDataWithCredentials } from '@/utils/flowCredentialsHelper' +import { getCredentialModalDismissed, setCredentialModalDismissed } from '@/utils/credentialModalPreference' + +const nodeDefinitionCache = new Map() + +const fetchNodeDefinition = async (nodeName) => { + if (!nodeName) return null + if (nodeDefinitionCache.has(nodeName)) return nodeDefinitionCache.get(nodeName) + try { + const response = await nodesApi.getSpecificNode(nodeName) + const nodeDefinition = response?.data || null + if (nodeDefinition) { + nodeDefinitionCache.set(nodeName, nodeDefinition) + } + return nodeDefinition + } catch (error) { + console.warn(`Failed to fetch node definition for ${nodeName}:`, error) + return null + } +} + +const resetState = (setShowModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) => { + setShowModal(false) + setCredentialList([]) + setAllCredentialsState([]) + setInitialDontShowAgain(false) +} + +const buildCallbackPayload = (flowState, evaluation, mode) => ({ + flowData: flowState?.raw ?? null, + parsedFlow: flowState?.parsed ?? null, + allCredentials: evaluation?.allCredentials ?? [], + missingCredentials: evaluation?.missingCredentials ?? [], + mode +}) + +export const useFlowCredentials = () => { + const [showCredentialModal, setShowCredentialModal] = useState(false) + const [credentialList, setCredentialList] = useState([]) + const [allCredentialsState, setAllCredentialsState] = useState([]) + const [initialDontShowAgain, setInitialDontShowAgain] = useState(false) + + const preferenceScopeRef = useRef(null) + const callbacksRef = useRef({ onAssign: null, onSkip: null, onCancel: null }) + const flowRef = useRef({ raw: null, parsed: null, format: 'object' }) + const evaluationRef = useRef({ allCredentials: [], missingCredentials: [] }) + const modeRef = useRef('missing') + + const openCredentialModal = useCallback(async (flowData, options = {}) => { + const { preferenceScope = null, onAssign, onSkip, onCancel, forceShow = false, mode = 'missing' } = options + + const evaluation = await collectFlowCredentials(flowData, { fetchNodeDefinition }) + evaluationRef.current = evaluation + + const listForModal = mode === 'all' ? evaluation.allCredentials : evaluation.missingCredentials + const hasItems = listForModal.length > 0 + const preferenceDismissed = preferenceScope ? getCredentialModalDismissed(preferenceScope) : false + + if (!forceShow && (!hasItems || preferenceDismissed)) { + return { shown: false, evaluation } + } + + const isStringInput = typeof flowData === 'string' + const parsedFlow = isStringInput ? JSON.parse(flowData) : { ...flowData } + + flowRef.current = { + raw: isStringInput ? flowData : parsedFlow, + parsed: parsedFlow, + format: isStringInput ? 'string' : 'object' + } + + callbacksRef.current = { onAssign, onSkip, onCancel } + preferenceScopeRef.current = preferenceScope + modeRef.current = mode + + setCredentialList(listForModal) + setAllCredentialsState(evaluation.allCredentials) + setInitialDontShowAgain(preferenceScope ? preferenceDismissed : false) + setShowCredentialModal(true) + + return { shown: true, evaluation } + }, []) + + const applyPreference = (options) => { + const scope = preferenceScopeRef.current + if (!scope) return + if (options?.dontShowDirty) { + setCredentialModalDismissed(scope, !!options.dontShowAgain) + } + } + + const handleAssign = useCallback((credentialAssignments, options = {}) => { + applyPreference(options) + + const currentFlow = flowRef.current + const format = currentFlow.format + const source = format === 'string' ? currentFlow.raw : currentFlow.parsed + + let nextRaw = currentFlow.raw + let nextParsed = currentFlow.parsed + + if (credentialAssignments && Object.keys(credentialAssignments).length > 0) { + const updatedFlow = updateFlowDataWithCredentials(source, credentialAssignments) + nextParsed = updatedFlow + nextRaw = format === 'string' ? JSON.stringify(updatedFlow) : updatedFlow + } + + flowRef.current = { + raw: nextRaw, + parsed: nextParsed, + format + } + + callbacksRef.current.onAssign?.( + nextRaw, + credentialAssignments, + buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current) + ) + + resetState(setShowCredentialModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) + preferenceScopeRef.current = null + callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } + }, []) + + const handleSkip = useCallback((options = {}) => { + applyPreference(options) + callbacksRef.current.onSkip?.(flowRef.current.raw, buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current)) + + resetState(setShowCredentialModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) + preferenceScopeRef.current = null + callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } + }, []) + + const handleCancel = useCallback((options = {}) => { + applyPreference(options) + callbacksRef.current.onCancel?.(flowRef.current.raw, buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current)) + + resetState(setShowCredentialModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) + preferenceScopeRef.current = null + callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } + }, []) + + return { + showCredentialModal, + missingCredentials: credentialList, + allCredentials: allCredentialsState, + initialDontShowAgain, + modalMode: modeRef.current, + openCredentialModal, + handleAssign, + handleSkip, + handleCancel + } +} + +export default useFlowCredentials diff --git a/packages/ui/src/utils/flowCredentialsHelper.js b/packages/ui/src/utils/flowCredentialsHelper.js index 4078a39f3ad..f9036c05f0d 100644 --- a/packages/ui/src/utils/flowCredentialsHelper.js +++ b/packages/ui/src/utils/flowCredentialsHelper.js @@ -212,6 +212,41 @@ export const groupAllCredentialsByType = (allCredentials) => { * @param {object} credentialAssignments - Object mapping nodeId to credentialId * @returns {object} Updated flow data */ +const CONFIG_COMPONENT_KEYS = [ + 'llmModel', + 'llmModelName', + 'agentModel', + 'conditionAgentModel', + 'retrieverModel', + 'model', + 'modelName', + 'chatModel', + 'assistantModel', + 'selectedAssistant', + 'toolModel', + 'selectedTool', + 'agentFlowModel' +] + +const buildConfigEntryKey = (nodeId, parameterName, credentialType) => `${nodeId}:${parameterName}:${credentialType}` + +const resolveConfigComponentName = (config, inputs, parameterName) => { + if (!config || typeof config !== 'object') return '' + for (const key of CONFIG_COMPONENT_KEYS) { + if (typeof config[key] === 'string' && config[key]) return config[key] + } + const matchingInputKey = parameterName.replace(/Config$/, '') + if (matchingInputKey && typeof inputs?.[matchingInputKey] === 'string') { + return inputs[matchingInputKey] + } + for (const value of Object.values(config)) { + if (typeof value === 'string' && value) { + return value + } + } + return '' +} + export const updateFlowDataWithCredentials = (flowData, credentialAssignments) => { try { const flow = typeof flowData === 'string' ? JSON.parse(flowData) : { ...flowData } @@ -225,15 +260,26 @@ export const updateFlowDataWithCredentials = (flowData, credentialAssignments) = if (credentialAssignments[node.id]) { const updatedNode = { ...node } updatedNode.data = { ...node.data } + const assignedCredential = credentialAssignments[node.id] // Set the credential - updatedNode.data.credential = credentialAssignments[node.id] + updatedNode.data.credential = assignedCredential // Also set in inputs for compatibility if (!updatedNode.data.inputs) { updatedNode.data.inputs = {} } - updatedNode.data.inputs['FLOWISE_CREDENTIAL_ID'] = credentialAssignments[node.id] + updatedNode.data.inputs['FLOWISE_CREDENTIAL_ID'] = assignedCredential + + // Update nested config objects that track credential assignments + Object.entries(updatedNode.data.inputs).forEach(([inputKey, inputValue]) => { + if (!inputValue || typeof inputValue !== 'object') return + if (!Object.prototype.hasOwnProperty.call(inputValue, 'credential')) return + updatedNode.data.inputs[inputKey] = { + ...inputValue, + credential: assignedCredential + } + }) return updatedNode } @@ -247,6 +293,104 @@ export const updateFlowDataWithCredentials = (flowData, credentialAssignments) = } } +export const collectFlowCredentials = async (flowData, options = {}) => { + const { fetchNodeDefinition } = options + const flow = typeof flowData === 'string' ? JSON.parse(flowData) : { ...flowData } + + if (!flow.nodes || !Array.isArray(flow.nodes)) { + return { + allCredentials: [], + missingCredentials: [], + hasCredentials: false, + hasMissingCredentials: false + } + } + + const baseMissing = extractMissingCredentials(flow).missingCredentials || [] + const baseAll = extractAllCredentials(flow).allCredentials || [] + + const allCredentials = [...baseAll] + const missingCredentials = [...baseMissing] + + const seenAllKeys = new Set(allCredentials.map((cred) => buildConfigEntryKey(cred.nodeId, cred.parameterName, cred.credentialType))) + const seenMissingKeys = new Set( + missingCredentials.map((cred) => buildConfigEntryKey(cred.nodeId, cred.parameterName, cred.credentialType)) + ) + + if (typeof fetchNodeDefinition === 'function') { + for (const node of flow.nodes) { + const nodeData = node.data || {} + const inputs = nodeData.inputs || {} + + for (const [inputKey, inputValue] of Object.entries(inputs)) { + if (!inputKey.endsWith('Config')) continue + if (!inputValue || typeof inputValue !== 'object') continue + if (!Object.prototype.hasOwnProperty.call(inputValue, 'credential')) continue + + const componentName = resolveConfigComponentName(inputValue, inputs, inputKey) + + let nodeDefinition = null + let credentialNames = [] + + if (componentName) { + try { + nodeDefinition = await fetchNodeDefinition(componentName) + const credentialObj = nodeDefinition?.credential + if (credentialObj?.credentialNames && Array.isArray(credentialObj.credentialNames)) { + credentialNames = credentialObj.credentialNames + } + } catch (error) { + console.warn(`Failed to load node definition for ${componentName}:`, error) + } + } + + if (!credentialNames.length) { + credentialNames = componentName ? [componentName] : [inputKey] + } + + const label = + nodeDefinition?.label || + (componentName ? toSentenceCase(componentName) : toSentenceCase(inputKey.replace(/Config$/, 'Credential'))) + + credentialNames.forEach((credentialName) => { + const entryKey = buildConfigEntryKey(node.id, inputKey, credentialName) + const isAssigned = !!inputValue.credential + const entry = { + nodeId: node.id, + nodeName: nodeData.name || 'Unknown Node', + nodeCategory: nodeData.category || 'Unknown', + nodeType: nodeData.type || node.type || 'Unknown', + credentialType: credentialName, + parameterName: inputKey, + label, + isOptional: false, + isRequired: true, + isAssigned, + assignedCredentialId: isAssigned ? inputValue.credential : null + } + + if (!seenAllKeys.has(entryKey)) { + seenAllKeys.add(entryKey) + allCredentials.push(entry) + } + + if (!isAssigned && !seenMissingKeys.has(entryKey)) { + seenMissingKeys.add(entryKey) + missingCredentials.push(entry) + } + }) + } + } + } + + return { + allCredentials, + missingCredentials, + hasCredentials: allCredentials.length > 0, + hasMissingCredentials: missingCredentials.length > 0 + } +} + /** * Check if a credential assignment is valid for a node * @param {object} node - Flow node diff --git a/packages/ui/src/views/agentflowsv2/Canvas.jsx b/packages/ui/src/views/agentflowsv2/Canvas.jsx index 3203dbe0316..c0b18c0774f 100644 --- a/packages/ui/src/views/agentflowsv2/Canvas.jsx +++ b/packages/ui/src/views/agentflowsv2/Canvas.jsx @@ -32,6 +32,8 @@ import EditNodeDialog from '@/views/agentflowsv2/EditNodeDialog' import ChatPopUp from '@/views/chatmessage/ChatPopUp' import ValidationPopUp from '@/views/chatmessage/ValidationPopUp' import { flowContext } from '@/store/context/ReactFlowContext' +import useFlowCredentials from '@/hooks/useFlowCredentials' +import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' // API import nodesApi from '@/api/nodes' @@ -101,6 +103,10 @@ const AgentflowCanvas = () => { const [editNodeDialogOpen, setEditNodeDialogOpen] = useState(false) const [editNodeDialogProps, setEditNodeDialogProps] = useState({}) + const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = + useFlowCredentials() + const hasPromptedCredentialsRef = useRef(false) + const reactFlowWrapper = useRef(null) // ==============================|| Chatflow API ||============================== // @@ -163,6 +169,10 @@ const AgentflowCanvas = () => { setNodes(nodes) setEdges(flowData.edges || []) setTimeout(() => setDirty(), 0) + + hasPromptedCredentialsRef.current = true + const preferenceScope = chatflowId ? `flow:${chatflowId}` : null + openCredentialModal(flowData, { preferenceScope }).catch((error) => console.error('Failed to open credential modal:', error)) } catch (e) { console.error(e) } @@ -572,8 +582,19 @@ const AgentflowCanvas = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasDataStore.chatflow]) + useEffect(() => { + if (canvasDataStore.chatflow?.flowData && !hasPromptedCredentialsRef.current) { + hasPromptedCredentialsRef.current = true + const preferenceScope = canvasDataStore.chatflow?.id ? `flow:${canvasDataStore.chatflow.id}` : null + openCredentialModal(canvasDataStore.chatflow.flowData, { preferenceScope }).catch((error) => + console.error('Failed to open credential modal:', error) + ) + } + }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) + // Initialization useEffect(() => { + hasPromptedCredentialsRef.current = false setIsSyncNodesButtonEnabled(false) if (chatflowId) { getSpecificChatflowApi.request(chatflowId) @@ -779,6 +800,15 @@ const AgentflowCanvas = () => { + + ) } diff --git a/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx b/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx index f3eb529676f..219d1b14b60 100644 --- a/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx +++ b/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx @@ -17,6 +17,8 @@ import MarketplaceCanvasHeader from '@/views/marketplaces/MarketplaceCanvasHeade import StickyNote from './StickyNote' import EditNodeDialog from '@/views/agentflowsv2/EditNodeDialog' import { flowContext } from '@/store/context/ReactFlowContext' +import useFlowCredentials from '@/hooks/useFlowCredentials' +import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' const nodeTypes = { agentFlow: AgentFlowNode, stickyNote: StickyNote, iteration: IterationNode } const edgeTypes = { agentFlow: AgentFlowEdge } @@ -40,6 +42,10 @@ const MarketplaceCanvasV2 = () => { const reactFlowWrapper = useRef(null) const { setReactFlowInstance } = useContext(flowContext) + const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = + useFlowCredentials() + const hasPromptedCredentialsRef = useRef(false) + // ==============================|| useEffect ||============================== // useEffect(() => { @@ -47,10 +53,16 @@ const MarketplaceCanvasV2 = () => { const initialFlow = JSON.parse(flowData) setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [flowData]) + if (!hasPromptedCredentialsRef.current) { + hasPromptedCredentialsRef.current = true + const preferenceScope = state?.id ? `template:${state.id}` : null + openCredentialModal(flowData, { preferenceScope }).catch((error) => + console.error('Failed to open credential modal:', error) + ) + } + } + }, [flowData, openCredentialModal, state]) const onChatflowCopy = (stateData) => { // stateData is now the complete state with all template information @@ -125,6 +137,15 @@ const MarketplaceCanvasV2 = () => { + + ) } diff --git a/packages/ui/src/views/canvas/index.jsx b/packages/ui/src/views/canvas/index.jsx index 12c377559f3..b4d30527b75 100644 --- a/packages/ui/src/views/canvas/index.jsx +++ b/packages/ui/src/views/canvas/index.jsx @@ -58,7 +58,7 @@ import { usePrompt } from '@/utils/usePrompt' import { FLOWISE_CREDENTIAL_ID } from '@/store/constant' // credential checking -import { useCredentialChecker } from '@/hooks/useCredentialChecker' +import useFlowCredentials from '@/hooks/useFlowCredentials' import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' const nodeTypes = { customNode: CanvasNode, stickyNote: StickyNote } const edgeTypes = { buttonedge: ButtonEdge } @@ -110,15 +110,9 @@ const Canvas = ({ chatflowid: chatflowId }) => { const reactFlowWrapper = useRef(null) const canvasHeaderRef = useRef(null) - const { - showCredentialModal, - missingCredentials, - checkCredentials, - initialDontShowAgain, - handleAssign, - handleSkip, - handleCancel - } = useCredentialChecker() + const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = + useFlowCredentials() + const hasPromptedCredentialsRef = useRef(false) // ==============================|| Chatflow API ||============================== // @@ -615,10 +609,21 @@ const Canvas = ({ chatflowid: chatflowId }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasDataStore.chatflow]) + useEffect(() => { + if (canvasDataStore.chatflow?.flowData && !hasPromptedCredentialsRef.current) { + hasPromptedCredentialsRef.current = true + const preferenceScope = canvasDataStore.chatflow?.id ? `flow:${canvasDataStore.chatflow.id}` : null + openCredentialModal(canvasDataStore.chatflow.flowData, { preferenceScope }).catch((error) => + console.error('Failed to open credential modal:', error) + ) + } + }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) + // Initialization useEffect(() => { // console.log('🎨 Canvas initialization effect running:', { chatflowId }) + hasPromptedCredentialsRef.current = false setIsSyncNodesButtonEnabled(false) setIsUpsertButtonEnabled(false) if (chatflowId) { @@ -819,7 +824,6 @@ const Canvas = ({ chatflowid: chatflowId }) => { onAssign={handleAssign} onSkip={handleSkip} onCancel={handleCancel} - flowData={null} initialDontShowAgain={initialDontShowAgain} /> diff --git a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx index 0ec1b4562de..270dd83128c 100644 --- a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx +++ b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx @@ -16,7 +16,7 @@ import StickyNote from '../canvas/StickyNote' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' // credential checking -import { useCredentialChecker } from '@/hooks/useCredentialChecker' +import useFlowCredentials from '@/hooks/useFlowCredentials' import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' const nodeTypes = { customNode: MarketplaceCanvasNode, stickyNote: StickyNote } @@ -39,15 +39,9 @@ const MarketplaceCanvas = () => { const reactFlowWrapper = useRef(null) // Credential checking hook - const { - showCredentialModal, - missingCredentials, - checkCredentials, - initialDontShowAgain, - handleAssign, - handleSkip, - handleCancel - } = useCredentialChecker() + const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = + useFlowCredentials() + const hasPromptedCredentialsRef = useRef(false) // ==============================|| useEffect ||============================== // @@ -56,10 +50,16 @@ const MarketplaceCanvas = () => { const initialFlow = JSON.parse(flowData) setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [flowData]) + if (!hasPromptedCredentialsRef.current) { + hasPromptedCredentialsRef.current = true + const preferenceScope = state?.id ? `template:${state.id}` : null + openCredentialModal(flowData, { preferenceScope }).catch((error) => + console.error('Failed to open credential modal:', error) + ) + } + } + }, [flowData, openCredentialModal, state]) const onChatflowCopy = (stateData) => { // stateData is now the complete state with all template information @@ -146,7 +146,6 @@ const MarketplaceCanvas = () => { onAssign={handleAssign} onSkip={handleSkip} onCancel={handleCancel} - flowData={flowData ? JSON.parse(flowData) : null} initialDontShowAgain={initialDontShowAgain} /> From f762299cc5c84639c200b569cb4068a62d7d6875 Mon Sep 17 00:00:00 2001 From: Diego Costa Date: Tue, 28 Oct 2025 13:02:12 -0300 Subject: [PATCH 2/7] refactor: enhance credential modal handling in agentflow and marketplace canvases - Updated credential modal logic to ensure it only triggers after user actions, improving user experience. - Removed unnecessary credential modal calls during initial flow loading. - Implemented automatic credential modal opening based on flow data and QuickSetup parameter. - Cleaned up code by removing unused credential modal components from MarketplaceCanvas. - Ensured proper state management for credential prompts across different user interactions. This refactor addresses previous issues with credential modal visibility and enhances the overall flow of the application. --- packages/ui/src/views/agentflowsv2/Canvas.jsx | 78 ++++++++++++++++--- .../views/agentflowsv2/MarketplaceCanvas.jsx | 25 +----- packages/ui/src/views/canvas/index.jsx | 77 ++++++++++++++++-- .../views/marketplaces/MarketplaceCanvas.jsx | 29 +------ 4 files changed, 140 insertions(+), 69 deletions(-) diff --git a/packages/ui/src/views/agentflowsv2/Canvas.jsx b/packages/ui/src/views/agentflowsv2/Canvas.jsx index c0b18c0774f..e09e5ab40d3 100644 --- a/packages/ui/src/views/agentflowsv2/Canvas.jsx +++ b/packages/ui/src/views/agentflowsv2/Canvas.jsx @@ -170,9 +170,8 @@ const AgentflowCanvas = () => { setEdges(flowData.edges || []) setTimeout(() => setDirty(), 0) - hasPromptedCredentialsRef.current = true - const preferenceScope = chatflowId ? `flow:${chatflowId}` : null - openCredentialModal(flowData, { preferenceScope }).catch((error) => console.error('Failed to open credential modal:', error)) + // Credential modal will be triggered automatically after save via useEffect + // watching canvasDataStore.chatflow.flowData (lines 585-593) } catch (e) { console.error(e) } @@ -538,6 +537,10 @@ const AgentflowCanvas = () => { const initialFlow = chatflow.flowData ? JSON.parse(chatflow.flowData) : [] setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) + // Allow modal to check for missing credentials when loading existing flow + // Modal will show immediately if credentials are missing (unless user dismissed it) + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) } else if (getSpecificChatflowApi.error) { errorFailed(`Failed to retrieve ${canvasTitle}: ${getSpecificChatflowApi.error.response.data.message}`) @@ -550,6 +553,9 @@ const AgentflowCanvas = () => { useEffect(() => { if (createNewChatflowApi.data) { const chatflow = createNewChatflowApi.data + // Reset to allow modal to show after first save + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) saveChatflowSuccess() window.history.replaceState(state, null, `/v2/agentcanvas/${chatflow.id}`) @@ -563,6 +569,9 @@ const AgentflowCanvas = () => { // Update chatflow successful useEffect(() => { if (updateChatflowApi.data) { + // Reset to allow modal to check for missing credentials after update + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data }) saveChatflowSuccess() } else if (updateChatflowApi.error) { @@ -583,18 +592,57 @@ const AgentflowCanvas = () => { }, [canvasDataStore.chatflow]) useEffect(() => { - if (canvasDataStore.chatflow?.flowData && !hasPromptedCredentialsRef.current) { - hasPromptedCredentialsRef.current = true - const preferenceScope = canvasDataStore.chatflow?.id ? `flow:${canvasDataStore.chatflow.id}` : null - openCredentialModal(canvasDataStore.chatflow.flowData, { preferenceScope }).catch((error) => - console.error('Failed to open credential modal:', error) - ) + const flowId = canvasDataStore.chatflow?.id + const flowData = canvasDataStore.chatflow?.flowData + + if (!flowId || !flowData || hasPromptedCredentialsRef.current) { + return + } + + hasPromptedCredentialsRef.current = true + const preferenceScope = `flow:${flowId}` + openCredentialModal(flowData, { preferenceScope }).catch((error) => console.error('Failed to open credential modal:', error)) + }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) + + // Listen for QuickSetup parameter to open credential modal in "all" mode + useEffect(() => { + const handleQuickSetup = () => { + const searchParams = new URLSearchParams(window.location.search) + const isQuickSetup = searchParams.get('QuickSetup') === 'true' + + if (isQuickSetup && canvasDataStore.chatflow?.flowData) { + const flowId = canvasDataStore.chatflow.id + const preferenceScope = flowId ? `flow:${flowId}` : null + + // Open modal with all credentials (mode: 'all') and force show + openCredentialModal(canvasDataStore.chatflow.flowData, { + preferenceScope, + mode: 'all', + forceShow: true + }).catch((error) => console.error('Failed to open credential modal:', error)) + + // Clean up URL parameter + searchParams.delete('QuickSetup') + const newUrl = `${window.location.pathname}${searchParams.toString() ? '?' + searchParams.toString() : ''}` + window.history.replaceState({}, '', newUrl) + } + } + + // Check immediately on mount + handleQuickSetup() + + // Listen for popstate events (when button triggers history change) + window.addEventListener('popstate', handleQuickSetup) + + return () => { + window.removeEventListener('popstate', handleQuickSetup) } }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) // Initialization useEffect(() => { - hasPromptedCredentialsRef.current = false + // DO NOT reset hasPromptedCredentialsRef here - it prevents duplicate modals + // The ref is managed by the credential modal useEffect and save handlers setIsSyncNodesButtonEnabled(false) if (chatflowId) { getSpecificChatflowApi.request(chatflowId) @@ -606,6 +654,10 @@ const AgentflowCanvas = () => { setNodes([]) setEdges([]) } + // Block credential modal until user saves + // Modal should only show AFTER user clicks save + // MUST set this BEFORE dispatch to prevent modal trigger + hasPromptedCredentialsRef.current = true dispatch({ type: SET_CHATFLOW, chatflow: { @@ -634,6 +686,9 @@ const AgentflowCanvas = () => { //TODO: prevent paste event when input focused, temporary fix: catch chatflow syntax if (pasteData.includes('{"nodes":[') && pasteData.includes('],"edges":[')) { handleLoadFlow(pasteData) + // Block credential modal until user saves + // Modal should only show AFTER user clicks save + hasPromptedCredentialsRef.current = true } } @@ -649,6 +704,9 @@ const AgentflowCanvas = () => { useEffect(() => { if (templateFlowData && templateFlowData.includes('"nodes":[') && templateFlowData.includes('],"edges":[')) { handleLoadFlow(templateFlowData) + // Block credential modal until user saves + // Modal should only show AFTER user clicks save + hasPromptedCredentialsRef.current = true } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx b/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx index 219d1b14b60..64a015f12d2 100644 --- a/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx +++ b/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx @@ -17,8 +17,6 @@ import MarketplaceCanvasHeader from '@/views/marketplaces/MarketplaceCanvasHeade import StickyNote from './StickyNote' import EditNodeDialog from '@/views/agentflowsv2/EditNodeDialog' import { flowContext } from '@/store/context/ReactFlowContext' -import useFlowCredentials from '@/hooks/useFlowCredentials' -import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' const nodeTypes = { agentFlow: AgentFlowNode, stickyNote: StickyNote, iteration: IterationNode } const edgeTypes = { agentFlow: AgentFlowEdge } @@ -42,10 +40,6 @@ const MarketplaceCanvasV2 = () => { const reactFlowWrapper = useRef(null) const { setReactFlowInstance } = useContext(flowContext) - const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = - useFlowCredentials() - const hasPromptedCredentialsRef = useRef(false) - // ==============================|| useEffect ||============================== // useEffect(() => { @@ -53,16 +47,8 @@ const MarketplaceCanvasV2 = () => { const initialFlow = JSON.parse(flowData) setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) - - if (!hasPromptedCredentialsRef.current) { - hasPromptedCredentialsRef.current = true - const preferenceScope = state?.id ? `template:${state.id}` : null - openCredentialModal(flowData, { preferenceScope }).catch((error) => - console.error('Failed to open credential modal:', error) - ) - } } - }, [flowData, openCredentialModal, state]) + }, [flowData]) const onChatflowCopy = (stateData) => { // stateData is now the complete state with all template information @@ -137,15 +123,6 @@ const MarketplaceCanvasV2 = () => { - - ) } diff --git a/packages/ui/src/views/canvas/index.jsx b/packages/ui/src/views/canvas/index.jsx index b4d30527b75..820b72027a2 100644 --- a/packages/ui/src/views/canvas/index.jsx +++ b/packages/ui/src/views/canvas/index.jsx @@ -227,6 +227,11 @@ const Canvas = ({ chatflowid: chatflowId }) => { flowData: JSON.stringify({ nodes, edges }) } + // Block credential modal from showing until user saves + // Modal should only show AFTER user clicks save + // MUST set this BEFORE dispatch to prevent modal trigger + hasPromptedCredentialsRef.current = true + dispatch({ type: SET_CHATFLOW, chatflow: newChatflow }) setChatflow(newChatflow) setNodes(nodes) @@ -554,6 +559,10 @@ const Canvas = ({ chatflowid: chatflowId }) => { const initialFlow = chatflow.flowData ? JSON.parse(chatflow.flowData) : [] setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) + // Allow modal to check for missing credentials when loading existing flow + // Modal will show immediately if credentials are missing (unless user dismissed it) + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) } else if (getSpecificChatflowApi?.error?.response?.data?.message) { errorFailed(`Failed to save ${canvasTitle}: ${getSpecificChatflowApi.error.response.data.message}`) @@ -569,6 +578,9 @@ const Canvas = ({ chatflowid: chatflowId }) => { if (createNewChatflowApi?.data) { const chatflow = createNewChatflowApi.data // console.log('✅ Create new chatflow successful, navigating to:', `/${isAgentCanvas ? 'agentcanvas' : 'canvas'}/${chatflow.id}`) + // Reset to allow modal to show after first save + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) saveChatflowSuccess() navigate(`/${isAgentCanvas ? 'agentcanvas' : 'canvas'}/${chatflow.id}`, { @@ -587,6 +599,9 @@ const Canvas = ({ chatflowid: chatflowId }) => { useEffect(() => { if (updateChatflowApi?.data) { // console.log('✅ Update chatflow successful, NOT navigating (should we?)') + // Reset to allow modal to check for missing credentials after update + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data }) saveChatflowSuccess() } else if (updateChatflowApi?.error?.response?.data?.message) { @@ -610,12 +625,50 @@ const Canvas = ({ chatflowid: chatflowId }) => { }, [canvasDataStore.chatflow]) useEffect(() => { - if (canvasDataStore.chatflow?.flowData && !hasPromptedCredentialsRef.current) { - hasPromptedCredentialsRef.current = true - const preferenceScope = canvasDataStore.chatflow?.id ? `flow:${canvasDataStore.chatflow.id}` : null - openCredentialModal(canvasDataStore.chatflow.flowData, { preferenceScope }).catch((error) => - console.error('Failed to open credential modal:', error) - ) + const flowId = canvasDataStore.chatflow?.id + const flowData = canvasDataStore.chatflow?.flowData + + if (!flowId || !flowData || hasPromptedCredentialsRef.current) { + return + } + + hasPromptedCredentialsRef.current = true + const preferenceScope = `flow:${flowId}` + openCredentialModal(flowData, { preferenceScope }).catch((error) => console.error('Failed to open credential modal:', error)) + }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) + + // Listen for QuickSetup parameter to open credential modal in "all" mode + useEffect(() => { + const handleQuickSetup = () => { + const searchParams = new URLSearchParams(window.location.search) + const isQuickSetup = searchParams.get('QuickSetup') === 'true' + + if (isQuickSetup && canvasDataStore.chatflow?.flowData) { + const flowId = canvasDataStore.chatflow.id + const preferenceScope = flowId ? `flow:${flowId}` : null + + // Open modal with all credentials (mode: 'all') and force show + openCredentialModal(canvasDataStore.chatflow.flowData, { + preferenceScope, + mode: 'all', + forceShow: true + }).catch((error) => console.error('Failed to open credential modal:', error)) + + // Clean up URL parameter + searchParams.delete('QuickSetup') + const newUrl = `${window.location.pathname}${searchParams.toString() ? '?' + searchParams.toString() : ''}` + window.history.replaceState({}, '', newUrl) + } + } + + // Check immediately on mount + handleQuickSetup() + + // Listen for popstate events (when button triggers history change) + window.addEventListener('popstate', handleQuickSetup) + + return () => { + window.removeEventListener('popstate', handleQuickSetup) } }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) @@ -623,7 +676,8 @@ const Canvas = ({ chatflowid: chatflowId }) => { useEffect(() => { // console.log('🎨 Canvas initialization effect running:', { chatflowId }) - hasPromptedCredentialsRef.current = false + // DO NOT reset hasPromptedCredentialsRef here - it prevents duplicate modals + // The ref is managed by the credential modal useEffect and save handlers setIsSyncNodesButtonEnabled(false) setIsUpsertButtonEnabled(false) if (chatflowId) { @@ -653,6 +707,12 @@ const Canvas = ({ chatflowid: chatflowId }) => { deployed: false, isPublic: false } + + // Block credential modal from showing until user saves + // Modal should only show AFTER user clicks save + // MUST set this BEFORE dispatch to prevent modal trigger + hasPromptedCredentialsRef.current = true + setChatflow(newChatflow) dispatch({ type: SET_CHATFLOW, chatflow: newChatflow }) @@ -669,6 +729,9 @@ const Canvas = ({ chatflowid: chatflowId }) => { // console.log('🎨 No duplicated flow data, creating blank canvas') setNodes([]) setEdges([]) + // Block credential modal for blank canvas until user saves + // MUST set this BEFORE dispatch to prevent modal trigger + hasPromptedCredentialsRef.current = true setChatflow({ name: templateName ? `Copy of ${templateName}` : `Untitled ${canvasTitle}` }) diff --git a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx index 270dd83128c..92595ddf550 100644 --- a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx +++ b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx @@ -15,10 +15,6 @@ import MarketplaceCanvasHeader from './MarketplaceCanvasHeader' import StickyNote from '../canvas/StickyNote' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' -// credential checking -import useFlowCredentials from '@/hooks/useFlowCredentials' -import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' - const nodeTypes = { customNode: MarketplaceCanvasNode, stickyNote: StickyNote } const edgeTypes = { buttonedge: '' } @@ -38,11 +34,6 @@ const MarketplaceCanvas = () => { const reactFlowWrapper = useRef(null) - // Credential checking hook - const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = - useFlowCredentials() - const hasPromptedCredentialsRef = useRef(false) - // ==============================|| useEffect ||============================== // useEffect(() => { @@ -50,16 +41,8 @@ const MarketplaceCanvas = () => { const initialFlow = JSON.parse(flowData) setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) - - if (!hasPromptedCredentialsRef.current) { - hasPromptedCredentialsRef.current = true - const preferenceScope = state?.id ? `template:${state.id}` : null - openCredentialModal(flowData, { preferenceScope }).catch((error) => - console.error('Failed to open credential modal:', error) - ) - } } - }, [flowData, openCredentialModal, state]) + }, [flowData]) const onChatflowCopy = (stateData) => { // stateData is now the complete state with all template information @@ -138,16 +121,6 @@ const MarketplaceCanvas = () => { {/* Confirm Dialog */} - - {/* Unified Credentials Modal */} - ) } From fc6db1d8ba719d0a151ac1b5dedcb64deb86a50f Mon Sep 17 00:00:00 2001 From: Diego Costa Date: Tue, 28 Oct 2025 13:15:53 -0300 Subject: [PATCH 3/7] refactor: consolidate credential modal logic and update billing providers --- packages/ui/src/hooks/useFlowCredentials.js | 158 ----------------- .../ui/src/utils/flowCredentialsHelper.js | 160 +----------------- packages/ui/src/views/agentflowsv2/Canvas.jsx | 88 ---------- .../views/agentflowsv2/MarketplaceCanvas.jsx | 2 + packages/ui/src/views/canvas/index.jsx | 82 +-------- .../views/marketplaces/MarketplaceCanvas.jsx | 21 +++ 6 files changed, 35 insertions(+), 476 deletions(-) delete mode 100644 packages/ui/src/hooks/useFlowCredentials.js diff --git a/packages/ui/src/hooks/useFlowCredentials.js b/packages/ui/src/hooks/useFlowCredentials.js deleted file mode 100644 index dbddaeb4f55..00000000000 --- a/packages/ui/src/hooks/useFlowCredentials.js +++ /dev/null @@ -1,158 +0,0 @@ -import { useCallback, useRef, useState } from 'react' -import nodesApi from '@/api/nodes' -import { collectFlowCredentials, updateFlowDataWithCredentials } from '@/utils/flowCredentialsHelper' -import { getCredentialModalDismissed, setCredentialModalDismissed } from '@/utils/credentialModalPreference' - -const nodeDefinitionCache = new Map() - -const fetchNodeDefinition = async (nodeName) => { - if (!nodeName) return null - if (nodeDefinitionCache.has(nodeName)) return nodeDefinitionCache.get(nodeName) - try { - const response = await nodesApi.getSpecificNode(nodeName) - const nodeDefinition = response?.data || null - if (nodeDefinition) { - nodeDefinitionCache.set(nodeName, nodeDefinition) - } - return nodeDefinition - } catch (error) { - console.warn(`Failed to fetch node definition for ${nodeName}:`, error) - return null - } -} - -const resetState = (setShowModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) => { - setShowModal(false) - setCredentialList([]) - setAllCredentialsState([]) - setInitialDontShowAgain(false) -} - -const buildCallbackPayload = (flowState, evaluation, mode) => ({ - flowData: flowState?.raw ?? null, - parsedFlow: flowState?.parsed ?? null, - allCredentials: evaluation?.allCredentials ?? [], - missingCredentials: evaluation?.missingCredentials ?? [], - mode -}) - -export const useFlowCredentials = () => { - const [showCredentialModal, setShowCredentialModal] = useState(false) - const [credentialList, setCredentialList] = useState([]) - const [allCredentialsState, setAllCredentialsState] = useState([]) - const [initialDontShowAgain, setInitialDontShowAgain] = useState(false) - - const preferenceScopeRef = useRef(null) - const callbacksRef = useRef({ onAssign: null, onSkip: null, onCancel: null }) - const flowRef = useRef({ raw: null, parsed: null, format: 'object' }) - const evaluationRef = useRef({ allCredentials: [], missingCredentials: [] }) - const modeRef = useRef('missing') - - const openCredentialModal = useCallback(async (flowData, options = {}) => { - const { preferenceScope = null, onAssign, onSkip, onCancel, forceShow = false, mode = 'missing' } = options - - const evaluation = await collectFlowCredentials(flowData, { fetchNodeDefinition }) - evaluationRef.current = evaluation - - const listForModal = mode === 'all' ? evaluation.allCredentials : evaluation.missingCredentials - const hasItems = listForModal.length > 0 - const preferenceDismissed = preferenceScope ? getCredentialModalDismissed(preferenceScope) : false - - if (!forceShow && (!hasItems || preferenceDismissed)) { - return { shown: false, evaluation } - } - - const isStringInput = typeof flowData === 'string' - const parsedFlow = isStringInput ? JSON.parse(flowData) : { ...flowData } - - flowRef.current = { - raw: isStringInput ? flowData : parsedFlow, - parsed: parsedFlow, - format: isStringInput ? 'string' : 'object' - } - - callbacksRef.current = { onAssign, onSkip, onCancel } - preferenceScopeRef.current = preferenceScope - modeRef.current = mode - - setCredentialList(listForModal) - setAllCredentialsState(evaluation.allCredentials) - setInitialDontShowAgain(preferenceScope ? preferenceDismissed : false) - setShowCredentialModal(true) - - return { shown: true, evaluation } - }, []) - - const applyPreference = (options) => { - const scope = preferenceScopeRef.current - if (!scope) return - if (options?.dontShowDirty) { - setCredentialModalDismissed(scope, !!options.dontShowAgain) - } - } - - const handleAssign = useCallback((credentialAssignments, options = {}) => { - applyPreference(options) - - const currentFlow = flowRef.current - const format = currentFlow.format - const source = format === 'string' ? currentFlow.raw : currentFlow.parsed - - let nextRaw = currentFlow.raw - let nextParsed = currentFlow.parsed - - if (credentialAssignments && Object.keys(credentialAssignments).length > 0) { - const updatedFlow = updateFlowDataWithCredentials(source, credentialAssignments) - nextParsed = updatedFlow - nextRaw = format === 'string' ? JSON.stringify(updatedFlow) : updatedFlow - } - - flowRef.current = { - raw: nextRaw, - parsed: nextParsed, - format - } - - callbacksRef.current.onAssign?.( - nextRaw, - credentialAssignments, - buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current) - ) - - resetState(setShowCredentialModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) - preferenceScopeRef.current = null - callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } - }, []) - - const handleSkip = useCallback((options = {}) => { - applyPreference(options) - callbacksRef.current.onSkip?.(flowRef.current.raw, buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current)) - - resetState(setShowCredentialModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) - preferenceScopeRef.current = null - callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } - }, []) - - const handleCancel = useCallback((options = {}) => { - applyPreference(options) - callbacksRef.current.onCancel?.(flowRef.current.raw, buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current)) - - resetState(setShowCredentialModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) - preferenceScopeRef.current = null - callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } - }, []) - - return { - showCredentialModal, - missingCredentials: credentialList, - allCredentials: allCredentialsState, - initialDontShowAgain, - modalMode: modeRef.current, - openCredentialModal, - handleAssign, - handleSkip, - handleCancel - } -} - -export default useFlowCredentials diff --git a/packages/ui/src/utils/flowCredentialsHelper.js b/packages/ui/src/utils/flowCredentialsHelper.js index f9036c05f0d..0197d08dfde 100644 --- a/packages/ui/src/utils/flowCredentialsHelper.js +++ b/packages/ui/src/utils/flowCredentialsHelper.js @@ -212,41 +212,6 @@ export const groupAllCredentialsByType = (allCredentials) => { * @param {object} credentialAssignments - Object mapping nodeId to credentialId * @returns {object} Updated flow data */ -const CONFIG_COMPONENT_KEYS = [ - 'llmModel', - 'llmModelName', - 'agentModel', - 'conditionAgentModel', - 'retrieverModel', - 'model', - 'modelName', - 'chatModel', - 'assistantModel', - 'selectedAssistant', - 'toolModel', - 'selectedTool', - 'agentFlowModel' -] - -const buildConfigEntryKey = (nodeId, parameterName, credentialType) => `${nodeId}:${parameterName}:${credentialType}` - -const resolveConfigComponentName = (config, inputs, parameterName) => { - if (!config || typeof config !== 'object') return '' - for (const key of CONFIG_COMPONENT_KEYS) { - if (typeof config[key] === 'string' && config[key]) return config[key] - } - const matchingInputKey = parameterName.replace(/Config$/, '') - if (matchingInputKey && typeof inputs?.[matchingInputKey] === 'string') { - return inputs[matchingInputKey] - } - for (const value of Object.values(config)) { - if (typeof value === 'string' && value) { - return value - } - } - return '' -} - export const updateFlowDataWithCredentials = (flowData, credentialAssignments) => { try { const flow = typeof flowData === 'string' ? JSON.parse(flowData) : { ...flowData } @@ -260,26 +225,15 @@ export const updateFlowDataWithCredentials = (flowData, credentialAssignments) = if (credentialAssignments[node.id]) { const updatedNode = { ...node } updatedNode.data = { ...node.data } - const assignedCredential = credentialAssignments[node.id] // Set the credential - updatedNode.data.credential = assignedCredential + updatedNode.data.credential = credentialAssignments[node.id] // Also set in inputs for compatibility if (!updatedNode.data.inputs) { updatedNode.data.inputs = {} } - updatedNode.data.inputs['FLOWISE_CREDENTIAL_ID'] = assignedCredential - - // Update nested config objects that track credential assignments - Object.entries(updatedNode.data.inputs).forEach(([inputKey, inputValue]) => { - if (!inputValue || typeof inputValue !== 'object') return - if (!Object.prototype.hasOwnProperty.call(inputValue, 'credential')) return - updatedNode.data.inputs[inputKey] = { - ...inputValue, - credential: assignedCredential - } - }) + updatedNode.data.inputs['FLOWISE_CREDENTIAL_ID'] = credentialAssignments[node.id] return updatedNode } @@ -293,104 +247,6 @@ export const updateFlowDataWithCredentials = (flowData, credentialAssignments) = } } -export const collectFlowCredentials = async (flowData, options = {}) => { - const { fetchNodeDefinition } = options - const flow = typeof flowData === 'string' ? JSON.parse(flowData) : { ...flowData } - - if (!flow.nodes || !Array.isArray(flow.nodes)) { - return { - allCredentials: [], - missingCredentials: [], - hasCredentials: false, - hasMissingCredentials: false - } - } - - const baseMissing = extractMissingCredentials(flow).missingCredentials || [] - const baseAll = extractAllCredentials(flow).allCredentials || [] - - const allCredentials = [...baseAll] - const missingCredentials = [...baseMissing] - - const seenAllKeys = new Set(allCredentials.map((cred) => buildConfigEntryKey(cred.nodeId, cred.parameterName, cred.credentialType))) - const seenMissingKeys = new Set( - missingCredentials.map((cred) => buildConfigEntryKey(cred.nodeId, cred.parameterName, cred.credentialType)) - ) - - if (typeof fetchNodeDefinition === 'function') { - for (const node of flow.nodes) { - const nodeData = node.data || {} - const inputs = nodeData.inputs || {} - - for (const [inputKey, inputValue] of Object.entries(inputs)) { - if (!inputKey.endsWith('Config')) continue - if (!inputValue || typeof inputValue !== 'object') continue - if (!Object.prototype.hasOwnProperty.call(inputValue, 'credential')) continue - - const componentName = resolveConfigComponentName(inputValue, inputs, inputKey) - - let nodeDefinition = null - let credentialNames = [] - - if (componentName) { - try { - nodeDefinition = await fetchNodeDefinition(componentName) - const credentialObj = nodeDefinition?.credential - if (credentialObj?.credentialNames && Array.isArray(credentialObj.credentialNames)) { - credentialNames = credentialObj.credentialNames - } - } catch (error) { - console.warn(`Failed to load node definition for ${componentName}:`, error) - } - } - - if (!credentialNames.length) { - credentialNames = componentName ? [componentName] : [inputKey] - } - - const label = - nodeDefinition?.label || - (componentName ? toSentenceCase(componentName) : toSentenceCase(inputKey.replace(/Config$/, 'Credential'))) - - credentialNames.forEach((credentialName) => { - const entryKey = buildConfigEntryKey(node.id, inputKey, credentialName) - const isAssigned = !!inputValue.credential - const entry = { - nodeId: node.id, - nodeName: nodeData.name || 'Unknown Node', - nodeCategory: nodeData.category || 'Unknown', - nodeType: nodeData.type || node.type || 'Unknown', - credentialType: credentialName, - parameterName: inputKey, - label, - isOptional: false, - isRequired: true, - isAssigned, - assignedCredentialId: isAssigned ? inputValue.credential : null - } - - if (!seenAllKeys.has(entryKey)) { - seenAllKeys.add(entryKey) - allCredentials.push(entry) - } - - if (!isAssigned && !seenMissingKeys.has(entryKey)) { - seenMissingKeys.add(entryKey) - missingCredentials.push(entry) - } - }) - } - } - } - - return { - allCredentials, - missingCredentials, - hasCredentials: allCredentials.length > 0, - hasMissingCredentials: missingCredentials.length > 0 - } -} - /** * Check if a credential assignment is valid for a node * @param {object} node - Flow node @@ -414,18 +270,18 @@ export const isValidCredentialAssignment = (node, credentialId, availableCredent */ export const organizeCredentialsByPriority = (groupedCredentials) => { const sections = { - required: [], // Unconnected required credentials - optional: [], // Unconnected optional credentials - connected: [] // All connected credentials + required: [], // Unconnected required credentials + optional: [], // Unconnected optional credentials + connected: [] // All connected credentials } Object.entries(groupedCredentials).forEach(([groupKey, group]) => { const isConnected = group.isAssigned || false - + // Check if ANY node in this group is marked as required // isRequired is calculated as: category.isCore || !credentialParam.optional const isRequired = group.nodes?.some((node) => node.isRequired === true) || false - + // Also track isCore for sorting connected credentials const isCore = group.nodes?.some((node) => node.isCore === true) || false @@ -452,7 +308,7 @@ export const organizeCredentialsByPriority = (groupedCredentials) => { // Sort functions const sortByLabel = (a, b) => (a.label || '').localeCompare(b.label || '') - + // Sort connected section: required first, then alphabetically within each group const sortConnected = (a, b) => { // Required credentials before optional diff --git a/packages/ui/src/views/agentflowsv2/Canvas.jsx b/packages/ui/src/views/agentflowsv2/Canvas.jsx index e09e5ab40d3..3203dbe0316 100644 --- a/packages/ui/src/views/agentflowsv2/Canvas.jsx +++ b/packages/ui/src/views/agentflowsv2/Canvas.jsx @@ -32,8 +32,6 @@ import EditNodeDialog from '@/views/agentflowsv2/EditNodeDialog' import ChatPopUp from '@/views/chatmessage/ChatPopUp' import ValidationPopUp from '@/views/chatmessage/ValidationPopUp' import { flowContext } from '@/store/context/ReactFlowContext' -import useFlowCredentials from '@/hooks/useFlowCredentials' -import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' // API import nodesApi from '@/api/nodes' @@ -103,10 +101,6 @@ const AgentflowCanvas = () => { const [editNodeDialogOpen, setEditNodeDialogOpen] = useState(false) const [editNodeDialogProps, setEditNodeDialogProps] = useState({}) - const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = - useFlowCredentials() - const hasPromptedCredentialsRef = useRef(false) - const reactFlowWrapper = useRef(null) // ==============================|| Chatflow API ||============================== // @@ -169,9 +163,6 @@ const AgentflowCanvas = () => { setNodes(nodes) setEdges(flowData.edges || []) setTimeout(() => setDirty(), 0) - - // Credential modal will be triggered automatically after save via useEffect - // watching canvasDataStore.chatflow.flowData (lines 585-593) } catch (e) { console.error(e) } @@ -537,10 +528,6 @@ const AgentflowCanvas = () => { const initialFlow = chatflow.flowData ? JSON.parse(chatflow.flowData) : [] setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) - // Allow modal to check for missing credentials when loading existing flow - // Modal will show immediately if credentials are missing (unless user dismissed it) - // MUST set this BEFORE dispatch to allow modal trigger - hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) } else if (getSpecificChatflowApi.error) { errorFailed(`Failed to retrieve ${canvasTitle}: ${getSpecificChatflowApi.error.response.data.message}`) @@ -553,9 +540,6 @@ const AgentflowCanvas = () => { useEffect(() => { if (createNewChatflowApi.data) { const chatflow = createNewChatflowApi.data - // Reset to allow modal to show after first save - // MUST set this BEFORE dispatch to allow modal trigger - hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) saveChatflowSuccess() window.history.replaceState(state, null, `/v2/agentcanvas/${chatflow.id}`) @@ -569,9 +553,6 @@ const AgentflowCanvas = () => { // Update chatflow successful useEffect(() => { if (updateChatflowApi.data) { - // Reset to allow modal to check for missing credentials after update - // MUST set this BEFORE dispatch to allow modal trigger - hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data }) saveChatflowSuccess() } else if (updateChatflowApi.error) { @@ -591,58 +572,8 @@ const AgentflowCanvas = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasDataStore.chatflow]) - useEffect(() => { - const flowId = canvasDataStore.chatflow?.id - const flowData = canvasDataStore.chatflow?.flowData - - if (!flowId || !flowData || hasPromptedCredentialsRef.current) { - return - } - - hasPromptedCredentialsRef.current = true - const preferenceScope = `flow:${flowId}` - openCredentialModal(flowData, { preferenceScope }).catch((error) => console.error('Failed to open credential modal:', error)) - }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) - - // Listen for QuickSetup parameter to open credential modal in "all" mode - useEffect(() => { - const handleQuickSetup = () => { - const searchParams = new URLSearchParams(window.location.search) - const isQuickSetup = searchParams.get('QuickSetup') === 'true' - - if (isQuickSetup && canvasDataStore.chatflow?.flowData) { - const flowId = canvasDataStore.chatflow.id - const preferenceScope = flowId ? `flow:${flowId}` : null - - // Open modal with all credentials (mode: 'all') and force show - openCredentialModal(canvasDataStore.chatflow.flowData, { - preferenceScope, - mode: 'all', - forceShow: true - }).catch((error) => console.error('Failed to open credential modal:', error)) - - // Clean up URL parameter - searchParams.delete('QuickSetup') - const newUrl = `${window.location.pathname}${searchParams.toString() ? '?' + searchParams.toString() : ''}` - window.history.replaceState({}, '', newUrl) - } - } - - // Check immediately on mount - handleQuickSetup() - - // Listen for popstate events (when button triggers history change) - window.addEventListener('popstate', handleQuickSetup) - - return () => { - window.removeEventListener('popstate', handleQuickSetup) - } - }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) - // Initialization useEffect(() => { - // DO NOT reset hasPromptedCredentialsRef here - it prevents duplicate modals - // The ref is managed by the credential modal useEffect and save handlers setIsSyncNodesButtonEnabled(false) if (chatflowId) { getSpecificChatflowApi.request(chatflowId) @@ -654,10 +585,6 @@ const AgentflowCanvas = () => { setNodes([]) setEdges([]) } - // Block credential modal until user saves - // Modal should only show AFTER user clicks save - // MUST set this BEFORE dispatch to prevent modal trigger - hasPromptedCredentialsRef.current = true dispatch({ type: SET_CHATFLOW, chatflow: { @@ -686,9 +613,6 @@ const AgentflowCanvas = () => { //TODO: prevent paste event when input focused, temporary fix: catch chatflow syntax if (pasteData.includes('{"nodes":[') && pasteData.includes('],"edges":[')) { handleLoadFlow(pasteData) - // Block credential modal until user saves - // Modal should only show AFTER user clicks save - hasPromptedCredentialsRef.current = true } } @@ -704,9 +628,6 @@ const AgentflowCanvas = () => { useEffect(() => { if (templateFlowData && templateFlowData.includes('"nodes":[') && templateFlowData.includes('],"edges":[')) { handleLoadFlow(templateFlowData) - // Block credential modal until user saves - // Modal should only show AFTER user clicks save - hasPromptedCredentialsRef.current = true } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -858,15 +779,6 @@ const AgentflowCanvas = () => { - - ) } diff --git a/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx b/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx index 64a015f12d2..f3eb529676f 100644 --- a/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx +++ b/packages/ui/src/views/agentflowsv2/MarketplaceCanvas.jsx @@ -48,6 +48,8 @@ const MarketplaceCanvasV2 = () => { setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) } + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [flowData]) const onChatflowCopy = (stateData) => { diff --git a/packages/ui/src/views/canvas/index.jsx b/packages/ui/src/views/canvas/index.jsx index 820b72027a2..f623177bf0f 100644 --- a/packages/ui/src/views/canvas/index.jsx +++ b/packages/ui/src/views/canvas/index.jsx @@ -58,7 +58,7 @@ import { usePrompt } from '@/utils/usePrompt' import { FLOWISE_CREDENTIAL_ID } from '@/store/constant' // credential checking -import useFlowCredentials from '@/hooks/useFlowCredentials' +import { useCredentialChecker } from '@/hooks/useCredentialChecker' import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' const nodeTypes = { customNode: CanvasNode, stickyNote: StickyNote } const edgeTypes = { buttonedge: ButtonEdge } @@ -110,9 +110,8 @@ const Canvas = ({ chatflowid: chatflowId }) => { const reactFlowWrapper = useRef(null) const canvasHeaderRef = useRef(null) - const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = - useFlowCredentials() - const hasPromptedCredentialsRef = useRef(false) + const { showCredentialModal, missingCredentials, checkCredentials, initialDontShowAgain, handleAssign, handleSkip, handleCancel } = + useCredentialChecker() // ==============================|| Chatflow API ||============================== // @@ -227,11 +226,6 @@ const Canvas = ({ chatflowid: chatflowId }) => { flowData: JSON.stringify({ nodes, edges }) } - // Block credential modal from showing until user saves - // Modal should only show AFTER user clicks save - // MUST set this BEFORE dispatch to prevent modal trigger - hasPromptedCredentialsRef.current = true - dispatch({ type: SET_CHATFLOW, chatflow: newChatflow }) setChatflow(newChatflow) setNodes(nodes) @@ -559,10 +553,6 @@ const Canvas = ({ chatflowid: chatflowId }) => { const initialFlow = chatflow.flowData ? JSON.parse(chatflow.flowData) : [] setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) - // Allow modal to check for missing credentials when loading existing flow - // Modal will show immediately if credentials are missing (unless user dismissed it) - // MUST set this BEFORE dispatch to allow modal trigger - hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) } else if (getSpecificChatflowApi?.error?.response?.data?.message) { errorFailed(`Failed to save ${canvasTitle}: ${getSpecificChatflowApi.error.response.data.message}`) @@ -578,9 +568,6 @@ const Canvas = ({ chatflowid: chatflowId }) => { if (createNewChatflowApi?.data) { const chatflow = createNewChatflowApi.data // console.log('✅ Create new chatflow successful, navigating to:', `/${isAgentCanvas ? 'agentcanvas' : 'canvas'}/${chatflow.id}`) - // Reset to allow modal to show after first save - // MUST set this BEFORE dispatch to allow modal trigger - hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) saveChatflowSuccess() navigate(`/${isAgentCanvas ? 'agentcanvas' : 'canvas'}/${chatflow.id}`, { @@ -599,9 +586,6 @@ const Canvas = ({ chatflowid: chatflowId }) => { useEffect(() => { if (updateChatflowApi?.data) { // console.log('✅ Update chatflow successful, NOT navigating (should we?)') - // Reset to allow modal to check for missing credentials after update - // MUST set this BEFORE dispatch to allow modal trigger - hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data }) saveChatflowSuccess() } else if (updateChatflowApi?.error?.response?.data?.message) { @@ -624,60 +608,10 @@ const Canvas = ({ chatflowid: chatflowId }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasDataStore.chatflow]) - useEffect(() => { - const flowId = canvasDataStore.chatflow?.id - const flowData = canvasDataStore.chatflow?.flowData - - if (!flowId || !flowData || hasPromptedCredentialsRef.current) { - return - } - - hasPromptedCredentialsRef.current = true - const preferenceScope = `flow:${flowId}` - openCredentialModal(flowData, { preferenceScope }).catch((error) => console.error('Failed to open credential modal:', error)) - }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) - - // Listen for QuickSetup parameter to open credential modal in "all" mode - useEffect(() => { - const handleQuickSetup = () => { - const searchParams = new URLSearchParams(window.location.search) - const isQuickSetup = searchParams.get('QuickSetup') === 'true' - - if (isQuickSetup && canvasDataStore.chatflow?.flowData) { - const flowId = canvasDataStore.chatflow.id - const preferenceScope = flowId ? `flow:${flowId}` : null - - // Open modal with all credentials (mode: 'all') and force show - openCredentialModal(canvasDataStore.chatflow.flowData, { - preferenceScope, - mode: 'all', - forceShow: true - }).catch((error) => console.error('Failed to open credential modal:', error)) - - // Clean up URL parameter - searchParams.delete('QuickSetup') - const newUrl = `${window.location.pathname}${searchParams.toString() ? '?' + searchParams.toString() : ''}` - window.history.replaceState({}, '', newUrl) - } - } - - // Check immediately on mount - handleQuickSetup() - - // Listen for popstate events (when button triggers history change) - window.addEventListener('popstate', handleQuickSetup) - - return () => { - window.removeEventListener('popstate', handleQuickSetup) - } - }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) - // Initialization useEffect(() => { // console.log('🎨 Canvas initialization effect running:', { chatflowId }) - // DO NOT reset hasPromptedCredentialsRef here - it prevents duplicate modals - // The ref is managed by the credential modal useEffect and save handlers setIsSyncNodesButtonEnabled(false) setIsUpsertButtonEnabled(false) if (chatflowId) { @@ -707,12 +641,6 @@ const Canvas = ({ chatflowid: chatflowId }) => { deployed: false, isPublic: false } - - // Block credential modal from showing until user saves - // Modal should only show AFTER user clicks save - // MUST set this BEFORE dispatch to prevent modal trigger - hasPromptedCredentialsRef.current = true - setChatflow(newChatflow) dispatch({ type: SET_CHATFLOW, chatflow: newChatflow }) @@ -729,9 +657,6 @@ const Canvas = ({ chatflowid: chatflowId }) => { // console.log('🎨 No duplicated flow data, creating blank canvas') setNodes([]) setEdges([]) - // Block credential modal for blank canvas until user saves - // MUST set this BEFORE dispatch to prevent modal trigger - hasPromptedCredentialsRef.current = true setChatflow({ name: templateName ? `Copy of ${templateName}` : `Untitled ${canvasTitle}` }) @@ -887,6 +812,7 @@ const Canvas = ({ chatflowid: chatflowId }) => { onAssign={handleAssign} onSkip={handleSkip} onCancel={handleCancel} + flowData={null} initialDontShowAgain={initialDontShowAgain} /> diff --git a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx index 92595ddf550..317d68e89d2 100644 --- a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx +++ b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx @@ -15,6 +15,10 @@ import MarketplaceCanvasHeader from './MarketplaceCanvasHeader' import StickyNote from '../canvas/StickyNote' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' +// credential checking +import { useCredentialChecker } from '@/hooks/useCredentialChecker' +import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' + const nodeTypes = { customNode: MarketplaceCanvasNode, stickyNote: StickyNote } const edgeTypes = { buttonedge: '' } @@ -34,6 +38,10 @@ const MarketplaceCanvas = () => { const reactFlowWrapper = useRef(null) + // Credential checking hook + const { showCredentialModal, missingCredentials, checkCredentials, initialDontShowAgain, handleAssign, handleSkip, handleCancel } = + useCredentialChecker() + // ==============================|| useEffect ||============================== // useEffect(() => { @@ -42,6 +50,8 @@ const MarketplaceCanvas = () => { setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) } + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [flowData]) const onChatflowCopy = (stateData) => { @@ -121,6 +131,17 @@ const MarketplaceCanvas = () => { {/* Confirm Dialog */} + + {/* Unified Credentials Modal */} + ) } From f0dc43587f3ec1a3c31c0e26082366dbc6746c69 Mon Sep 17 00:00:00 2001 From: Diego Costa Date: Tue, 28 Oct 2025 13:52:41 -0300 Subject: [PATCH 4/7] fix: restore credential modal logic in agentflow canvas after rebase --- packages/ui/src/views/agentflowsv2/Canvas.jsx | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/packages/ui/src/views/agentflowsv2/Canvas.jsx b/packages/ui/src/views/agentflowsv2/Canvas.jsx index 3203dbe0316..e09e5ab40d3 100644 --- a/packages/ui/src/views/agentflowsv2/Canvas.jsx +++ b/packages/ui/src/views/agentflowsv2/Canvas.jsx @@ -32,6 +32,8 @@ import EditNodeDialog from '@/views/agentflowsv2/EditNodeDialog' import ChatPopUp from '@/views/chatmessage/ChatPopUp' import ValidationPopUp from '@/views/chatmessage/ValidationPopUp' import { flowContext } from '@/store/context/ReactFlowContext' +import useFlowCredentials from '@/hooks/useFlowCredentials' +import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' // API import nodesApi from '@/api/nodes' @@ -101,6 +103,10 @@ const AgentflowCanvas = () => { const [editNodeDialogOpen, setEditNodeDialogOpen] = useState(false) const [editNodeDialogProps, setEditNodeDialogProps] = useState({}) + const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = + useFlowCredentials() + const hasPromptedCredentialsRef = useRef(false) + const reactFlowWrapper = useRef(null) // ==============================|| Chatflow API ||============================== // @@ -163,6 +169,9 @@ const AgentflowCanvas = () => { setNodes(nodes) setEdges(flowData.edges || []) setTimeout(() => setDirty(), 0) + + // Credential modal will be triggered automatically after save via useEffect + // watching canvasDataStore.chatflow.flowData (lines 585-593) } catch (e) { console.error(e) } @@ -528,6 +537,10 @@ const AgentflowCanvas = () => { const initialFlow = chatflow.flowData ? JSON.parse(chatflow.flowData) : [] setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) + // Allow modal to check for missing credentials when loading existing flow + // Modal will show immediately if credentials are missing (unless user dismissed it) + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) } else if (getSpecificChatflowApi.error) { errorFailed(`Failed to retrieve ${canvasTitle}: ${getSpecificChatflowApi.error.response.data.message}`) @@ -540,6 +553,9 @@ const AgentflowCanvas = () => { useEffect(() => { if (createNewChatflowApi.data) { const chatflow = createNewChatflowApi.data + // Reset to allow modal to show after first save + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) saveChatflowSuccess() window.history.replaceState(state, null, `/v2/agentcanvas/${chatflow.id}`) @@ -553,6 +569,9 @@ const AgentflowCanvas = () => { // Update chatflow successful useEffect(() => { if (updateChatflowApi.data) { + // Reset to allow modal to check for missing credentials after update + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data }) saveChatflowSuccess() } else if (updateChatflowApi.error) { @@ -572,8 +591,58 @@ const AgentflowCanvas = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasDataStore.chatflow]) + useEffect(() => { + const flowId = canvasDataStore.chatflow?.id + const flowData = canvasDataStore.chatflow?.flowData + + if (!flowId || !flowData || hasPromptedCredentialsRef.current) { + return + } + + hasPromptedCredentialsRef.current = true + const preferenceScope = `flow:${flowId}` + openCredentialModal(flowData, { preferenceScope }).catch((error) => console.error('Failed to open credential modal:', error)) + }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) + + // Listen for QuickSetup parameter to open credential modal in "all" mode + useEffect(() => { + const handleQuickSetup = () => { + const searchParams = new URLSearchParams(window.location.search) + const isQuickSetup = searchParams.get('QuickSetup') === 'true' + + if (isQuickSetup && canvasDataStore.chatflow?.flowData) { + const flowId = canvasDataStore.chatflow.id + const preferenceScope = flowId ? `flow:${flowId}` : null + + // Open modal with all credentials (mode: 'all') and force show + openCredentialModal(canvasDataStore.chatflow.flowData, { + preferenceScope, + mode: 'all', + forceShow: true + }).catch((error) => console.error('Failed to open credential modal:', error)) + + // Clean up URL parameter + searchParams.delete('QuickSetup') + const newUrl = `${window.location.pathname}${searchParams.toString() ? '?' + searchParams.toString() : ''}` + window.history.replaceState({}, '', newUrl) + } + } + + // Check immediately on mount + handleQuickSetup() + + // Listen for popstate events (when button triggers history change) + window.addEventListener('popstate', handleQuickSetup) + + return () => { + window.removeEventListener('popstate', handleQuickSetup) + } + }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) + // Initialization useEffect(() => { + // DO NOT reset hasPromptedCredentialsRef here - it prevents duplicate modals + // The ref is managed by the credential modal useEffect and save handlers setIsSyncNodesButtonEnabled(false) if (chatflowId) { getSpecificChatflowApi.request(chatflowId) @@ -585,6 +654,10 @@ const AgentflowCanvas = () => { setNodes([]) setEdges([]) } + // Block credential modal until user saves + // Modal should only show AFTER user clicks save + // MUST set this BEFORE dispatch to prevent modal trigger + hasPromptedCredentialsRef.current = true dispatch({ type: SET_CHATFLOW, chatflow: { @@ -613,6 +686,9 @@ const AgentflowCanvas = () => { //TODO: prevent paste event when input focused, temporary fix: catch chatflow syntax if (pasteData.includes('{"nodes":[') && pasteData.includes('],"edges":[')) { handleLoadFlow(pasteData) + // Block credential modal until user saves + // Modal should only show AFTER user clicks save + hasPromptedCredentialsRef.current = true } } @@ -628,6 +704,9 @@ const AgentflowCanvas = () => { useEffect(() => { if (templateFlowData && templateFlowData.includes('"nodes":[') && templateFlowData.includes('],"edges":[')) { handleLoadFlow(templateFlowData) + // Block credential modal until user saves + // Modal should only show AFTER user clicks save + hasPromptedCredentialsRef.current = true } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -779,6 +858,15 @@ const AgentflowCanvas = () => { + + ) } From dbd9c80b1246cf253204d1c057628784cd83b9f7 Mon Sep 17 00:00:00 2001 From: Diego Costa Date: Tue, 28 Oct 2025 15:35:25 -0300 Subject: [PATCH 5/7] Refactor credential extraction and defer agentflow modal --- .../utils/src/extractAllCredentials.ts | 75 +-------- .../utils/src/extractMissingCredentials.ts | 77 +-------- .../utils/src/processFlowCredentials.ts | 83 +++++++++ packages/ui/src/hooks/useFlowCredentials.js | 158 ++++++++++++++++++ .../ui/src/utils/flowCredentialsHelper.js | 28 ++++ packages/ui/src/views/agentflowsv2/Canvas.jsx | 89 ++++++---- packages/ui/src/views/canvas/index.jsx | 75 ++++++++- .../views/marketplaces/MarketplaceCanvas.jsx | 21 --- 8 files changed, 407 insertions(+), 199 deletions(-) create mode 100644 packages-answers/utils/src/processFlowCredentials.ts create mode 100644 packages/ui/src/hooks/useFlowCredentials.js diff --git a/packages-answers/utils/src/extractAllCredentials.ts b/packages-answers/utils/src/extractAllCredentials.ts index adb48488d3c..b53142fb31c 100644 --- a/packages-answers/utils/src/extractAllCredentials.ts +++ b/packages-answers/utils/src/extractAllCredentials.ts @@ -1,4 +1,4 @@ -import { getCredentialCategory } from './getCredentialCategory' +import { processFlowCredentials } from './processFlowCredentials' /** * Extract all credentials (assigned and unassigned) from flow data for QuickSetup mode @@ -6,75 +6,10 @@ import { getCredentialCategory } from './getCredentialCategory' * @returns {object} Object containing all credentials info */ export const extractAllCredentials = (flowData: string | any) => { - try { - // Parse flow data if it's a string - const flow = typeof flowData === 'string' ? JSON.parse(flowData) : flowData + const { credentials, hasCredentials } = processFlowCredentials(flowData) - if (!flow.nodes || !Array.isArray(flow.nodes)) { - return { allCredentials: [], hasCredentials: false } - } - - const allCredentials: any[] = [] - const credentialTypes = new Set() - - // Iterate through all nodes - flow.nodes.forEach((node: any) => { - if (node.data && node.data.inputParams) { - // Find all credential input parameters - const credentialParams = node.data.inputParams.filter((param: any) => { - return param.type === 'credential' - }) - - credentialParams.forEach((credentialParam: any) => { - // Check if credential is assigned - const hasCredential = - node.data.credential || - (node.data.inputs && node.data.inputs[credentialParam.name]) || - (node.data.inputs && node.data.inputs['FLOWISE_CREDENTIAL_ID']) - - // Extract credential names - const credentialNames = credentialParam.credentialNames || [] - - credentialNames.forEach((credentialName: string) => { - credentialTypes.add(credentialName) - - // Get category information to determine if credential is core - const category = getCredentialCategory(node.data.category, credentialName) - - // Credential is required if: - // 1. Category is core (Chat Models, Agents, etc), OR - // 2. Parameter explicitly marked as NOT optional (optional: false) - // Default to optional if not explicitly marked - const isRequired = category.isCore || (credentialParam.optional === false) - - const credInfo = { - nodeId: node.id, - nodeName: node.data.name || 'Unknown Node', - nodeCategory: node.data.category || 'Unknown', - nodeType: node.data.type || node.type || 'Unknown', - credentialType: credentialName, - parameterName: credentialParam.name, - label: credentialParam.label || credentialParam.name, - isRequired: isRequired, - isCore: category.isCore, - categoryType: category.type, - categoryDisplayName: category.displayName, - isAssigned: !!hasCredential, - assignedCredentialId: hasCredential || null - } - - allCredentials.push(credInfo) - }) - }) - } - }) - - return { - allCredentials, - hasCredentials: credentialTypes.size > 0 - } - } catch (error) { - console.error('Error in extractAllCredentials:', error) - return { allCredentials: [], hasCredentials: false } + return { + allCredentials: credentials, + hasCredentials } } diff --git a/packages-answers/utils/src/extractMissingCredentials.ts b/packages-answers/utils/src/extractMissingCredentials.ts index 2a9be8ff0e8..9e2773be94c 100644 --- a/packages-answers/utils/src/extractMissingCredentials.ts +++ b/packages-answers/utils/src/extractMissingCredentials.ts @@ -2,7 +2,7 @@ * Utility functions for detecting and managing missing credentials in flows */ -import { getCredentialCategory } from './getCredentialCategory' +import { processFlowCredentials } from './processFlowCredentials' /** * Extract required credentials from flow data and identify missing ones @@ -10,75 +10,14 @@ import { getCredentialCategory } from './getCredentialCategory' * @returns {object} Object containing missing credentials info */ export const extractMissingCredentials = (flowData: string | any) => { - try { - // Parse flow data if it's a string - const flow = typeof flowData === 'string' ? JSON.parse(flowData) : flowData + const { credentials, hasCredentials } = processFlowCredentials(flowData) - if (!flow.nodes || !Array.isArray(flow.nodes)) { - return { missingCredentials: [], hasCredentials: false } - } + const missingCredentials = credentials + .filter((credential) => !credential.isAssigned) + .map(({ isAssigned, assignedCredentialId, ...rest }) => rest) - const missingCredentials: any[] = [] - const credentialTypes = new Set() - - // Iterate through all nodes - flow.nodes.forEach((node: any) => { - if (node.data && node.data.inputParams) { - // Find all credential input parameters - const credentialParams = node.data.inputParams.filter((param: any) => { - return param.type === 'credential' - }) - - credentialParams.forEach((credentialParam: any) => { - // Check if credential is assigned - const hasCredential = - node.data.credential || - (node.data.inputs && node.data.inputs[credentialParam.name]) || - (node.data.inputs && node.data.inputs['FLOWISE_CREDENTIAL_ID']) - - if (!hasCredential) { - // Extract credential names - const credentialNames = credentialParam.credentialNames || [] - - credentialNames.forEach((credentialName: string) => { - credentialTypes.add(credentialName) - - // Get category information to determine if credential is core - const category = getCredentialCategory(node.data.category, credentialName) - - // Credential is required if: - // 1. Category is core (Chat Models, Agents, etc), OR - // 2. Parameter explicitly marked as NOT optional (optional: false) - // Default to optional if not explicitly marked - const isRequired = category.isCore || (credentialParam.optional === false) - - const missingCred = { - nodeId: node.id, - nodeName: node.data.name || 'Unknown Node', - nodeCategory: node.data.category || 'Unknown', - nodeType: node.data.type || node.type || 'Unknown', - credentialType: credentialName, - parameterName: credentialParam.name, - label: credentialParam.label || credentialParam.name, - isRequired: isRequired, - isCore: category.isCore, - categoryType: category.type, - categoryDisplayName: category.displayName - } - - missingCredentials.push(missingCred) - }) - } - }) - } - }) - - return { - missingCredentials, - hasCredentials: credentialTypes.size > 0 - } - } catch (error) { - console.error('Error in extractMissingCredentials:', error) - return { missingCredentials: [], hasCredentials: false } + return { + missingCredentials, + hasCredentials } } diff --git a/packages-answers/utils/src/processFlowCredentials.ts b/packages-answers/utils/src/processFlowCredentials.ts new file mode 100644 index 00000000000..c855ae209d7 --- /dev/null +++ b/packages-answers/utils/src/processFlowCredentials.ts @@ -0,0 +1,83 @@ +import { getCredentialCategory } from './getCredentialCategory' + +type FlowData = string | Record + +export type FlowCredentialRecord = { + nodeId: string + nodeName: string + nodeCategory: string + nodeType: string + credentialType: string + parameterName: string + label: string + isRequired: boolean + isCore: boolean + categoryType: string + categoryDisplayName: string + isAssigned: boolean + assignedCredentialId: string | null +} + +type ProcessResult = { + credentials: FlowCredentialRecord[] + hasCredentials: boolean +} + +export const processFlowCredentials = (flowData: FlowData): ProcessResult => { + try { + const flow = typeof flowData === 'string' ? JSON.parse(flowData) : flowData + + if (!flow || !Array.isArray(flow.nodes)) { + return { credentials: [], hasCredentials: false } + } + + const credentials: FlowCredentialRecord[] = [] + const credentialTypes = new Set() + + flow.nodes.forEach((node: any) => { + const inputParams = node?.data?.inputParams + if (!Array.isArray(inputParams)) return + + const credentialParams = inputParams.filter((param: any) => param?.type === 'credential') + + credentialParams.forEach((credentialParam: any) => { + const inputs = node?.data?.inputs + const hasCredential = + node?.data?.credential || (inputs && (inputs[credentialParam.name] || inputs['FLOWISE_CREDENTIAL_ID'])) + + const credentialNames: string[] = credentialParam?.credentialNames || [] + + credentialNames.forEach((credentialName: string) => { + credentialTypes.add(credentialName) + + const category = getCredentialCategory(node?.data?.category, credentialName) + const isRequired = category.isCore || credentialParam?.optional === false + + credentials.push({ + nodeId: node?.id, + nodeName: node?.data?.name || 'Unknown Node', + nodeCategory: node?.data?.category || 'Unknown', + nodeType: node?.data?.type || node?.type || 'Unknown', + credentialType: credentialName, + parameterName: credentialParam?.name, + label: credentialParam?.label || credentialParam?.name, + isRequired, + isCore: category.isCore, + categoryType: category.type, + categoryDisplayName: category.displayName, + isAssigned: !!hasCredential, + assignedCredentialId: hasCredential || null + }) + }) + }) + }) + + return { + credentials, + hasCredentials: credentialTypes.size > 0 + } + } catch (error) { + console.error('Error in processFlowCredentials:', error) + return { credentials: [], hasCredentials: false } + } +} diff --git a/packages/ui/src/hooks/useFlowCredentials.js b/packages/ui/src/hooks/useFlowCredentials.js new file mode 100644 index 00000000000..dbddaeb4f55 --- /dev/null +++ b/packages/ui/src/hooks/useFlowCredentials.js @@ -0,0 +1,158 @@ +import { useCallback, useRef, useState } from 'react' +import nodesApi from '@/api/nodes' +import { collectFlowCredentials, updateFlowDataWithCredentials } from '@/utils/flowCredentialsHelper' +import { getCredentialModalDismissed, setCredentialModalDismissed } from '@/utils/credentialModalPreference' + +const nodeDefinitionCache = new Map() + +const fetchNodeDefinition = async (nodeName) => { + if (!nodeName) return null + if (nodeDefinitionCache.has(nodeName)) return nodeDefinitionCache.get(nodeName) + try { + const response = await nodesApi.getSpecificNode(nodeName) + const nodeDefinition = response?.data || null + if (nodeDefinition) { + nodeDefinitionCache.set(nodeName, nodeDefinition) + } + return nodeDefinition + } catch (error) { + console.warn(`Failed to fetch node definition for ${nodeName}:`, error) + return null + } +} + +const resetState = (setShowModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) => { + setShowModal(false) + setCredentialList([]) + setAllCredentialsState([]) + setInitialDontShowAgain(false) +} + +const buildCallbackPayload = (flowState, evaluation, mode) => ({ + flowData: flowState?.raw ?? null, + parsedFlow: flowState?.parsed ?? null, + allCredentials: evaluation?.allCredentials ?? [], + missingCredentials: evaluation?.missingCredentials ?? [], + mode +}) + +export const useFlowCredentials = () => { + const [showCredentialModal, setShowCredentialModal] = useState(false) + const [credentialList, setCredentialList] = useState([]) + const [allCredentialsState, setAllCredentialsState] = useState([]) + const [initialDontShowAgain, setInitialDontShowAgain] = useState(false) + + const preferenceScopeRef = useRef(null) + const callbacksRef = useRef({ onAssign: null, onSkip: null, onCancel: null }) + const flowRef = useRef({ raw: null, parsed: null, format: 'object' }) + const evaluationRef = useRef({ allCredentials: [], missingCredentials: [] }) + const modeRef = useRef('missing') + + const openCredentialModal = useCallback(async (flowData, options = {}) => { + const { preferenceScope = null, onAssign, onSkip, onCancel, forceShow = false, mode = 'missing' } = options + + const evaluation = await collectFlowCredentials(flowData, { fetchNodeDefinition }) + evaluationRef.current = evaluation + + const listForModal = mode === 'all' ? evaluation.allCredentials : evaluation.missingCredentials + const hasItems = listForModal.length > 0 + const preferenceDismissed = preferenceScope ? getCredentialModalDismissed(preferenceScope) : false + + if (!forceShow && (!hasItems || preferenceDismissed)) { + return { shown: false, evaluation } + } + + const isStringInput = typeof flowData === 'string' + const parsedFlow = isStringInput ? JSON.parse(flowData) : { ...flowData } + + flowRef.current = { + raw: isStringInput ? flowData : parsedFlow, + parsed: parsedFlow, + format: isStringInput ? 'string' : 'object' + } + + callbacksRef.current = { onAssign, onSkip, onCancel } + preferenceScopeRef.current = preferenceScope + modeRef.current = mode + + setCredentialList(listForModal) + setAllCredentialsState(evaluation.allCredentials) + setInitialDontShowAgain(preferenceScope ? preferenceDismissed : false) + setShowCredentialModal(true) + + return { shown: true, evaluation } + }, []) + + const applyPreference = (options) => { + const scope = preferenceScopeRef.current + if (!scope) return + if (options?.dontShowDirty) { + setCredentialModalDismissed(scope, !!options.dontShowAgain) + } + } + + const handleAssign = useCallback((credentialAssignments, options = {}) => { + applyPreference(options) + + const currentFlow = flowRef.current + const format = currentFlow.format + const source = format === 'string' ? currentFlow.raw : currentFlow.parsed + + let nextRaw = currentFlow.raw + let nextParsed = currentFlow.parsed + + if (credentialAssignments && Object.keys(credentialAssignments).length > 0) { + const updatedFlow = updateFlowDataWithCredentials(source, credentialAssignments) + nextParsed = updatedFlow + nextRaw = format === 'string' ? JSON.stringify(updatedFlow) : updatedFlow + } + + flowRef.current = { + raw: nextRaw, + parsed: nextParsed, + format + } + + callbacksRef.current.onAssign?.( + nextRaw, + credentialAssignments, + buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current) + ) + + resetState(setShowCredentialModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) + preferenceScopeRef.current = null + callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } + }, []) + + const handleSkip = useCallback((options = {}) => { + applyPreference(options) + callbacksRef.current.onSkip?.(flowRef.current.raw, buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current)) + + resetState(setShowCredentialModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) + preferenceScopeRef.current = null + callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } + }, []) + + const handleCancel = useCallback((options = {}) => { + applyPreference(options) + callbacksRef.current.onCancel?.(flowRef.current.raw, buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current)) + + resetState(setShowCredentialModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) + preferenceScopeRef.current = null + callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } + }, []) + + return { + showCredentialModal, + missingCredentials: credentialList, + allCredentials: allCredentialsState, + initialDontShowAgain, + modalMode: modeRef.current, + openCredentialModal, + handleAssign, + handleSkip, + handleCancel + } +} + +export default useFlowCredentials diff --git a/packages/ui/src/utils/flowCredentialsHelper.js b/packages/ui/src/utils/flowCredentialsHelper.js index 0197d08dfde..97f49b59b38 100644 --- a/packages/ui/src/utils/flowCredentialsHelper.js +++ b/packages/ui/src/utils/flowCredentialsHelper.js @@ -5,10 +5,38 @@ import { extractMissingCredentials } from '@utils/extractMissingCredentials' import { extractAllCredentials } from '@utils/extractAllCredentials' import { getCredentialCategory } from '@utils/getCredentialCategory' +import { processFlowCredentials } from '@utils/processFlowCredentials' // Re-export for convenience export { extractMissingCredentials, extractAllCredentials, getCredentialCategory } +/** + * Collect all credential information from flow data (both missing and all credentials) + * This is an async wrapper that combines extractMissingCredentials and extractAllCredentials + * @param {string|object} flowData - Flow data as JSON string or object + * @param {object} options - Optional configuration (e.g., { fetchNodeDefinition }) + * @returns {Promise} Object containing missingCredentials and allCredentials arrays + */ +export const collectFlowCredentials = async (flowData, _options = {}) => { + try { + const { credentials } = processFlowCredentials(flowData) + const missingCredentials = credentials + .filter((credential) => !credential.isAssigned) + .map(({ isAssigned, assignedCredentialId, ...rest }) => rest) + + return { + missingCredentials, + allCredentials: credentials + } + } catch (error) { + console.error('Error in collectFlowCredentials:', error) + return { + missingCredentials: [], + allCredentials: [] + } + } +} + /** * Convert camelCase or PascalCase string to sentence case * @param {string} str - String in camelCase or PascalCase diff --git a/packages/ui/src/views/agentflowsv2/Canvas.jsx b/packages/ui/src/views/agentflowsv2/Canvas.jsx index e09e5ab40d3..d8be5d1f793 100644 --- a/packages/ui/src/views/agentflowsv2/Canvas.jsx +++ b/packages/ui/src/views/agentflowsv2/Canvas.jsx @@ -591,53 +591,72 @@ const AgentflowCanvas = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasDataStore.chatflow]) + // Only show credential modal for flows that have been saved to database (have an ID) + // This prevents modal from showing during template load (before save) useEffect(() => { - const flowId = canvasDataStore.chatflow?.id - const flowData = canvasDataStore.chatflow?.flowData - - if (!flowId || !flowData || hasPromptedCredentialsRef.current) { - return - } + const currentChatflow = canvasDataStore.chatflow + if (!reactFlowInstance) return + if (!currentChatflow?.flowData || !currentChatflow?.id || hasPromptedCredentialsRef.current) return + const canManageFlow = + currentChatflow.canEdit === undefined && currentChatflow.isOwner === undefined + ? true + : currentChatflow.canEdit ?? currentChatflow.isOwner ?? false + if (!canManageFlow) return hasPromptedCredentialsRef.current = true - const preferenceScope = `flow:${flowId}` - openCredentialModal(flowData, { preferenceScope }).catch((error) => console.error('Failed to open credential modal:', error)) - }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) - - // Listen for QuickSetup parameter to open credential modal in "all" mode + const preferenceScope = `flow:${currentChatflow.id}` + openCredentialModal(currentChatflow.flowData, { preferenceScope }).catch((error) => + console.error('Failed to open credential modal:', error) + ) + }, [ + canvasDataStore.chatflow?.flowData, + canvasDataStore.chatflow?.id, + canvasDataStore.chatflow?.canEdit, + canvasDataStore.chatflow?.isOwner, + openCredentialModal, + reactFlowInstance + ]) + + // Handle QuickSetup URL parameter - opens modal in "manage credentials" mode useEffect(() => { - const handleQuickSetup = () => { - const searchParams = new URLSearchParams(window.location.search) - const isQuickSetup = searchParams.get('QuickSetup') === 'true' - - if (isQuickSetup && canvasDataStore.chatflow?.flowData) { - const flowId = canvasDataStore.chatflow.id - const preferenceScope = flowId ? `flow:${flowId}` : null + const checkQuickSetupParam = () => { + const urlParams = new URLSearchParams(window.location.search) + const isQuickSetup = urlParams.get('QuickSetup') === 'true' + + const canManageFlow = + canvasDataStore.chatflow?.canEdit === undefined && canvasDataStore.chatflow?.isOwner === undefined + ? true + : canvasDataStore.chatflow?.canEdit ?? canvasDataStore.chatflow?.isOwner ?? false + + if (isQuickSetup && reactFlowInstance && canvasDataStore.chatflow?.flowData && canvasDataStore.chatflow?.id && canManageFlow) { + // Remove QuickSetup parameter from URL + urlParams.delete('QuickSetup') + const newUrl = `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}` + window.history.replaceState({}, '', newUrl) - // Open modal with all credentials (mode: 'all') and force show + // Open modal in "all credentials" mode with forceShow + const preferenceScope = `flow:${canvasDataStore.chatflow.id}` openCredentialModal(canvasDataStore.chatflow.flowData, { preferenceScope, mode: 'all', forceShow: true - }).catch((error) => console.error('Failed to open credential modal:', error)) - - // Clean up URL parameter - searchParams.delete('QuickSetup') - const newUrl = `${window.location.pathname}${searchParams.toString() ? '?' + searchParams.toString() : ''}` - window.history.replaceState({}, '', newUrl) + }).catch((error) => console.error('Failed to open QuickSetup modal:', error)) } } - // Check immediately on mount - handleQuickSetup() - - // Listen for popstate events (when button triggers history change) - window.addEventListener('popstate', handleQuickSetup) - - return () => { - window.removeEventListener('popstate', handleQuickSetup) - } - }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) + checkQuickSetupParam() + + // Listen for popstate events (triggered by the QuickSetup button) + window.addEventListener('popstate', checkQuickSetupParam) + return () => window.removeEventListener('popstate', checkQuickSetupParam) + }, [ + canvasDataStore.chatflow?.flowData, + canvasDataStore.chatflow?.id, + canvasDataStore.chatflow?.canEdit, + canvasDataStore.chatflow?.isOwner, + openCredentialModal, + reactFlowInstance + ]) // Initialization useEffect(() => { diff --git a/packages/ui/src/views/canvas/index.jsx b/packages/ui/src/views/canvas/index.jsx index f623177bf0f..21d39f07d5c 100644 --- a/packages/ui/src/views/canvas/index.jsx +++ b/packages/ui/src/views/canvas/index.jsx @@ -58,7 +58,7 @@ import { usePrompt } from '@/utils/usePrompt' import { FLOWISE_CREDENTIAL_ID } from '@/store/constant' // credential checking -import { useCredentialChecker } from '@/hooks/useCredentialChecker' +import useFlowCredentials from '@/hooks/useFlowCredentials' import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' const nodeTypes = { customNode: CanvasNode, stickyNote: StickyNote } const edgeTypes = { buttonedge: ButtonEdge } @@ -110,8 +110,9 @@ const Canvas = ({ chatflowid: chatflowId }) => { const reactFlowWrapper = useRef(null) const canvasHeaderRef = useRef(null) - const { showCredentialModal, missingCredentials, checkCredentials, initialDontShowAgain, handleAssign, handleSkip, handleCancel } = - useCredentialChecker() + const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = + useFlowCredentials() + const hasPromptedCredentialsRef = useRef(false) // ==============================|| Chatflow API ||============================== // @@ -226,6 +227,11 @@ const Canvas = ({ chatflowid: chatflowId }) => { flowData: JSON.stringify({ nodes, edges }) } + // Block credential modal from showing until user saves + // Modal should only show AFTER user clicks save + // MUST set this BEFORE dispatch to prevent modal trigger + hasPromptedCredentialsRef.current = true + dispatch({ type: SET_CHATFLOW, chatflow: newChatflow }) setChatflow(newChatflow) setNodes(nodes) @@ -553,6 +559,10 @@ const Canvas = ({ chatflowid: chatflowId }) => { const initialFlow = chatflow.flowData ? JSON.parse(chatflow.flowData) : [] setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) + // Allow modal to check for missing credentials when loading existing flow + // Modal will show immediately if credentials are missing (unless user dismissed it) + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) } else if (getSpecificChatflowApi?.error?.response?.data?.message) { errorFailed(`Failed to save ${canvasTitle}: ${getSpecificChatflowApi.error.response.data.message}`) @@ -568,6 +578,9 @@ const Canvas = ({ chatflowid: chatflowId }) => { if (createNewChatflowApi?.data) { const chatflow = createNewChatflowApi.data // console.log('✅ Create new chatflow successful, navigating to:', `/${isAgentCanvas ? 'agentcanvas' : 'canvas'}/${chatflow.id}`) + // Reset to allow modal to show after first save + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow }) saveChatflowSuccess() navigate(`/${isAgentCanvas ? 'agentcanvas' : 'canvas'}/${chatflow.id}`, { @@ -586,6 +599,9 @@ const Canvas = ({ chatflowid: chatflowId }) => { useEffect(() => { if (updateChatflowApi?.data) { // console.log('✅ Update chatflow successful, NOT navigating (should we?)') + // Reset to allow modal to check for missing credentials after update + // MUST set this BEFORE dispatch to allow modal trigger + hasPromptedCredentialsRef.current = false dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data }) saveChatflowSuccess() } else if (updateChatflowApi?.error?.response?.data?.message) { @@ -608,10 +624,53 @@ const Canvas = ({ chatflowid: chatflowId }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [canvasDataStore.chatflow]) + // Only show credential modal for flows that have been saved to database (have an ID) + // This prevents modal from showing during template load (before save) + useEffect(() => { + if (canvasDataStore.chatflow?.flowData && canvasDataStore.chatflow?.id && !hasPromptedCredentialsRef.current) { + hasPromptedCredentialsRef.current = true + const preferenceScope = canvasDataStore.chatflow?.id ? `flow:${canvasDataStore.chatflow.id}` : null + openCredentialModal(canvasDataStore.chatflow.flowData, { preferenceScope }).catch((error) => + console.error('Failed to open credential modal:', error) + ) + } + }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) + + // Handle QuickSetup URL parameter - opens modal in "manage credentials" mode + useEffect(() => { + const checkQuickSetupParam = () => { + const urlParams = new URLSearchParams(window.location.search) + const isQuickSetup = urlParams.get('QuickSetup') === 'true' + + if (isQuickSetup && canvasDataStore.chatflow?.flowData && canvasDataStore.chatflow?.id) { + // Remove QuickSetup parameter from URL + urlParams.delete('QuickSetup') + const newUrl = `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}` + window.history.replaceState({}, '', newUrl) + + // Open modal in "all credentials" mode with forceShow + const preferenceScope = `flow:${canvasDataStore.chatflow.id}` + openCredentialModal(canvasDataStore.chatflow.flowData, { + preferenceScope, + mode: 'all', + forceShow: true + }).catch((error) => console.error('Failed to open QuickSetup modal:', error)) + } + } + + checkQuickSetupParam() + + // Listen for popstate events (triggered by the QuickSetup button) + window.addEventListener('popstate', checkQuickSetupParam) + return () => window.removeEventListener('popstate', checkQuickSetupParam) + }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) + // Initialization useEffect(() => { // console.log('🎨 Canvas initialization effect running:', { chatflowId }) + // DO NOT reset hasPromptedCredentialsRef here - it prevents duplicate modals + // The ref is managed by the credential modal useEffect and save handlers setIsSyncNodesButtonEnabled(false) setIsUpsertButtonEnabled(false) if (chatflowId) { @@ -641,6 +700,12 @@ const Canvas = ({ chatflowid: chatflowId }) => { deployed: false, isPublic: false } + + // Block credential modal from showing until user saves + // Modal should only show AFTER user clicks save + // MUST set this BEFORE dispatch to prevent modal trigger + hasPromptedCredentialsRef.current = true + setChatflow(newChatflow) dispatch({ type: SET_CHATFLOW, chatflow: newChatflow }) @@ -657,6 +722,9 @@ const Canvas = ({ chatflowid: chatflowId }) => { // console.log('🎨 No duplicated flow data, creating blank canvas') setNodes([]) setEdges([]) + // Block credential modal for blank canvas until user saves + // MUST set this BEFORE dispatch to prevent modal trigger + hasPromptedCredentialsRef.current = true setChatflow({ name: templateName ? `Copy of ${templateName}` : `Untitled ${canvasTitle}` }) @@ -812,7 +880,6 @@ const Canvas = ({ chatflowid: chatflowId }) => { onAssign={handleAssign} onSkip={handleSkip} onCancel={handleCancel} - flowData={null} initialDontShowAgain={initialDontShowAgain} /> diff --git a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx index 317d68e89d2..92595ddf550 100644 --- a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx +++ b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx @@ -15,10 +15,6 @@ import MarketplaceCanvasHeader from './MarketplaceCanvasHeader' import StickyNote from '../canvas/StickyNote' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' -// credential checking -import { useCredentialChecker } from '@/hooks/useCredentialChecker' -import UnifiedCredentialsModal from '@/ui-component/dialog/UnifiedCredentialsModal' - const nodeTypes = { customNode: MarketplaceCanvasNode, stickyNote: StickyNote } const edgeTypes = { buttonedge: '' } @@ -38,10 +34,6 @@ const MarketplaceCanvas = () => { const reactFlowWrapper = useRef(null) - // Credential checking hook - const { showCredentialModal, missingCredentials, checkCredentials, initialDontShowAgain, handleAssign, handleSkip, handleCancel } = - useCredentialChecker() - // ==============================|| useEffect ||============================== // useEffect(() => { @@ -50,8 +42,6 @@ const MarketplaceCanvas = () => { setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) } - - // eslint-disable-next-line react-hooks/exhaustive-deps }, [flowData]) const onChatflowCopy = (stateData) => { @@ -131,17 +121,6 @@ const MarketplaceCanvas = () => { {/* Confirm Dialog */} - - {/* Unified Credentials Modal */} - ) } From b8a0c73212d09fbe23e7a0f28c1ca22732010924 Mon Sep 17 00:00:00 2001 From: Diego Costa Date: Tue, 28 Oct 2025 15:43:43 -0300 Subject: [PATCH 6/7] refactor: improve error handling in flow credential processes - Updated error handling in `useFlowCredentials` and `flowCredentialsHelper` to suppress error logs and return null or default values instead. - Enhanced user experience in `AgentflowCanvas` and `Canvas` components by replacing console error logs with user-friendly error messages. - Cleaned up error handling in `processFlowCredentials` to maintain consistency across credential processing functions. This refactor aims to streamline error management and improve overall application stability. --- packages-answers/utils/src/processFlowCredentials.ts | 3 +-- packages/ui/src/hooks/useFlowCredentials.js | 3 +-- packages/ui/src/utils/flowCredentialsHelper.js | 3 +-- packages/ui/src/views/agentflowsv2/Canvas.jsx | 8 +++----- packages/ui/src/views/canvas/index.jsx | 6 ++---- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages-answers/utils/src/processFlowCredentials.ts b/packages-answers/utils/src/processFlowCredentials.ts index c855ae209d7..a5bbcf445b3 100644 --- a/packages-answers/utils/src/processFlowCredentials.ts +++ b/packages-answers/utils/src/processFlowCredentials.ts @@ -76,8 +76,7 @@ export const processFlowCredentials = (flowData: FlowData): ProcessResult => { credentials, hasCredentials: credentialTypes.size > 0 } - } catch (error) { - console.error('Error in processFlowCredentials:', error) + } catch (_error) { return { credentials: [], hasCredentials: false } } } diff --git a/packages/ui/src/hooks/useFlowCredentials.js b/packages/ui/src/hooks/useFlowCredentials.js index dbddaeb4f55..5fdc6a90838 100644 --- a/packages/ui/src/hooks/useFlowCredentials.js +++ b/packages/ui/src/hooks/useFlowCredentials.js @@ -15,8 +15,7 @@ const fetchNodeDefinition = async (nodeName) => { nodeDefinitionCache.set(nodeName, nodeDefinition) } return nodeDefinition - } catch (error) { - console.warn(`Failed to fetch node definition for ${nodeName}:`, error) + } catch (_error) { return null } } diff --git a/packages/ui/src/utils/flowCredentialsHelper.js b/packages/ui/src/utils/flowCredentialsHelper.js index 97f49b59b38..eeb288f7b1a 100644 --- a/packages/ui/src/utils/flowCredentialsHelper.js +++ b/packages/ui/src/utils/flowCredentialsHelper.js @@ -28,8 +28,7 @@ export const collectFlowCredentials = async (flowData, _options = {}) => { missingCredentials, allCredentials: credentials } - } catch (error) { - console.error('Error in collectFlowCredentials:', error) + } catch (_error) { return { missingCredentials: [], allCredentials: [] diff --git a/packages/ui/src/views/agentflowsv2/Canvas.jsx b/packages/ui/src/views/agentflowsv2/Canvas.jsx index d8be5d1f793..10f0f019088 100644 --- a/packages/ui/src/views/agentflowsv2/Canvas.jsx +++ b/packages/ui/src/views/agentflowsv2/Canvas.jsx @@ -173,7 +173,7 @@ const AgentflowCanvas = () => { // Credential modal will be triggered automatically after save via useEffect // watching canvasDataStore.chatflow.flowData (lines 585-593) } catch (e) { - console.error(e) + errorFailed('Failed to load flow data. Please verify the file content.') } } @@ -605,9 +605,7 @@ const AgentflowCanvas = () => { hasPromptedCredentialsRef.current = true const preferenceScope = `flow:${currentChatflow.id}` - openCredentialModal(currentChatflow.flowData, { preferenceScope }).catch((error) => - console.error('Failed to open credential modal:', error) - ) + openCredentialModal(currentChatflow.flowData, { preferenceScope }).catch(() => {}) }, [ canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, @@ -640,7 +638,7 @@ const AgentflowCanvas = () => { preferenceScope, mode: 'all', forceShow: true - }).catch((error) => console.error('Failed to open QuickSetup modal:', error)) + }).catch(() => {}) } } diff --git a/packages/ui/src/views/canvas/index.jsx b/packages/ui/src/views/canvas/index.jsx index 21d39f07d5c..d54a873b421 100644 --- a/packages/ui/src/views/canvas/index.jsx +++ b/packages/ui/src/views/canvas/index.jsx @@ -630,9 +630,7 @@ const Canvas = ({ chatflowid: chatflowId }) => { if (canvasDataStore.chatflow?.flowData && canvasDataStore.chatflow?.id && !hasPromptedCredentialsRef.current) { hasPromptedCredentialsRef.current = true const preferenceScope = canvasDataStore.chatflow?.id ? `flow:${canvasDataStore.chatflow.id}` : null - openCredentialModal(canvasDataStore.chatflow.flowData, { preferenceScope }).catch((error) => - console.error('Failed to open credential modal:', error) - ) + openCredentialModal(canvasDataStore.chatflow.flowData, { preferenceScope }).catch(() => {}) } }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) @@ -654,7 +652,7 @@ const Canvas = ({ chatflowid: chatflowId }) => { preferenceScope, mode: 'all', forceShow: true - }).catch((error) => console.error('Failed to open QuickSetup modal:', error)) + }).catch(() => {}) } } From db7f6f64c0d88e450cab5c34cf27a1ef86053320 Mon Sep 17 00:00:00 2001 From: Max Techera Date: Wed, 29 Oct 2025 13:25:06 -0300 Subject: [PATCH 7/7] fix: address critical PR review issues in credential modal flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add missing allCredentials and modalMode props to UnifiedCredentialsModal - Remove unused _options parameter from collectFlowCredentials - Add error logging in development mode for debugging - Convert useFlowCredentials to TypeScript with minimal type annotations Fixes critical issue where QuickSetup feature would break due to missing props. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ...owCredentials.js => useFlowCredentials.ts} | 32 +++++++++---------- .../ui/src/utils/flowCredentialsHelper.js | 3 +- packages/ui/src/views/agentflowsv2/Canvas.jsx | 27 +++++++++++++--- packages/ui/src/views/canvas/index.jsx | 27 +++++++++++++--- 4 files changed, 63 insertions(+), 26 deletions(-) rename packages/ui/src/hooks/{useFlowCredentials.js => useFlowCredentials.ts} (82%) diff --git a/packages/ui/src/hooks/useFlowCredentials.js b/packages/ui/src/hooks/useFlowCredentials.ts similarity index 82% rename from packages/ui/src/hooks/useFlowCredentials.js rename to packages/ui/src/hooks/useFlowCredentials.ts index 5fdc6a90838..d038a261938 100644 --- a/packages/ui/src/hooks/useFlowCredentials.js +++ b/packages/ui/src/hooks/useFlowCredentials.ts @@ -3,9 +3,9 @@ import nodesApi from '@/api/nodes' import { collectFlowCredentials, updateFlowDataWithCredentials } from '@/utils/flowCredentialsHelper' import { getCredentialModalDismissed, setCredentialModalDismissed } from '@/utils/credentialModalPreference' -const nodeDefinitionCache = new Map() +const nodeDefinitionCache = new Map() -const fetchNodeDefinition = async (nodeName) => { +const fetchNodeDefinition = async (nodeName: string) => { if (!nodeName) return null if (nodeDefinitionCache.has(nodeName)) return nodeDefinitionCache.get(nodeName) try { @@ -20,14 +20,14 @@ const fetchNodeDefinition = async (nodeName) => { } } -const resetState = (setShowModal, setCredentialList, setAllCredentialsState, setInitialDontShowAgain) => { +const resetState = (setShowModal: any, setCredentialList: any, setAllCredentialsState: any, setInitialDontShowAgain: any) => { setShowModal(false) setCredentialList([]) setAllCredentialsState([]) setInitialDontShowAgain(false) } -const buildCallbackPayload = (flowState, evaluation, mode) => ({ +const buildCallbackPayload = (flowState: any, evaluation: any, mode: string) => ({ flowData: flowState?.raw ?? null, parsedFlow: flowState?.parsed ?? null, allCredentials: evaluation?.allCredentials ?? [], @@ -37,17 +37,17 @@ const buildCallbackPayload = (flowState, evaluation, mode) => ({ export const useFlowCredentials = () => { const [showCredentialModal, setShowCredentialModal] = useState(false) - const [credentialList, setCredentialList] = useState([]) - const [allCredentialsState, setAllCredentialsState] = useState([]) + const [credentialList, setCredentialList] = useState([]) + const [allCredentialsState, setAllCredentialsState] = useState([]) const [initialDontShowAgain, setInitialDontShowAgain] = useState(false) - const preferenceScopeRef = useRef(null) - const callbacksRef = useRef({ onAssign: null, onSkip: null, onCancel: null }) - const flowRef = useRef({ raw: null, parsed: null, format: 'object' }) - const evaluationRef = useRef({ allCredentials: [], missingCredentials: [] }) - const modeRef = useRef('missing') + const preferenceScopeRef = useRef(null) + const callbacksRef = useRef({ onAssign: null, onSkip: null, onCancel: null }) + const flowRef = useRef({ raw: null, parsed: null, format: 'object' }) + const evaluationRef = useRef({ allCredentials: [], missingCredentials: [] }) + const modeRef = useRef('missing') - const openCredentialModal = useCallback(async (flowData, options = {}) => { + const openCredentialModal = useCallback(async (flowData: any, options: any = {}) => { const { preferenceScope = null, onAssign, onSkip, onCancel, forceShow = false, mode = 'missing' } = options const evaluation = await collectFlowCredentials(flowData, { fetchNodeDefinition }) @@ -82,7 +82,7 @@ export const useFlowCredentials = () => { return { shown: true, evaluation } }, []) - const applyPreference = (options) => { + const applyPreference = (options: any) => { const scope = preferenceScopeRef.current if (!scope) return if (options?.dontShowDirty) { @@ -90,7 +90,7 @@ export const useFlowCredentials = () => { } } - const handleAssign = useCallback((credentialAssignments, options = {}) => { + const handleAssign = useCallback((credentialAssignments: any, options: any = {}) => { applyPreference(options) const currentFlow = flowRef.current @@ -123,7 +123,7 @@ export const useFlowCredentials = () => { callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } }, []) - const handleSkip = useCallback((options = {}) => { + const handleSkip = useCallback((options: any = {}) => { applyPreference(options) callbacksRef.current.onSkip?.(flowRef.current.raw, buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current)) @@ -132,7 +132,7 @@ export const useFlowCredentials = () => { callbacksRef.current = { onAssign: null, onSkip: null, onCancel: null } }, []) - const handleCancel = useCallback((options = {}) => { + const handleCancel = useCallback((options: any = {}) => { applyPreference(options) callbacksRef.current.onCancel?.(flowRef.current.raw, buildCallbackPayload(flowRef.current, evaluationRef.current, modeRef.current)) diff --git a/packages/ui/src/utils/flowCredentialsHelper.js b/packages/ui/src/utils/flowCredentialsHelper.js index eeb288f7b1a..eeb9fac4e0e 100644 --- a/packages/ui/src/utils/flowCredentialsHelper.js +++ b/packages/ui/src/utils/flowCredentialsHelper.js @@ -14,10 +14,9 @@ export { extractMissingCredentials, extractAllCredentials, getCredentialCategory * Collect all credential information from flow data (both missing and all credentials) * This is an async wrapper that combines extractMissingCredentials and extractAllCredentials * @param {string|object} flowData - Flow data as JSON string or object - * @param {object} options - Optional configuration (e.g., { fetchNodeDefinition }) * @returns {Promise} Object containing missingCredentials and allCredentials arrays */ -export const collectFlowCredentials = async (flowData, _options = {}) => { +export const collectFlowCredentials = async (flowData) => { try { const { credentials } = processFlowCredentials(flowData) const missingCredentials = credentials diff --git a/packages/ui/src/views/agentflowsv2/Canvas.jsx b/packages/ui/src/views/agentflowsv2/Canvas.jsx index 10f0f019088..873e858b955 100644 --- a/packages/ui/src/views/agentflowsv2/Canvas.jsx +++ b/packages/ui/src/views/agentflowsv2/Canvas.jsx @@ -103,8 +103,17 @@ const AgentflowCanvas = () => { const [editNodeDialogOpen, setEditNodeDialogOpen] = useState(false) const [editNodeDialogProps, setEditNodeDialogProps] = useState({}) - const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = - useFlowCredentials() + const { + showCredentialModal, + missingCredentials, + allCredentials, + modalMode, + initialDontShowAgain, + openCredentialModal, + handleAssign, + handleSkip, + handleCancel + } = useFlowCredentials() const hasPromptedCredentialsRef = useRef(false) const reactFlowWrapper = useRef(null) @@ -605,7 +614,11 @@ const AgentflowCanvas = () => { hasPromptedCredentialsRef.current = true const preferenceScope = `flow:${currentChatflow.id}` - openCredentialModal(currentChatflow.flowData, { preferenceScope }).catch(() => {}) + openCredentialModal(currentChatflow.flowData, { preferenceScope }).catch((error) => { + if (process.env.NODE_ENV === 'development') { + console.error('[Canvas] Failed to open credential modal:', error) + } + }) }, [ canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, @@ -638,7 +651,11 @@ const AgentflowCanvas = () => { preferenceScope, mode: 'all', forceShow: true - }).catch(() => {}) + }).catch((error) => { + if (process.env.NODE_ENV === 'development') { + console.error('[Canvas] Failed to open credential modal (QuickSetup):', error) + } + }) } } @@ -879,6 +896,8 @@ const AgentflowCanvas = () => { { const reactFlowWrapper = useRef(null) const canvasHeaderRef = useRef(null) - const { showCredentialModal, missingCredentials, initialDontShowAgain, openCredentialModal, handleAssign, handleSkip, handleCancel } = - useFlowCredentials() + const { + showCredentialModal, + missingCredentials, + allCredentials, + modalMode, + initialDontShowAgain, + openCredentialModal, + handleAssign, + handleSkip, + handleCancel + } = useFlowCredentials() const hasPromptedCredentialsRef = useRef(false) // ==============================|| Chatflow API ||============================== // @@ -630,7 +639,11 @@ const Canvas = ({ chatflowid: chatflowId }) => { if (canvasDataStore.chatflow?.flowData && canvasDataStore.chatflow?.id && !hasPromptedCredentialsRef.current) { hasPromptedCredentialsRef.current = true const preferenceScope = canvasDataStore.chatflow?.id ? `flow:${canvasDataStore.chatflow.id}` : null - openCredentialModal(canvasDataStore.chatflow.flowData, { preferenceScope }).catch(() => {}) + openCredentialModal(canvasDataStore.chatflow.flowData, { preferenceScope }).catch((error) => { + if (process.env.NODE_ENV === 'development') { + console.error('[Canvas] Failed to open credential modal:', error) + } + }) } }, [canvasDataStore.chatflow?.flowData, canvasDataStore.chatflow?.id, openCredentialModal]) @@ -652,7 +665,11 @@ const Canvas = ({ chatflowid: chatflowId }) => { preferenceScope, mode: 'all', forceShow: true - }).catch(() => {}) + }).catch((error) => { + if (process.env.NODE_ENV === 'development') { + console.error('[Canvas] Failed to open credential modal (QuickSetup):', error) + } + }) } } @@ -875,6 +892,8 @@ const Canvas = ({ chatflowid: chatflowId }) => {