diff --git a/web/app/components/plugins/card/index.spec.tsx b/web/app/components/plugins/card/index.spec.tsx
index fc100a08e0..8c51ef666b 100644
--- a/web/app/components/plugins/card/index.spec.tsx
+++ b/web/app/components/plugins/card/index.spec.tsx
@@ -998,58 +998,59 @@ describe('Icon', () => {
render()
expect(screen.getByTestId('app-icon')).toBeInTheDocument()
- it('should not render status indicators when src is object with installed=true', () => {
- render()
+ })
- // Status indicators should not render for object src
- expect(screen.queryByTestId('ri-check-line')).not.toBeInTheDocument()
- })
+ it('should not render status indicators when src is object with installed=true', () => {
+ render()
- it('should not render status indicators when src is object with installFailed=true', () => {
- render()
+ // Status indicators should not render for object src
+ expect(screen.queryByTestId('ri-check-line')).not.toBeInTheDocument()
+ })
- // Status indicators should not render for object src
- expect(screen.queryByTestId('ri-close-line')).not.toBeInTheDocument()
- })
+ it('should not render status indicators when src is object with installFailed=true', () => {
+ render()
- it('should render object src with all size variants', () => {
- const sizes: Array<'xs' | 'tiny' | 'small' | 'medium' | 'large'> = ['xs', 'tiny', 'small', 'medium', 'large']
+ // Status indicators should not render for object src
+ expect(screen.queryByTestId('ri-close-line')).not.toBeInTheDocument()
+ })
- sizes.forEach((size) => {
- const { unmount } = render()
- expect(screen.getByTestId('app-icon')).toHaveAttribute('data-size', size)
- unmount()
- })
- })
+ it('should render object src with all size variants', () => {
+ const sizes: Array<'xs' | 'tiny' | 'small' | 'medium' | 'large'> = ['xs', 'tiny', 'small', 'medium', 'large']
- it('should render object src with custom className', () => {
- const { container } = render(
- ,
- )
-
- expect(container.querySelector('.custom-object-icon')).toBeInTheDocument()
- })
-
- it('should pass correct props to AppIcon for object src', () => {
- render()
-
- const appIcon = screen.getByTestId('app-icon')
- expect(appIcon).toHaveAttribute('data-icon', '😀')
- expect(appIcon).toHaveAttribute('data-background', '#123456')
- expect(appIcon).toHaveAttribute('data-icon-type', 'emoji')
- })
-
- it('should render inner icon only when shouldUseMcpIcon returns true', () => {
- // Test with MCP icon content
- const { unmount } = render()
- expect(screen.getByTestId('inner-icon')).toBeInTheDocument()
+ sizes.forEach((size) => {
+ const { unmount } = render()
+ expect(screen.getByTestId('app-icon')).toHaveAttribute('data-size', size)
unmount()
-
- // Test without MCP icon content
- render()
- expect(screen.queryByTestId('inner-icon')).not.toBeInTheDocument()
})
})
+
+ it('should render object src with custom className', () => {
+ const { container } = render(
+ ,
+ )
+
+ expect(container.querySelector('.custom-object-icon')).toBeInTheDocument()
+ })
+
+ it('should pass correct props to AppIcon for object src', () => {
+ render()
+
+ const appIcon = screen.getByTestId('app-icon')
+ expect(appIcon).toHaveAttribute('data-icon', '😀')
+ expect(appIcon).toHaveAttribute('data-background', '#123456')
+ expect(appIcon).toHaveAttribute('data-icon-type', 'emoji')
+ })
+
+ it('should render inner icon only when shouldUseMcpIcon returns true', () => {
+ // Test with MCP icon content
+ const { unmount } = render()
+ expect(screen.getByTestId('inner-icon')).toBeInTheDocument()
+ unmount()
+
+ // Test without MCP icon content
+ render()
+ expect(screen.queryByTestId('inner-icon')).not.toBeInTheDocument()
+ })
})
// ================================
diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/index.spec.tsx
index 415e7bf80c..fd66e7c45e 100644
--- a/web/app/components/plugins/plugin-detail-panel/app-selector/index.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/app-selector/index.spec.tsx
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react'
import type { App } from '@/types/app'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, screen } from '@testing-library/react'
+import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { InputVarType } from '@/app/components/workflow/types'
import { AppModeEnum } from '@/types/app'
@@ -9,6 +10,7 @@ import AppInputsForm from './app-inputs-form'
import AppInputsPanel from './app-inputs-panel'
import AppPicker from './app-picker'
import AppTrigger from './app-trigger'
+
import AppSelector from './index'
// ==================== Mock Setup ====================
@@ -73,44 +75,59 @@ afterAll(() => {
})
// Mock portal components for controlled positioning in tests
-let mockPortalOpenState = false
+// Use React context to properly scope open state per portal instance (for nested portals)
+const _PortalOpenContext = React.createContext(false)
-vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
- PortalToFollowElem: ({
- children,
- open,
- }: {
- children: ReactNode
- open?: boolean
- }) => {
- mockPortalOpenState = open || false
- return (
-
+vi.mock('@/app/components/base/portal-to-follow-elem', () => {
+ // Context reference shared across mock components
+ let sharedContext: React.Context
| null = null
+
+ // Lazily get or create the context
+ const getContext = (): React.Context => {
+ if (!sharedContext)
+ sharedContext = React.createContext(false)
+ return sharedContext
+ }
+
+ return {
+ PortalToFollowElem: ({
+ children,
+ open,
+ }: {
+ children: ReactNode
+ open?: boolean
+ }) => {
+ const Context = getContext()
+ return React.createElement(
+ Context.Provider,
+ { value: open || false },
+ React.createElement('div', { 'data-testid': 'portal-to-follow-elem', 'data-open': open }, children),
+ )
+ },
+ PortalToFollowElemTrigger: ({
+ children,
+ onClick,
+ className,
+ }: {
+ children: ReactNode
+ onClick?: () => void
+ className?: string
+ }) => (
+
{children}
- )
- },
- PortalToFollowElemTrigger: ({
- children,
- onClick,
- className,
- }: {
- children: ReactNode
- onClick?: () => void
- className?: string
- }) => (
-
- {children}
-
- ),
- PortalToFollowElemContent: ({ children, className }: { children: ReactNode, className?: string }) => {
- if (!mockPortalOpenState)
- return null
- return (
- {children}
- )
- },
-}))
+ ),
+ PortalToFollowElemContent: ({ children, className }: { children: ReactNode, className?: string }) => {
+ const Context = getContext()
+ const isOpen = React.useContext(Context)
+ if (!isOpen)
+ return null
+ return (
+ {children}
+ )
+ },
+ }
+})
// Mock service hooks
let mockAppListData: { pages: Array<{ data: App[], has_more: boolean, page: number }> } | undefined
@@ -129,10 +146,12 @@ const getAppDetailData = (appId: string) => {
return mockAppDetailData
if (!appId)
return undefined
+ // Extract number from appId (e.g., 'app-1' -> '1') for consistent naming with createMockApps
+ const appNumber = appId.replace('app-', '')
// Return a basic mock app structure
return {
id: appId,
- name: `App ${appId}`,
+ name: `App ${appNumber}`,
mode: 'chat',
icon_type: 'emoji',
icon: '🤖',
@@ -345,20 +364,25 @@ const createMockApp = (overrides: Record = {}): App => ({
...overrides,
} as unknown as App)
+// Helper function to get app mode based on index
+const getAppModeByIndex = (index: number): AppModeEnum => {
+ if (index % 5 === 0)
+ return AppModeEnum.ADVANCED_CHAT
+ if (index % 4 === 0)
+ return AppModeEnum.AGENT_CHAT
+ if (index % 3 === 0)
+ return AppModeEnum.WORKFLOW
+ if (index % 2 === 0)
+ return AppModeEnum.COMPLETION
+ return AppModeEnum.CHAT
+}
+
const createMockApps = (count: number): App[] => {
return Array.from({ length: count }, (_, i) =>
createMockApp({
id: `app-${i + 1}`,
name: `App ${i + 1}`,
- mode: i % 5 === 0
- ? AppModeEnum.ADVANCED_CHAT
- : i % 4 === 0
- ? AppModeEnum.AGENT_CHAT
- : i % 3 === 0
- ? AppModeEnum.WORKFLOW
- : i % 2 === 0
- ? AppModeEnum.COMPLETION
- : AppModeEnum.CHAT,
+ mode: getAppModeByIndex(i),
}))
}
@@ -446,7 +470,6 @@ describe('AppPicker', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
- mockPortalOpenState = false
})
afterEach(() => {
@@ -460,14 +483,12 @@ describe('AppPicker', () => {
})
it('should render app list when open', () => {
- mockPortalOpenState = true
render()
expect(screen.getByText('App 1')).toBeInTheDocument()
expect(screen.getByText('App 2')).toBeInTheDocument()
})
it('should show loading indicator when isLoading is true', () => {
- mockPortalOpenState = true
render()
expect(screen.getByText('common.loading')).toBeInTheDocument()
})
@@ -480,7 +501,6 @@ describe('AppPicker', () => {
describe('User Interactions', () => {
it('should call onSelect when app is clicked', () => {
- mockPortalOpenState = true
const onSelect = vi.fn()
render()
@@ -489,7 +509,6 @@ describe('AppPicker', () => {
})
it('should call onSearchChange when typing in search input', () => {
- mockPortalOpenState = true
const onSearchChange = vi.fn()
render()
@@ -517,35 +536,30 @@ describe('AppPicker', () => {
describe('App Type Display', () => {
it('should display correct app type for CHAT', () => {
- mockPortalOpenState = true
const apps = [createMockApp({ id: 'chat-app', name: 'Chat App', mode: AppModeEnum.CHAT })]
render()
expect(screen.getByText('chat')).toBeInTheDocument()
})
it('should display correct app type for WORKFLOW', () => {
- mockPortalOpenState = true
const apps = [createMockApp({ id: 'workflow-app', name: 'Workflow App', mode: AppModeEnum.WORKFLOW })]
render()
expect(screen.getByText('workflow')).toBeInTheDocument()
})
it('should display correct app type for ADVANCED_CHAT', () => {
- mockPortalOpenState = true
const apps = [createMockApp({ id: 'chatflow-app', name: 'Chatflow App', mode: AppModeEnum.ADVANCED_CHAT })]
render()
expect(screen.getByText('chatflow')).toBeInTheDocument()
})
it('should display correct app type for AGENT_CHAT', () => {
- mockPortalOpenState = true
const apps = [createMockApp({ id: 'agent-app', name: 'Agent App', mode: AppModeEnum.AGENT_CHAT })]
render()
expect(screen.getByText('agent')).toBeInTheDocument()
})
it('should display correct app type for COMPLETION', () => {
- mockPortalOpenState = true
const apps = [createMockApp({ id: 'completion-app', name: 'Completion App', mode: AppModeEnum.COMPLETION })]
render()
expect(screen.getByText('completion')).toBeInTheDocument()
@@ -554,13 +568,11 @@ describe('AppPicker', () => {
describe('Edge Cases', () => {
it('should handle empty apps array', () => {
- mockPortalOpenState = true
render()
expect(screen.queryByRole('listitem')).not.toBeInTheDocument()
})
it('should handle search text with value', () => {
- mockPortalOpenState = true
render()
const input = screen.getByTestId('input')
expect(input).toHaveValue('test search')
@@ -569,7 +581,6 @@ describe('AppPicker', () => {
describe('Search Clear', () => {
it('should call onSearchChange with empty string when clear button is clicked', () => {
- mockPortalOpenState = true
const onSearchChange = vi.fn()
render()
@@ -581,7 +592,6 @@ describe('AppPicker', () => {
describe('Infinite Scroll', () => {
it('should not call onLoadMore when isLoading is true', () => {
- mockPortalOpenState = true
const onLoadMore = vi.fn()
render()
@@ -594,7 +604,6 @@ describe('AppPicker', () => {
})
it('should not call onLoadMore when hasMore is false', () => {
- mockPortalOpenState = true
const onLoadMore = vi.fn()
render()
@@ -607,7 +616,6 @@ describe('AppPicker', () => {
})
it('should call onLoadMore when intersection observer fires and conditions are met', () => {
- mockPortalOpenState = true
const onLoadMore = vi.fn()
render()
@@ -619,7 +627,6 @@ describe('AppPicker', () => {
})
it('should not call onLoadMore when target is not intersecting', () => {
- mockPortalOpenState = true
const onLoadMore = vi.fn()
render()
@@ -631,7 +638,6 @@ describe('AppPicker', () => {
})
it('should handle observer target ref', () => {
- mockPortalOpenState = true
render()
// The component should render without errors
@@ -642,11 +648,9 @@ describe('AppPicker', () => {
const { rerender } = render()
// Change isShow to true
- mockPortalOpenState = true
rerender()
// Then back to false
- mockPortalOpenState = false
rerender()
// Should not crash
@@ -654,8 +658,6 @@ describe('AppPicker', () => {
})
it('should setup intersection observer when isShow is true', () => {
- mockPortalOpenState = true
-
render()
// IntersectionObserver callback should have been set
@@ -663,14 +665,12 @@ describe('AppPicker', () => {
})
it('should disconnect observer when isShow changes from true to false', () => {
- mockPortalOpenState = true
const { rerender } = render()
// Verify observer was set up
expect(intersectionObserverCallback).not.toBeNull()
// Change to not shown - should disconnect observer (lines 74-75)
- mockPortalOpenState = false
rerender()
// Component should render without errors
@@ -678,7 +678,6 @@ describe('AppPicker', () => {
})
it('should cleanup observer on component unmount', () => {
- mockPortalOpenState = true
const { unmount } = render()
// Unmount should trigger cleanup without throwing
@@ -686,8 +685,6 @@ describe('AppPicker', () => {
})
it('should handle MutationObserver callback when target becomes available', () => {
- mockPortalOpenState = true
-
render()
// Trigger MutationObserver callback (simulates DOM change)
@@ -699,8 +696,6 @@ describe('AppPicker', () => {
it('should not setup IntersectionObserver when observerTarget is null', () => {
// When isShow is false, the observer target won't be in the DOM
- mockPortalOpenState = false
-
render()
// The guard at line 84 should prevent setup
@@ -708,7 +703,6 @@ describe('AppPicker', () => {
})
it('should debounce onLoadMore calls using loadingRef', () => {
- mockPortalOpenState = true
const onLoadMore = vi.fn()
render()
@@ -1430,7 +1424,6 @@ describe('AppSelector', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
- mockPortalOpenState = false
mockAppListData = {
pages: [{ data: createMockApps(5), has_more: false, page: 1 }],
}
@@ -2095,7 +2088,6 @@ describe('AppSelector Integration', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
- mockPortalOpenState = false
mockAppListData = {
pages: [{ data: createMockApps(5), has_more: false, page: 1 }],
}
diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx
index 40b0ba9205..5d0fa6d4b8 100644
--- a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx
@@ -23,8 +23,8 @@ const PAGE_SIZE = 20
type Props = {
value?: {
app_id: string
- inputs: Record
- files?: any[]
+ inputs: Record
+ files?: unknown[]
}
scope?: string
disabled?: boolean
@@ -32,8 +32,8 @@ type Props = {
offset?: OffsetOptions
onSelect: (app: {
app_id: string
- inputs: Record
- files?: any[]
+ inputs: Record
+ files?: unknown[]
}) => void
supportAddCustomTool?: boolean
}
@@ -63,12 +63,12 @@ const AppSelector: FC = ({
name: searchText,
})
- const pages = data?.pages ?? []
const displayedApps = useMemo(() => {
+ const pages = data?.pages ?? []
if (!pages.length)
return []
return pages.flatMap(({ data: apps }) => apps)
- }, [pages])
+ }, [data?.pages])
// fetch selected app by id to avoid pagination gaps
const { data: selectedAppDetail } = useAppDetail(value?.app_id || '')
@@ -130,7 +130,7 @@ const AppSelector: FC = ({
setIsShowChooseApp(false)
}
- const handleFormChange = (inputs: Record) => {
+ const handleFormChange = (inputs: Record) => {
const newFiles = inputs['#image#']
delete inputs['#image#']
const newValue = {
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx
index 0cee3a2111..6ffb8756d3 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx
@@ -6,6 +6,7 @@ import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import type {
NodeOutPutVar,
ValueSelector,
+ Var,
} from '@/app/components/workflow/types'
import {
RiArrowRightUpLine,
@@ -34,9 +35,21 @@ import { VarType } from '@/app/components/workflow/types'
import { cn } from '@/utils/classnames'
import SchemaModal from './schema-modal'
+type ReasoningConfigInputValue = {
+ type?: VarKindType
+ value?: unknown
+} | null
+
+type ReasoningConfigInput = {
+ value: ReasoningConfigInputValue
+ auto?: 0 | 1
+}
+
+export type ReasoningConfigValue = Record
+
type Props = {
- value: Record
- onChange: (val: Record) => void
+ value: ReasoningConfigValue
+ onChange: (val: ReasoningConfigValue) => void
schemas: ToolFormSchema[]
nodeOutputVars: NodeOutPutVar[]
availableNodes: Node[]
@@ -62,7 +75,7 @@ const ReasoningConfigForm: React.FC = ({
return VarKindType.mixed
}
- const handleAutomatic = (key: string, val: any, type: string) => {
+ const handleAutomatic = (key: string, val: boolean, type: string) => {
onChange({
...value,
[key]: {
@@ -71,7 +84,7 @@ const ReasoningConfigForm: React.FC = ({
},
})
}
- const handleTypeChange = useCallback((variable: string, defaultValue: any) => {
+ const handleTypeChange = useCallback((variable: string, defaultValue: unknown) => {
return (newType: VarKindType) => {
const res = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
@@ -83,7 +96,7 @@ const ReasoningConfigForm: React.FC = ({
}
}, [onChange, value])
const handleValueChange = useCallback((variable: string, varType: string) => {
- return (newValue: any) => {
+ return (newValue: unknown) => {
const res = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: getVarKindType(varType),
@@ -96,22 +109,23 @@ const ReasoningConfigForm: React.FC = ({
const handleAppChange = useCallback((variable: string) => {
return (app: {
app_id: string
- inputs: Record
- files?: any[]
+ inputs: Record
+ files?: unknown[]
}) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
- draft[variable].value = app as any
+ draft[variable].value = app
})
onChange(newValue)
}
}, [onChange, value])
const handleModelChange = useCallback((variable: string) => {
- return (model: any) => {
+ return (model: Record) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
+ const currentValue = draft[variable].value as Record | undefined
draft[variable].value = {
- ...draft[variable].value,
+ ...currentValue,
...model,
- } as any
+ }
})
onChange(newValue)
}
@@ -196,17 +210,17 @@ const ReasoningConfigForm: React.FC = ({
}
const getFilterVar = () => {
if (isNumber)
- return (varPayload: any) => varPayload.type === VarType.number
+ return (varPayload: Var) => varPayload.type === VarType.number
else if (isString)
- return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
+ return (varPayload: Var) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
else if (isFile)
- return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
+ return (varPayload: Var) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
else if (isBoolean)
- return (varPayload: any) => varPayload.type === VarType.boolean
+ return (varPayload: Var) => varPayload.type === VarType.boolean
else if (isObject)
- return (varPayload: any) => varPayload.type === VarType.object
+ return (varPayload: Var) => varPayload.type === VarType.object
else if (isArray)
- return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
+ return (varPayload: Var) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
return undefined
}
@@ -266,7 +280,7 @@ const ReasoningConfigForm: React.FC = ({
handleValueChange(variable, type)(e.target.value)}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
@@ -280,10 +294,10 @@ const ReasoningConfigForm: React.FC = ({
{isSelect && options && (
{
if (option.show_on.length)
- return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
+ return option.show_on.every(showOnItem => value[showOnItem.variable]?.value?.value === showOnItem.value)
return true
}).map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
@@ -295,7 +309,7 @@ const ReasoningConfigForm: React.FC = ({
= ({
, files?: unknown[] } | undefined}
onSelect={handleAppChange(variable)}
/>
)}
@@ -331,7 +345,7 @@ const ReasoningConfigForm: React.FC = ({
readonly={false}
isShowNodeName
nodeId={nodeId}
- value={varInput?.value || []}
+ value={(varInput?.value as string | ValueSelector) || []}
onChange={handleVariableSelectorChange(variable)}
filterVar={getFilterVar()}
schema={schema as Partial}
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-credentials-form.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-credentials-form.tsx
index 5277cebae7..0207f65336 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-credentials-form.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-credentials-form.tsx
@@ -1,6 +1,7 @@
'use client'
import type { FC } from 'react'
import type { Collection } from '@/app/components/tools/types'
+import type { ToolCredentialFormSchema } from '@/app/components/tools/utils/to-form-schema'
import {
RiArrowRightUpLine,
} from '@remixicon/react'
@@ -19,7 +20,7 @@ import { cn } from '@/utils/classnames'
type Props = {
collection: Collection
onCancel: () => void
- onSaved: (value: Record) => void
+ onSaved: (value: Record) => void
}
const ToolCredentialForm: FC = ({
@@ -29,9 +30,9 @@ const ToolCredentialForm: FC = ({
}) => {
const getValueFromI18nObject = useRenderI18nObject()
const { t } = useTranslation()
- const [credentialSchema, setCredentialSchema] = useState(null)
+ const [credentialSchema, setCredentialSchema] = useState(null)
const { name: collectionName } = collection
- const [tempCredential, setTempCredential] = React.useState({})
+ const [tempCredential, setTempCredential] = React.useState>({})
useEffect(() => {
fetchBuiltInToolCredentialSchema(collectionName).then(async (res) => {
const toolCredentialSchemas = toolCredentialToFormSchemas(res)
@@ -44,6 +45,8 @@ const ToolCredentialForm: FC = ({
}, [])
const handleSave = () => {
+ if (!credentialSchema)
+ return
for (const field of credentialSchema) {
if (field.required && !tempCredential[field.name]) {
Toast.notify({ type: 'error', message: t('errorMsg.fieldRequired', { ns: 'common', field: getValueFromI18nObject(field.label) }) })
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx
index 995175c5ea..dd85bc376c 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx
@@ -22,7 +22,7 @@ import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/compo
import { cn } from '@/utils/classnames'
type Props = {
- icon?: any
+ icon?: string | { content?: string, background?: string }
providerName?: string
isMCPTool?: boolean
providerShowName?: string
@@ -33,7 +33,7 @@ type Props = {
onDelete?: () => void
noAuth?: boolean
isError?: boolean
- errorTip?: any
+ errorTip?: React.ReactNode
uninstalled?: boolean
installInfo?: string
onInstall?: () => void
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-settings-panel.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-settings-panel.tsx
index 9144551a48..015b40d9fd 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-settings-panel.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-settings-panel.tsx
@@ -2,9 +2,11 @@
import type { FC } from 'react'
import type { Node } from 'reactflow'
import type { TabType } from '../hooks/use-tool-selector-state'
+import type { ReasoningConfigValue } from './reasoning-config-form'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ToolFormSchema } from '@/app/components/tools/utils/to-form-schema'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
+import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import type { NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
@@ -19,15 +21,15 @@ type ToolSettingsPanelProps = {
currType: TabType
settingsFormSchemas: ToolFormSchema[]
paramsFormSchemas: ToolFormSchema[]
- settingsValue: Record
+ settingsValue: ToolVarInputs
showTabSlider: boolean
userSettingsOnly: boolean
reasoningConfigOnly: boolean
nodeOutputVars: NodeOutPutVar[]
availableNodes: Node[]
onCurrTypeChange: (type: TabType) => void
- onSettingsFormChange: (v: Record) => void
- onParamsFormChange: (v: Record) => void
+ onSettingsFormChange: (v: ToolVarInputs) => void
+ onParamsFormChange: (v: ReasoningConfigValue) => void
}
/**
@@ -140,7 +142,7 @@ const ToolSettingsPanel: FC = ({
{/* Reasoning config form */}
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
{
const settingValues = generateFormValue(
tool.params,
- toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any),
+ toolParametersToFormSchemas((tool.paramSchemas as ToolParameter[]).filter(param => param.form !== 'llm')),
)
const paramValues = generateFormValue(
tool.params,
- toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any),
+ toolParametersToFormSchemas((tool.paramSchemas as ToolParameter[]).filter(param => param.form === 'llm')),
true,
)
return {
@@ -152,7 +155,7 @@ export const useToolSelectorState = ({
})
}, [value, onSelect])
- const handleSettingsFormChange = useCallback((v: Record) => {
+ const handleSettingsFormChange = useCallback((v: ResourceVarInputs) => {
if (!value)
return
const newValue = getStructureValue(v)
@@ -162,7 +165,7 @@ export const useToolSelectorState = ({
})
}, [value, onSelect])
- const handleParamsFormChange = useCallback((v: Record) => {
+ const handleParamsFormChange = useCallback((v: ReasoningConfigValue) => {
if (!value)
return
onSelect({
@@ -204,8 +207,8 @@ export const useToolSelectorState = ({
}
}, [invalidateAllBuiltinTools, invalidateInstalledPluginList])
- const getSettingsValue = useCallback(() => {
- return getPlainValue(value?.settings || {})
+ const getSettingsValue = useCallback((): ResourceVarInputs => {
+ return getPlainValue((value?.settings || {}) as Record) as ResourceVarInputs
}, [value?.settings])
return {
diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.spec.tsx
index 96ca1ed56d..f4ed1bcae5 100644
--- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.spec.tsx
@@ -8,6 +8,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CollectionType } from '@/app/components/tools/types'
+import { VarKindType } from '@/app/components/workflow/nodes/_base/types'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import {
SchemaModal,
@@ -556,7 +557,7 @@ describe('useToolSelectorState Hook', () => {
)
act(() => {
- result.current.handleSettingsFormChange({ key: 'value' })
+ result.current.handleSettingsFormChange({ key: { type: VarKindType.constant, value: 'value' } })
})
expect(onSelect).toHaveBeenCalledWith(
@@ -575,11 +576,11 @@ describe('useToolSelectorState Hook', () => {
)
act(() => {
- result.current.handleParamsFormChange({ param: 'value' })
+ result.current.handleParamsFormChange({ param: { value: { type: VarKindType.constant, value: 'value' } } })
})
expect(onSelect).toHaveBeenCalledWith(
- expect.objectContaining({ parameters: { param: 'value' } }),
+ expect.objectContaining({ parameters: { param: { value: { type: VarKindType.constant, value: 'value' } } } }),
)
})
diff --git a/web/app/components/tools/utils/to-form-schema.ts b/web/app/components/tools/utils/to-form-schema.ts
index ab8363dc9f..4171590375 100644
--- a/web/app/components/tools/utils/to-form-schema.ts
+++ b/web/app/components/tools/utils/to-form-schema.ts
@@ -5,6 +5,35 @@ import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
+// Type for form value input with type and value properties
+type FormValueInput = {
+ type?: string
+ value?: unknown
+}
+
+/**
+ * Form schema type for tool credentials.
+ * This type represents the schema returned by toolCredentialToFormSchemas.
+ */
+export type ToolCredentialFormSchema = {
+ name: string
+ variable: string
+ label: TypeWithI18N
+ type: string
+ required: boolean
+ default?: string
+ tooltip?: TypeWithI18N
+ placeholder?: TypeWithI18N
+ show_on: { variable: string, value: string }[]
+ options?: {
+ label: TypeWithI18N
+ value: string
+ show_on: { variable: string, value: string }[]
+ }[]
+ help?: TypeWithI18N | null
+ url?: string
+}
+
/**
* Form schema type for tool parameters.
* This type represents the schema returned by toolParametersToFormSchemas.
@@ -86,17 +115,17 @@ export const toolParametersToFormSchemas = (parameters: ToolParameter[]): ToolFo
return formSchemas
}
-export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => {
+export const toolCredentialToFormSchemas = (parameters: ToolCredential[]): ToolCredentialFormSchema[] => {
if (!parameters)
return []
- const formSchemas = parameters.map((parameter) => {
+ const formSchemas = parameters.map((parameter): ToolCredentialFormSchema => {
return {
...parameter,
variable: parameter.name,
type: toType(parameter.type),
label: parameter.label,
- tooltip: parameter.help,
+ tooltip: parameter.help ?? undefined,
show_on: [],
options: parameter.options?.map((option) => {
return {
@@ -109,7 +138,7 @@ export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => {
return formSchemas
}
-export const addDefaultValue = (value: Record, formSchemas: { variable: string, type: string, default?: any }[]) => {
+export const addDefaultValue = (value: Record, formSchemas: { variable: string, type: string, default?: unknown }[]) => {
const newValues = { ...value }
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
@@ -129,7 +158,7 @@ export const addDefaultValue = (value: Record, formSchemas: { varia
return newValues
}
-const correctInitialData = (type: string, target: any, defaultValue: any) => {
+const correctInitialData = (type: string, target: FormValueInput, defaultValue: unknown): FormValueInput => {
if (type === 'text-input' || type === 'secret-input')
target.type = 'mixed'
@@ -155,39 +184,39 @@ const correctInitialData = (type: string, target: any, defaultValue: any) => {
return target
}
-export const generateFormValue = (value: Record, formSchemas: { variable: string, default?: any, type: string }[], isReasoning = false) => {
- const newValues = {} as any
+export const generateFormValue = (value: Record, formSchemas: { variable: string, default?: unknown, type: string }[], isReasoning = false) => {
+ const newValues: Record = {}
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
- const value = formSchema.default
- newValues[formSchema.variable] = {
- value: {
- type: 'constant',
- value: formSchema.default,
- },
- ...(isReasoning ? { auto: 1, value: null } : {}),
+ const defaultVal = formSchema.default
+ if (isReasoning) {
+ newValues[formSchema.variable] = { auto: 1, value: null }
+ }
+ else {
+ const initialValue: FormValueInput = { type: 'constant', value: formSchema.default }
+ newValues[formSchema.variable] = {
+ value: correctInitialData(formSchema.type, initialValue, defaultVal),
+ }
}
- if (!isReasoning)
- newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, value)
}
})
return newValues
}
-export const getPlainValue = (value: Record) => {
- const plainValue = { ...value }
- Object.keys(plainValue).forEach((key) => {
+export const getPlainValue = (value: Record) => {
+ const plainValue: Record = {}
+ Object.keys(value).forEach((key) => {
plainValue[key] = {
- ...value[key].value,
+ ...(value[key].value as object),
}
})
return plainValue
}
-export const getStructureValue = (value: Record) => {
- const newValue = { ...value } as any
- Object.keys(newValue).forEach((key) => {
+export const getStructureValue = (value: Record): Record => {
+ const newValue: Record = {}
+ Object.keys(value).forEach((key) => {
newValue[key] = {
value: value[key],
}
@@ -195,17 +224,17 @@ export const getStructureValue = (value: Record) => {
return newValue
}
-export const getConfiguredValue = (value: Record, formSchemas: { variable: string, type: string, default?: any }[]) => {
- const newValues = { ...value }
+export const getConfiguredValue = (value: Record, formSchemas: { variable: string, type: string, default?: unknown }[]) => {
+ const newValues: Record = { ...value }
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
- const value = formSchema.default
- newValues[formSchema.variable] = {
+ const defaultVal = formSchema.default
+ const initialValue: FormValueInput = {
type: 'constant',
value: typeof formSchema.default === 'string' ? formSchema.default.replace(/\n/g, '\\n') : formSchema.default,
}
- newValues[formSchema.variable] = correctInitialData(formSchema.type, newValues[formSchema.variable], value)
+ newValues[formSchema.variable] = correctInitialData(formSchema.type, initialValue, defaultVal)
}
})
return newValues
@@ -220,24 +249,24 @@ const getVarKindType = (type: FormTypeEnum) => {
return VarKindType.mixed
}
-export const generateAgentToolValue = (value: Record, formSchemas: { variable: string, default?: any, type: string }[], isReasoning = false) => {
- const newValues = {} as any
+export const generateAgentToolValue = (value: Record, formSchemas: { variable: string, default?: unknown, type: string }[], isReasoning = false) => {
+ const newValues: Record = {}
if (!isReasoning) {
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
newValues[formSchema.variable] = {
value: {
type: 'constant',
- value: itemValue.value,
+ value: itemValue?.value,
},
}
- newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, itemValue.value)
+ newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value!, itemValue?.value)
})
}
else {
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
- if (itemValue.auto === 1) {
+ if (itemValue?.auto === 1) {
newValues[formSchema.variable] = {
auto: 1,
value: null,
@@ -246,7 +275,7 @@ export const generateAgentToolValue = (value: Record, formSchemas:
else {
newValues[formSchema.variable] = {
auto: 0,
- value: itemValue.value || {
+ value: (itemValue?.value as FormValueInput) || {
type: getVarKindType(formSchema.type as FormTypeEnum),
value: null,
},
diff --git a/web/app/components/workflow/nodes/tool/use-config.ts b/web/app/components/workflow/nodes/tool/use-config.ts
index add4282a99..7e4594f4f2 100644
--- a/web/app/components/workflow/nodes/tool/use-config.ts
+++ b/web/app/components/workflow/nodes/tool/use-config.ts
@@ -174,7 +174,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
draft.tool_configurations = getConfiguredValue(
tool_configurations,
toolSettingSchema,
- )
+ ) as ToolVarInputs
}
if (
!draft.tool_parameters
@@ -183,7 +183,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
draft.tool_parameters = getConfiguredValue(
tool_parameters,
toolInputVarSchema,
- )
+ ) as ToolVarInputs
}
})
return inputsWithDefaultValue
diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json
index b043a2d951..39ea10b290 100644
--- a/web/eslint-suppressions.json
+++ b/web/eslint-suppressions.json
@@ -2580,11 +2580,6 @@
"count": 8
}
},
- "app/components/plugins/plugin-detail-panel/app-selector/index.tsx": {
- "ts/no-explicit-any": {
- "count": 5
- }
- },
"app/components/plugins/plugin-detail-panel/datasource-action-list.tsx": {
"ts/no-explicit-any": {
"count": 1
@@ -2671,26 +2666,6 @@
"count": 2
}
},
- "app/components/plugins/plugin-detail-panel/tool-selector/index.tsx": {
- "ts/no-explicit-any": {
- "count": 15
- }
- },
- "app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form.tsx": {
- "ts/no-explicit-any": {
- "count": 24
- }
- },
- "app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form.tsx": {
- "ts/no-explicit-any": {
- "count": 3
- }
- },
- "app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx": {
- "ts/no-explicit-any": {
- "count": 2
- }
- },
"app/components/plugins/plugin-detail-panel/trigger/event-detail-drawer.tsx": {
"ts/no-explicit-any": {
"count": 5
@@ -3085,11 +3060,6 @@
"count": 4
}
},
- "app/components/tools/utils/to-form-schema.ts": {
- "ts/no-explicit-any": {
- "count": 15
- }
- },
"app/components/tools/workflow-tool/configure-button.tsx": {
"react-hooks/preserve-manual-memoization": {
"count": 2
@@ -4919,11 +4889,6 @@
"count": 4
}
},
- "service/tools.ts": {
- "ts/no-explicit-any": {
- "count": 2
- }
- },
"service/use-apps.ts": {
"ts/no-explicit-any": {
"count": 1
diff --git a/web/service/tools.ts b/web/service/tools.ts
index 99b84d3981..7ffe8ef65a 100644
--- a/web/service/tools.ts
+++ b/web/service/tools.ts
@@ -1,5 +1,6 @@
import type {
Collection,
+ Credential,
CustomCollectionBackend,
CustomParamSchema,
Tool,
@@ -41,9 +42,9 @@ export const fetchBuiltInToolCredentialSchema = (collectionName: string) => {
}
export const fetchBuiltInToolCredential = (collectionName: string) => {
- return get(`/workspaces/current/tool-provider/builtin/${collectionName}/credentials`)
+ return get>(`/workspaces/current/tool-provider/builtin/${collectionName}/credentials`)
}
-export const updateBuiltInToolCredential = (collectionName: string, credential: Record) => {
+export const updateBuiltInToolCredential = (collectionName: string, credential: Record) => {
return post(`/workspaces/current/tool-provider/builtin/${collectionName}/update`, {
body: {
credentials: credential,
@@ -102,7 +103,14 @@ export const importSchemaFromURL = (url: string) => {
})
}
-export const testAPIAvailable = (payload: any) => {
+export const testAPIAvailable = (payload: {
+ provider_name: string
+ tool_name: string
+ credentials: Credential
+ schema_type: string
+ schema: string
+ parameters: Record
+}) => {
return post('/workspaces/current/tool-provider/api/test/pre', {
body: {
...payload,