mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
test: cover tool auth warning and single-run hooks; add i18n for authorization required
This commit is contained in:
@ -1,9 +1,12 @@
|
||||
import type { CommonNodeType, Node } from '../../types'
|
||||
import type { ChecklistItem } from '../use-checklist'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import { createElement, Fragment } from 'react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { createEdge, createNode, resetFixtureCounters } from '../../__tests__/fixtures'
|
||||
import { resetReactFlowMockState, rfState } from '../../__tests__/reactflow-mock-state'
|
||||
import { renderWorkflowHook } from '../../__tests__/workflow-test-env'
|
||||
import { renderWorkflowComponent, renderWorkflowHook } from '../../__tests__/workflow-test-env'
|
||||
import { useStore } from '../../store'
|
||||
import { BlockEnum } from '../../types'
|
||||
import { useChecklist, useWorkflowRunValidation } from '../use-checklist'
|
||||
|
||||
@ -363,6 +366,45 @@ describe('useChecklist', () => {
|
||||
'workflow.errorMsg.invalidVariable',
|
||||
])
|
||||
})
|
||||
|
||||
it('should sync checklist items to the workflow store without render phase update warnings', async () => {
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
try {
|
||||
const startNode = createNode({ id: 'start', data: { type: BlockEnum.Start, title: 'Start' } })
|
||||
const codeNode = createNode({ id: 'code', data: { type: BlockEnum.Code, title: 'Code' } })
|
||||
|
||||
function Operator() {
|
||||
const checklistItems = useStore(state => state.checklistItems)
|
||||
return createElement('div', { 'data-testid': 'checklist-count' }, checklistItems.length)
|
||||
}
|
||||
|
||||
function WorkflowChecklist() {
|
||||
useChecklist([startNode, codeNode], [])
|
||||
return null
|
||||
}
|
||||
|
||||
const { store } = renderWorkflowComponent(
|
||||
createElement(
|
||||
Fragment,
|
||||
null,
|
||||
createElement(Operator),
|
||||
createElement(WorkflowChecklist),
|
||||
),
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.getState().checklistItems).toHaveLength(1)
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('checklist-count')).toHaveTextContent('1')
|
||||
expect(errorSpy.mock.calls.some(call =>
|
||||
call.some(arg => typeof arg === 'string' && arg.includes('Cannot update a component')),
|
||||
)).toBe(false)
|
||||
}
|
||||
finally {
|
||||
errorSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -17,8 +17,10 @@ import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import { useQueries, useQueryClient } from '@tanstack/react-query'
|
||||
import isDeepEqual from 'fast-deep-equal'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
@ -306,9 +308,16 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
}
|
||||
})
|
||||
|
||||
workflowStore.setState({ checklistItems: list })
|
||||
return list
|
||||
}, [nodes, edges, shouldCheckStartNode, nodesExtraData, buildInTools, customTools, workflowTools, mcpTools, language, dataSourceList, triggerPlugins, getToolIcon, strategyProviders, getCheckData, t, map, modelProviders, workflowStore])
|
||||
}, [nodes, edges, shouldCheckStartNode, nodesExtraData, buildInTools, customTools, workflowTools, mcpTools, language, dataSourceList, triggerPlugins, getToolIcon, strategyProviders, getCheckData, t, map, modelProviders])
|
||||
|
||||
useEffect(() => {
|
||||
const currentChecklistItems = workflowStore.getState().checklistItems
|
||||
if (isDeepEqual(currentChecklistItems, needWarningNodes))
|
||||
return
|
||||
|
||||
workflowStore.setState({ checklistItems: needWarningNodes })
|
||||
}, [needWarningNodes, workflowStore])
|
||||
|
||||
return needWarningNodes
|
||||
}
|
||||
|
||||
@ -28,8 +28,8 @@ import useQuestionClassifierSingleRunFormParams from '@/app/components/workflow/
|
||||
import useStartSingleRunFormParams from '@/app/components/workflow/nodes/start/use-single-run-form-params'
|
||||
import useTemplateTransformSingleRunFormParams from '@/app/components/workflow/nodes/template-transform/use-single-run-form-params'
|
||||
|
||||
import useToolGetDataForCheckMore from '@/app/components/workflow/nodes/tool/use-get-data-for-check-more'
|
||||
import useToolSingleRunFormParams from '@/app/components/workflow/nodes/tool/use-single-run-form-params'
|
||||
import useToolGetDataForCheckMore from '@/app/components/workflow/nodes/tool/hooks/use-get-data-for-check-more'
|
||||
import useToolSingleRunFormParams from '@/app/components/workflow/nodes/tool/hooks/use-single-run-form-params'
|
||||
import useTriggerPluginGetDataForCheckMore from '@/app/components/workflow/nodes/trigger-plugin/use-check-params'
|
||||
import useVariableAggregatorSingleRunFormParams from '@/app/components/workflow/nodes/variable-assigner/use-single-run-form-params'
|
||||
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { isToolAuthorizationRequired } from '../auth'
|
||||
|
||||
describe('isToolAuthorizationRequired', () => {
|
||||
it('should return true for built-in tools that require authorization and are not authorized', () => {
|
||||
expect(isToolAuthorizationRequired(CollectionType.builtIn, {
|
||||
allow_delete: true,
|
||||
is_team_authorization: false,
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when the built-in tool is already authorized', () => {
|
||||
expect(isToolAuthorizationRequired(CollectionType.builtIn, {
|
||||
allow_delete: true,
|
||||
is_team_authorization: true,
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for non-built-in tools even if the provider is unauthorized', () => {
|
||||
expect(isToolAuthorizationRequired(CollectionType.custom, {
|
||||
allow_delete: true,
|
||||
is_team_authorization: false,
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when the collection is missing or authorization is not required', () => {
|
||||
expect(isToolAuthorizationRequired(CollectionType.builtIn)).toBe(false)
|
||||
expect(isToolAuthorizationRequired(CollectionType.builtIn, {
|
||||
allow_delete: false,
|
||||
is_team_authorization: false,
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,99 @@
|
||||
import type { ToolNodeType } from '../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import Node from '../node'
|
||||
|
||||
const mockUseNodePluginInstallation = vi.hoisted(() => vi.fn())
|
||||
const mockUseCurrentToolCollection = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks/use-node-plugin-installation', () => ({
|
||||
useNodePluginInstallation: mockUseNodePluginInstallation,
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-current-tool-collection', () => ({
|
||||
__esModule: true,
|
||||
default: mockUseCurrentToolCollection,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/install-plugin-button', () => ({
|
||||
InstallPluginButton: () => <button type="button">Install Plugin</button>,
|
||||
}))
|
||||
|
||||
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
|
||||
title: 'Google Search',
|
||||
desc: '',
|
||||
type: BlockEnum.Tool,
|
||||
provider_id: 'google_search',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_name: 'Google Search',
|
||||
tool_name: 'google_search',
|
||||
tool_label: 'Google Search',
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('ToolNode', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseNodePluginInstallation.mockReturnValue({
|
||||
isChecking: false,
|
||||
isMissing: false,
|
||||
uniqueIdentifier: undefined,
|
||||
canInstall: false,
|
||||
onInstallSuccess: vi.fn(),
|
||||
shouldDim: false,
|
||||
})
|
||||
mockUseCurrentToolCollection.mockReturnValue({
|
||||
currentTools: [],
|
||||
currCollection: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Authorization Warning', () => {
|
||||
it('should render the authorization warning when the tool requires authorization and is not authorized', () => {
|
||||
mockUseCurrentToolCollection.mockReturnValue({
|
||||
currentTools: [],
|
||||
currCollection: {
|
||||
allow_delete: true,
|
||||
is_team_authorization: false,
|
||||
},
|
||||
})
|
||||
|
||||
render(<Node id="tool-node-1" data={createNodeData()} />)
|
||||
|
||||
expect(screen.getByText('workflow.nodes.tool.authorizationRequired')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep configuration rows visible when the authorization warning is shown', () => {
|
||||
mockUseCurrentToolCollection.mockReturnValue({
|
||||
currentTools: [],
|
||||
currCollection: {
|
||||
allow_delete: true,
|
||||
is_team_authorization: false,
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
<Node
|
||||
id="tool-node-1"
|
||||
data={createNodeData({
|
||||
tool_configurations: {
|
||||
region: { value: 'us' },
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('region')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.nodes.tool.authorizationRequired')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render nothing when there are no configs, no install action and no authorization warning', () => {
|
||||
const { container } = render(<Node id="tool-node-1" data={createNodeData()} />)
|
||||
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
})
|
||||
})
|
||||
309
web/app/components/workflow/nodes/tool/__tests__/panel.spec.tsx
Normal file
309
web/app/components/workflow/nodes/tool/__tests__/panel.spec.tsx
Normal file
@ -0,0 +1,309 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ToolNodeType } from '../types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import Panel from '../panel'
|
||||
|
||||
const mockUseConfig = vi.hoisted(() => vi.fn())
|
||||
const mockUseStore = vi.hoisted(() => vi.fn())
|
||||
const mockUseMatchSchemaType = vi.hoisted(() => vi.fn())
|
||||
const mockGetMatchedSchemaType = vi.hoisted(() => vi.fn())
|
||||
const mockWrapStructuredVarItem = vi.hoisted(() => vi.fn())
|
||||
const mockToolForm = vi.hoisted(() => vi.fn())
|
||||
const mockStructureOutputItem = vi.hoisted(() => vi.fn())
|
||||
const mockSplit = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../hooks/use-config', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockUseConfig(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: (state: typeof mockWorkflowStoreState) => unknown) => mockUseStore(selector),
|
||||
}))
|
||||
|
||||
vi.mock('../../_base/components/variable/use-match-schema-type', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockUseMatchSchemaType(...args),
|
||||
getMatchedSchemaType: (...args: unknown[]) => mockGetMatchedSchemaType(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/utils/tool', () => ({
|
||||
wrapStructuredVarItem: (...args: unknown[]) => mockWrapStructuredVarItem(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/output-vars', () => ({
|
||||
__esModule: true,
|
||||
default: (props: {
|
||||
title?: string
|
||||
children: ReactNode
|
||||
collapsed?: boolean
|
||||
onCollapse?: (collapsed: boolean) => void
|
||||
}) => (
|
||||
<div data-testid="output-vars">
|
||||
<div>{props.title ?? 'workflow.nodes.common.outputVars'}</div>
|
||||
{props.onCollapse && (
|
||||
<button type="button" onClick={() => props.onCollapse?.(!props.collapsed)}>
|
||||
toggle-output-vars
|
||||
</button>
|
||||
)}
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
),
|
||||
VarItem: (props: {
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
}) => (
|
||||
<div data-testid={`var-item-${props.name}`}>
|
||||
<span>{props.name}</span>
|
||||
<span>{props.type}</span>
|
||||
<span>{props.description}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../components/tool-form', () => ({
|
||||
__esModule: true,
|
||||
default: (props: {
|
||||
schema: CredentialFormSchema[]
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
}) => {
|
||||
mockToolForm(props)
|
||||
return (
|
||||
<div data-testid={`tool-form-${props.schema.map(item => item.variable).join('-') || 'empty'}`}>
|
||||
{props.showManageInputField && props.onManageInputField && (
|
||||
<button type="button" onClick={props.onManageInputField}>
|
||||
Manage Input Field
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show', () => ({
|
||||
__esModule: true,
|
||||
default: (props: { payload: { id: string } }) => {
|
||||
mockStructureOutputItem(props)
|
||||
return <div data-testid="structured-output">{props.payload.id}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../_base/components/split', () => ({
|
||||
__esModule: true,
|
||||
default: (props: { className?: string }) => {
|
||||
mockSplit(props)
|
||||
return <div data-testid="split">{props.className ?? 'default'}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
const mockWorkflowStoreState = {
|
||||
pipelineId: undefined as string | undefined,
|
||||
setShowInputFieldPanel: vi.fn(),
|
||||
}
|
||||
|
||||
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
|
||||
title: 'Google Search',
|
||||
desc: '',
|
||||
type: BlockEnum.Tool,
|
||||
provider_id: 'google_search',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_name: 'Google Search',
|
||||
tool_name: 'google_search',
|
||||
tool_label: 'Google Search',
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createSchemaItem = (variable: string): CredentialFormSchema => ({
|
||||
name: variable,
|
||||
variable,
|
||||
label: { en_US: variable, zh_Hans: variable },
|
||||
type: FormTypeEnum.textInput,
|
||||
required: false,
|
||||
show_on: [],
|
||||
})
|
||||
|
||||
const renderPanel = (data: ToolNodeType = createNodeData()) => {
|
||||
const props: NodePanelProps<ToolNodeType> = {
|
||||
id: 'tool-node-1',
|
||||
data,
|
||||
panelProps: {
|
||||
getInputVars: vi.fn(() => []),
|
||||
toVarInputs: vi.fn(() => []),
|
||||
runInputData: {},
|
||||
runInputDataRef: { current: {} },
|
||||
setRunInputData: vi.fn(),
|
||||
runResult: null,
|
||||
},
|
||||
}
|
||||
|
||||
return render(<Panel {...props} />)
|
||||
}
|
||||
|
||||
const createConfigResult = (overrides: Record<string, unknown> = {}) => ({
|
||||
readOnly: false,
|
||||
inputs: {
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
},
|
||||
toolInputVarSchema: [] as CredentialFormSchema[],
|
||||
setInputVar: vi.fn(),
|
||||
toolSettingSchema: [] as CredentialFormSchema[],
|
||||
toolSettingValue: {},
|
||||
setToolSettingValue: vi.fn(),
|
||||
currCollection: { name: 'google_search' },
|
||||
isShowAuthBtn: false,
|
||||
isLoading: false,
|
||||
outputSchema: [],
|
||||
hasObjectOutput: false,
|
||||
currTool: { name: 'google_search' },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('ToolPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockWorkflowStoreState.pipelineId = undefined
|
||||
mockWorkflowStoreState.setShowInputFieldPanel = vi.fn()
|
||||
mockUseStore.mockImplementation(selector => selector(mockWorkflowStoreState))
|
||||
mockUseMatchSchemaType.mockReturnValue({
|
||||
schemaTypeDefinitions: [{ name: 'structured' }],
|
||||
})
|
||||
mockGetMatchedSchemaType.mockReturnValue('')
|
||||
mockWrapStructuredVarItem.mockImplementation((outputItem, schemaType) => ({
|
||||
id: `${outputItem.name}-${schemaType || 'plain'}`,
|
||||
}))
|
||||
mockUseConfig.mockReturnValue(createConfigResult())
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should render loading when config data is still loading', () => {
|
||||
mockUseConfig.mockReturnValue(createConfigResult({
|
||||
isLoading: true,
|
||||
}))
|
||||
|
||||
renderPanel()
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
expect(screen.queryByText('workflow.nodes.tool.inputVars')).not.toBeInTheDocument()
|
||||
expect(mockToolForm).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Rendering', () => {
|
||||
it('should render input and settings forms and forward the manage input field action', () => {
|
||||
mockWorkflowStoreState.pipelineId = 'pipeline-1'
|
||||
mockUseConfig.mockReturnValue(createConfigResult({
|
||||
inputs: {
|
||||
tool_parameters: { query: { value: 'weather' } },
|
||||
tool_configurations: {},
|
||||
},
|
||||
toolInputVarSchema: [createSchemaItem('query')],
|
||||
toolSettingSchema: [createSchemaItem('region')],
|
||||
toolSettingValue: { region: 'us' },
|
||||
}))
|
||||
|
||||
renderPanel()
|
||||
|
||||
expect(screen.getByText('workflow.nodes.tool.inputVars')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.nodes.tool.settings')).toBeInTheDocument()
|
||||
expect(screen.getAllByTestId('split')).toHaveLength(2)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Manage Input Field' }))
|
||||
|
||||
expect(mockToolForm).toHaveBeenCalledTimes(2)
|
||||
expect(mockToolForm.mock.calls[0][0]).toEqual(expect.objectContaining({
|
||||
nodeId: 'tool-node-1',
|
||||
showManageInputField: true,
|
||||
}))
|
||||
expect(mockToolForm.mock.calls[1][0]).toEqual(expect.objectContaining({
|
||||
nodeId: 'tool-node-1',
|
||||
}))
|
||||
expect(mockToolForm.mock.calls[1][0]).not.toHaveProperty('showManageInputField')
|
||||
expect(mockWorkflowStoreState.setShowInputFieldPanel).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should hide editable forms when the auth button is shown but keep output variables visible', () => {
|
||||
mockUseConfig.mockReturnValue(createConfigResult({
|
||||
isShowAuthBtn: true,
|
||||
toolInputVarSchema: [createSchemaItem('query')],
|
||||
toolSettingSchema: [createSchemaItem('region')],
|
||||
}))
|
||||
|
||||
renderPanel()
|
||||
|
||||
expect(screen.queryByText('workflow.nodes.tool.inputVars')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('workflow.nodes.tool.settings')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('text')).toBeInTheDocument()
|
||||
expect(screen.getByText('files')).toBeInTheDocument()
|
||||
expect(screen.getByText('json')).toBeInTheDocument()
|
||||
expect(mockToolForm).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Output Schema', () => {
|
||||
it('should render scalar and structured outputs with matched schema types', () => {
|
||||
mockGetMatchedSchemaType.mockImplementation((value: { type?: string }) => {
|
||||
return value?.type === 'string' ? 'qa_structured' : 'object_structured'
|
||||
})
|
||||
mockUseConfig.mockReturnValue(createConfigResult({
|
||||
hasObjectOutput: true,
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'summary',
|
||||
type: 'String',
|
||||
description: 'Summary field',
|
||||
value: { type: 'string' },
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
type: 'Object',
|
||||
description: 'Details field',
|
||||
value: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
renderPanel()
|
||||
|
||||
expect(screen.getByText('summary')).toBeInTheDocument()
|
||||
expect(screen.getByText('string (qa_structured)')).toBeInTheDocument()
|
||||
expect(screen.getByText('Summary field')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('structured-output')).toHaveTextContent('details-object_structured')
|
||||
expect(mockWrapStructuredVarItem).toHaveBeenCalledWith(expect.objectContaining({
|
||||
name: 'details',
|
||||
}), 'object_structured')
|
||||
expect(mockStructureOutputItem).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payload: { id: 'details-object_structured' },
|
||||
}))
|
||||
})
|
||||
|
||||
it('should render scalar outputs without a schema suffix when no schema type matches', () => {
|
||||
mockGetMatchedSchemaType.mockReturnValue('')
|
||||
mockUseConfig.mockReturnValue(createConfigResult({
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'summary',
|
||||
type: 'String',
|
||||
description: 'Summary field',
|
||||
value: { type: 'string' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
renderPanel()
|
||||
|
||||
expect(screen.getByTestId('var-item-summary')).toHaveTextContent('summary')
|
||||
expect(screen.getByTestId('var-item-summary')).toHaveTextContent('String'.toLowerCase())
|
||||
expect(screen.getByTestId('var-item-summary')).not.toHaveTextContent('qa_structured')
|
||||
})
|
||||
})
|
||||
})
|
||||
14
web/app/components/workflow/nodes/tool/auth.ts
Normal file
14
web/app/components/workflow/nodes/tool/auth.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { ToolWithProvider } from '../../types'
|
||||
import type { ToolNodeType } from './types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
|
||||
type ToolAuthorizationCollection = Pick<ToolWithProvider, 'allow_delete' | 'is_team_authorization'>
|
||||
|
||||
export const isToolAuthorizationRequired = (
|
||||
providerType: ToolNodeType['provider_type'],
|
||||
collection?: ToolAuthorizationCollection,
|
||||
) => {
|
||||
return providerType === CollectionType.builtIn
|
||||
&& !!collection?.allow_delete
|
||||
&& collection?.is_team_authorization === false
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
import type { ToolNodeType } from '../../types'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { VarType } from '../../types'
|
||||
import useConfig from '../use-config'
|
||||
|
||||
const mockSetInputs = vi.hoisted(() => vi.fn())
|
||||
const mockSetControlPromptEditorRerenderKey = vi.hoisted(() => vi.fn())
|
||||
const mockUseCurrentToolCollection = vi.hoisted(() => vi.fn())
|
||||
const mockGetConfiguredValue = vi.hoisted(() => vi.fn())
|
||||
const mockToolParametersToFormSchemas = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useLanguage: () => 'en_US',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useNodesReadOnly: () => ({ nodesReadOnly: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
|
||||
__esModule: true,
|
||||
default: (_id: string, data: ToolNodeType) => ({
|
||||
inputs: data,
|
||||
setInputs: mockSetInputs,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/tools/utils/to-form-schema', () => ({
|
||||
getConfiguredValue: (...args: unknown[]) => mockGetConfiguredValue(...args),
|
||||
toolParametersToFormSchemas: (...args: unknown[]) => mockToolParametersToFormSchemas(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/tools', () => ({
|
||||
updateBuiltInToolCredential: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidToolsByType: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setControlPromptEditorRerenderKey: mockSetControlPromptEditorRerenderKey,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../use-current-tool-collection', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockUseCurrentToolCollection(...args),
|
||||
}))
|
||||
|
||||
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
|
||||
title: 'Google Search',
|
||||
desc: '',
|
||||
type: BlockEnum.Tool,
|
||||
provider_id: 'google_search',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_name: 'Google Search',
|
||||
tool_name: 'google_search',
|
||||
tool_label: 'Google Search',
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const currentTool = {
|
||||
name: 'google_search',
|
||||
parameters: [
|
||||
{
|
||||
variable: 'query',
|
||||
form: 'llm',
|
||||
label: { en_US: 'Query' },
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'default query',
|
||||
},
|
||||
{
|
||||
variable: 'api_key',
|
||||
form: 'credential',
|
||||
label: { en_US: 'API Key' },
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'default secret',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const currentToolWithoutDefaults = {
|
||||
name: 'google_search',
|
||||
parameters: [
|
||||
{
|
||||
variable: 'query',
|
||||
form: 'llm',
|
||||
label: { en_US: 'Query' },
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
variable: 'api_key',
|
||||
form: 'credential',
|
||||
label: { en_US: 'API Key' },
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const createToolVarInput = (value: string) => ({
|
||||
type: VarType.mixed,
|
||||
value,
|
||||
})
|
||||
|
||||
describe('useConfig', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers()
|
||||
|
||||
mockUseCurrentToolCollection.mockReturnValue({
|
||||
currCollection: {
|
||||
name: 'google_search',
|
||||
allow_delete: true,
|
||||
is_team_authorization: false,
|
||||
tools: [currentTool],
|
||||
},
|
||||
})
|
||||
|
||||
mockToolParametersToFormSchemas.mockImplementation(parameters => parameters)
|
||||
mockGetConfiguredValue.mockImplementation((_value, schema: Array<{ variable: string, default?: string }>) => {
|
||||
return schema.reduce<Record<string, ReturnType<typeof createToolVarInput>>>((acc, item) => {
|
||||
acc[item.variable] = createToolVarInput(item.default || '')
|
||||
return acc
|
||||
}, {} as Record<string, ReturnType<typeof createToolVarInput>>)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.runOnlyPendingTimers()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('Default Value Sync', () => {
|
||||
it('should apply default values only once when the current payload is initially empty', () => {
|
||||
const emptyPayload = createNodeData()
|
||||
const syncedPayload = createNodeData({
|
||||
tool_parameters: { query: createToolVarInput('default query') },
|
||||
tool_configurations: { api_key: createToolVarInput('default secret') },
|
||||
})
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ payload }) => useConfig('tool-node-1', payload),
|
||||
{ initialProps: { payload: emptyPayload } },
|
||||
)
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
tool_parameters: { query: createToolVarInput('default query') },
|
||||
tool_configurations: { api_key: createToolVarInput('default secret') },
|
||||
}))
|
||||
|
||||
rerender({ payload: syncedPayload })
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not update inputs when tool values are already populated on first render', () => {
|
||||
renderHook(() => useConfig('tool-node-1', createNodeData({
|
||||
tool_parameters: { query: createToolVarInput('existing query') },
|
||||
tool_configurations: { api_key: createToolVarInput('existing secret') },
|
||||
})))
|
||||
|
||||
expect(mockSetInputs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not update inputs when empty schemas do not provide any default values', () => {
|
||||
mockUseCurrentToolCollection.mockReturnValue({
|
||||
currCollection: {
|
||||
name: 'google_search',
|
||||
allow_delete: true,
|
||||
is_team_authorization: false,
|
||||
tools: [currentToolWithoutDefaults],
|
||||
},
|
||||
})
|
||||
mockGetConfiguredValue.mockReturnValue({})
|
||||
|
||||
renderHook(() => useConfig('tool-node-1', createNodeData()))
|
||||
|
||||
expect(mockSetInputs).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,84 @@
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import useCurrentToolCollection from '../use-current-tool-collection'
|
||||
|
||||
const mockUseAllBuiltInTools = vi.hoisted(() => vi.fn())
|
||||
const mockUseAllCustomTools = vi.hoisted(() => vi.fn())
|
||||
const mockUseAllWorkflowTools = vi.hoisted(() => vi.fn())
|
||||
const mockUseAllMCPTools = vi.hoisted(() => vi.fn())
|
||||
const mockCanFindTool = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useAllBuiltInTools: () => mockUseAllBuiltInTools(),
|
||||
useAllCustomTools: () => mockUseAllCustomTools(),
|
||||
useAllWorkflowTools: () => mockUseAllWorkflowTools(),
|
||||
useAllMCPTools: () => mockUseAllMCPTools(),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils', () => ({
|
||||
canFindTool: (...args: unknown[]) => mockCanFindTool(...args),
|
||||
}))
|
||||
|
||||
const createToolCollection = (overrides: Partial<ToolWithProvider> = {}): ToolWithProvider => ({
|
||||
id: 'builtin-search',
|
||||
name: 'Google Search',
|
||||
type: CollectionType.builtIn,
|
||||
label: { en_US: 'Google Search' },
|
||||
description: { en_US: 'Search provider' },
|
||||
icon: '',
|
||||
icon_dark: '',
|
||||
background: '',
|
||||
tags: [],
|
||||
tools: [],
|
||||
allow_delete: true,
|
||||
is_team_authorization: false,
|
||||
meta: {} as ToolWithProvider['meta'],
|
||||
...overrides,
|
||||
}) as ToolWithProvider
|
||||
|
||||
describe('useCurrentToolCollection', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCanFindTool.mockImplementation((collectionId: string, providerId: string) => collectionId === providerId)
|
||||
mockUseAllBuiltInTools.mockReturnValue({ data: [] })
|
||||
mockUseAllCustomTools.mockReturnValue({ data: [] })
|
||||
mockUseAllWorkflowTools.mockReturnValue({ data: [] })
|
||||
mockUseAllMCPTools.mockReturnValue({ data: [] })
|
||||
})
|
||||
|
||||
it('should return the built-in collection list and matched provider for built-in tools', () => {
|
||||
const builtInCollection = createToolCollection({ id: 'builtin-search' })
|
||||
mockUseAllBuiltInTools.mockReturnValue({ data: [builtInCollection] })
|
||||
|
||||
const { result } = renderHook(() => useCurrentToolCollection(CollectionType.builtIn, 'builtin-search'))
|
||||
|
||||
expect(result.current.currentTools).toEqual([builtInCollection])
|
||||
expect(result.current.currCollection).toBe(builtInCollection)
|
||||
expect(mockCanFindTool).toHaveBeenCalledWith('builtin-search', 'builtin-search')
|
||||
})
|
||||
|
||||
it('should select the custom tool collection when the provider type is custom', () => {
|
||||
const customCollection = createToolCollection({
|
||||
id: 'custom-search',
|
||||
type: CollectionType.custom,
|
||||
})
|
||||
mockUseAllCustomTools.mockReturnValue({ data: [customCollection] })
|
||||
|
||||
const { result } = renderHook(() => useCurrentToolCollection(CollectionType.custom, 'custom-search'))
|
||||
|
||||
expect(result.current.currentTools).toEqual([customCollection])
|
||||
expect(result.current.currCollection).toBe(customCollection)
|
||||
})
|
||||
|
||||
it('should return undefined when no collection matches the provider id', () => {
|
||||
mockUseAllBuiltInTools.mockReturnValue({
|
||||
data: [createToolCollection({ id: 'another-tool' })],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useCurrentToolCollection(CollectionType.builtIn, 'builtin-search'))
|
||||
|
||||
expect(result.current.currentTools).toHaveLength(1)
|
||||
expect(result.current.currCollection).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,49 @@
|
||||
import type { ToolNodeType } from '../../types'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import useGetDataForCheckMore from '../use-get-data-for-check-more'
|
||||
|
||||
const mockUseConfig = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../use-config', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockUseConfig(...args),
|
||||
}))
|
||||
|
||||
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
|
||||
title: 'Google Search',
|
||||
desc: '',
|
||||
type: BlockEnum.Tool,
|
||||
provider_id: 'google_search',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_name: 'Google Search',
|
||||
tool_name: 'google_search',
|
||||
tool_label: 'Google Search',
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('useGetDataForCheckMore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should expose the config hook validator as getData', () => {
|
||||
const getMoreDataForCheckValid = vi.fn(() => ({ provider: 'google' }))
|
||||
const payload = createNodeData()
|
||||
mockUseConfig.mockReturnValue({
|
||||
getMoreDataForCheckValid,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useGetDataForCheckMore({
|
||||
id: 'tool-node-1',
|
||||
payload,
|
||||
}))
|
||||
|
||||
expect(mockUseConfig).toHaveBeenCalledWith('tool-node-1', payload)
|
||||
expect(result.current.getData).toBe(getMoreDataForCheckValid)
|
||||
expect(result.current.getData()).toEqual({ provider: 'google' })
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,206 @@
|
||||
import type { ToolNodeType, VarType } from '../../types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
|
||||
import useSingleRunFormParams from '../use-single-run-form-params'
|
||||
|
||||
const mockUseToolIcon = vi.hoisted(() => vi.fn())
|
||||
const mockUseNodeCrud = vi.hoisted(() => vi.fn())
|
||||
const mockFormatToTracingNodeList = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useToolIcon: (...args: unknown[]) => mockUseToolIcon(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockUseNodeCrud(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/run/utils/format-log', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockFormatToTracingNodeList(...args),
|
||||
}))
|
||||
|
||||
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
|
||||
title: 'Google Search',
|
||||
desc: '',
|
||||
type: BlockEnum.Tool,
|
||||
provider_id: 'google_search',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_name: 'Google Search',
|
||||
tool_name: 'google_search',
|
||||
tool_label: 'Google Search',
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createInputVar = (variable: InputVar['variable']): InputVar => ({
|
||||
type: InputVarType.textInput,
|
||||
label: typeof variable === 'string' ? variable : 'invalid',
|
||||
variable,
|
||||
required: false,
|
||||
})
|
||||
|
||||
const createRunResult = (): NodeTracing => ({
|
||||
id: 'trace-1',
|
||||
index: 0,
|
||||
predecessor_node_id: '',
|
||||
node_id: 'tool-node-1',
|
||||
node_type: BlockEnum.Tool,
|
||||
title: 'Google Search',
|
||||
inputs: {},
|
||||
inputs_truncated: false,
|
||||
process_data: {},
|
||||
process_data_truncated: false,
|
||||
outputs_truncated: false,
|
||||
status: 'succeeded',
|
||||
elapsed_time: 1,
|
||||
metadata: {
|
||||
iterator_length: 0,
|
||||
iterator_index: 0,
|
||||
loop_length: 0,
|
||||
loop_index: 0,
|
||||
},
|
||||
created_at: 0,
|
||||
created_by: {
|
||||
id: 'user-1',
|
||||
name: 'User',
|
||||
email: 'user@example.com',
|
||||
},
|
||||
finished_at: 1,
|
||||
})
|
||||
|
||||
describe('useSingleRunFormParams', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseToolIcon.mockReturnValue('tool-icon')
|
||||
mockFormatToTracingNodeList.mockReturnValue([{ id: 'formatted-node' }])
|
||||
mockUseNodeCrud.mockImplementation((_id: string, payload: ToolNodeType) => ({
|
||||
inputs: payload,
|
||||
}))
|
||||
})
|
||||
|
||||
describe('Variable Extraction', () => {
|
||||
it('should build form inputs from variable params and settings and expose dependent vars', () => {
|
||||
const payload = createNodeData({
|
||||
tool_parameters: {
|
||||
query: { type: 'variable' as VarType, value: ['start', 'query'] },
|
||||
legacy_query: { type: 'variable' as VarType, value: 'legacy.answer' },
|
||||
constant_query: { type: 'constant' as VarType, value: 'fixed' },
|
||||
},
|
||||
tool_configurations: {
|
||||
prompt: { type: 'mixed' as VarType, value: 'prefix {{#tool.result#}}' },
|
||||
api_key: { type: 'constant' as VarType, value: 'secret' },
|
||||
plainText: 'ignored',
|
||||
},
|
||||
})
|
||||
const getInputVars = vi.fn(() => [
|
||||
createInputVar('#start.query#'),
|
||||
createInputVar(undefined as unknown as string),
|
||||
createInputVar('#legacy.answer#'),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useSingleRunFormParams({
|
||||
id: 'tool-node-1',
|
||||
payload,
|
||||
runInputData: {},
|
||||
runInputDataRef: { current: {} },
|
||||
getInputVars,
|
||||
setRunInputData: vi.fn(),
|
||||
toVarInputs: vi.fn(),
|
||||
runResult: null as unknown as NodeTracing,
|
||||
}))
|
||||
|
||||
expect(getInputVars).toHaveBeenCalledWith([
|
||||
'{{#start.query#}}',
|
||||
'{{#legacy.answer#}}',
|
||||
'prefix {{#tool.result#}}',
|
||||
])
|
||||
expect(result.current.forms).toHaveLength(1)
|
||||
expect(result.current.forms[0].inputs).toEqual([
|
||||
createInputVar('#start.query#'),
|
||||
createInputVar(undefined as unknown as string),
|
||||
createInputVar('#legacy.answer#'),
|
||||
])
|
||||
expect(result.current.forms[0].values).toEqual({})
|
||||
expect(result.current.toolIcon).toBe('tool-icon')
|
||||
expect(result.current.getDependentVars()).toEqual([
|
||||
['start', 'query'],
|
||||
['legacy', 'answer'],
|
||||
])
|
||||
expect(result.current.nodeInfo).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Updates', () => {
|
||||
it('should update form values and forward run input data on change', () => {
|
||||
const payload = createNodeData({
|
||||
tool_parameters: {
|
||||
nullable_constant: { type: 'constant' as VarType, value: null },
|
||||
query: { type: 'variable' as VarType, value: ['start', 'query'] },
|
||||
},
|
||||
})
|
||||
const getInputVars = vi.fn(() => [createInputVar('#start.query#')])
|
||||
const setRunInputData = vi.fn()
|
||||
|
||||
const { result } = renderHook(() => useSingleRunFormParams({
|
||||
id: 'tool-node-1',
|
||||
payload,
|
||||
runInputData: {},
|
||||
runInputDataRef: { current: {} },
|
||||
getInputVars,
|
||||
setRunInputData,
|
||||
toVarInputs: vi.fn(),
|
||||
runResult: null as unknown as NodeTracing,
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.forms[0].onChange({
|
||||
query: 'weather',
|
||||
tool_parameters: {
|
||||
nullable_constant: 'temp-value',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
expect(setRunInputData).toHaveBeenCalledWith({
|
||||
query: 'weather',
|
||||
tool_parameters: {
|
||||
nullable_constant: 'temp-value',
|
||||
},
|
||||
})
|
||||
expect(result.current.forms[0].values).toEqual({
|
||||
query: 'weather',
|
||||
tool_parameters: {
|
||||
nullable_constant: 'temp-value',
|
||||
},
|
||||
nullable_constant: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tracing Data', () => {
|
||||
it('should format the latest run result into node info when a run result exists', () => {
|
||||
const payload = createNodeData()
|
||||
const runResult = createRunResult()
|
||||
|
||||
const { result } = renderHook(() => useSingleRunFormParams({
|
||||
id: 'tool-node-1',
|
||||
payload,
|
||||
runInputData: {},
|
||||
runInputDataRef: { current: {} },
|
||||
getInputVars: vi.fn(() => []),
|
||||
setRunInputData: vi.fn(),
|
||||
toVarInputs: vi.fn(),
|
||||
runResult,
|
||||
}))
|
||||
|
||||
expect(mockFormatToTracingNodeList).toHaveBeenCalledWith([runResult], expect.any(Function))
|
||||
expect(result.current.nodeInfo).toEqual({ id: 'formatted-node' })
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ToolNodeType, ToolVarInputs } from './types'
|
||||
import type { ToolNodeType, ToolVarInputs } from '../types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { capitalize } from 'es-toolkit/string'
|
||||
@ -16,17 +16,14 @@ import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { updateBuiltInToolCredential } from '@/service/tools'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
useInvalidToolsByType,
|
||||
} from '@/service/use-tools'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { useWorkflowStore } from '../../store'
|
||||
import { normalizeJsonSchemaType } from './output-schema-utils'
|
||||
import { isToolAuthorizationRequired } from '../auth'
|
||||
import { normalizeJsonSchemaType } from '../output-schema-utils'
|
||||
import useCurrentToolCollection from './use-current-tool-collection'
|
||||
|
||||
const formatDisplayType = (output: Record<string, unknown>): string => {
|
||||
const normalizedType = normalizeJsonSchemaType(output) || 'Unknown'
|
||||
@ -55,33 +52,10 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
tool_parameters,
|
||||
} = inputs
|
||||
const isBuiltIn = provider_type === CollectionType.builtIn
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
const currentTools = useMemo(() => {
|
||||
switch (provider_type) {
|
||||
case CollectionType.builtIn:
|
||||
return buildInTools || []
|
||||
case CollectionType.custom:
|
||||
return customTools || []
|
||||
case CollectionType.workflow:
|
||||
return workflowTools || []
|
||||
case CollectionType.mcp:
|
||||
return mcpTools || []
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}, [buildInTools, customTools, mcpTools, provider_type, workflowTools])
|
||||
const currCollection = useMemo(() => {
|
||||
return currentTools.find(item => canFindTool(item.id, provider_id))
|
||||
}, [currentTools, provider_id])
|
||||
const { currCollection } = useCurrentToolCollection(provider_type, provider_id)
|
||||
|
||||
// Auth
|
||||
const needAuth = !!currCollection?.allow_delete
|
||||
const isAuthed = !!currCollection?.is_team_authorization
|
||||
const isShowAuthBtn = isBuiltIn && needAuth && !isAuthed
|
||||
const isShowAuthBtn = isToolAuthorizationRequired(provider_type, currCollection)
|
||||
const [
|
||||
showSetAuth,
|
||||
{ setTrue: showSetAuthModal, setFalse: hideSetAuthModal },
|
||||
@ -104,7 +78,6 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
hideSetAuthModal,
|
||||
t,
|
||||
invalidToolsByType,
|
||||
provider_type,
|
||||
],
|
||||
)
|
||||
|
||||
@ -172,38 +145,49 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
[inputs, setInputs],
|
||||
)
|
||||
|
||||
const formattingParameters = () => {
|
||||
const formattingParameters = useCallback(() => {
|
||||
const inputsWithDefaultValue = produce(inputs, (draft) => {
|
||||
if (
|
||||
!draft.tool_configurations
|
||||
|| Object.keys(draft.tool_configurations).length === 0
|
||||
) {
|
||||
draft.tool_configurations = getConfiguredValue(
|
||||
const configuredToolSettings = getConfiguredValue(
|
||||
tool_configurations,
|
||||
toolSettingSchema,
|
||||
) as ToolVarInputs
|
||||
if (Object.keys(configuredToolSettings).length > 0)
|
||||
draft.tool_configurations = configuredToolSettings
|
||||
}
|
||||
if (
|
||||
!draft.tool_parameters
|
||||
|| Object.keys(draft.tool_parameters).length === 0
|
||||
) {
|
||||
draft.tool_parameters = getConfiguredValue(
|
||||
const configuredToolParameters = getConfiguredValue(
|
||||
tool_parameters,
|
||||
toolInputVarSchema,
|
||||
) as ToolVarInputs
|
||||
if (Object.keys(configuredToolParameters).length > 0)
|
||||
draft.tool_parameters = configuredToolParameters
|
||||
}
|
||||
})
|
||||
return inputsWithDefaultValue
|
||||
}
|
||||
}, [inputs, toolInputVarSchema, toolSettingSchema, tool_configurations, tool_parameters])
|
||||
|
||||
useEffect(() => {
|
||||
if (!currTool)
|
||||
return
|
||||
const inputsWithDefaultValue = formattingParameters()
|
||||
if (inputsWithDefaultValue === inputs)
|
||||
return
|
||||
|
||||
const { setControlPromptEditorRerenderKey } = workflowStore.getState()
|
||||
setInputs(inputsWithDefaultValue)
|
||||
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
|
||||
}, [currTool])
|
||||
const rerenderTimeout = setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
|
||||
|
||||
return () => {
|
||||
clearTimeout(rerenderTimeout)
|
||||
}
|
||||
}, [currTool, formattingParameters, inputs, setInputs, workflowStore])
|
||||
|
||||
// setting when call
|
||||
const setInputVar = useCallback(
|
||||
@ -0,0 +1,47 @@
|
||||
import type { ToolNodeType } from '../types'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { useMemo } from 'react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { canFindTool } from '@/utils'
|
||||
|
||||
const useCurrentToolCollection = (
|
||||
providerType: ToolNodeType['provider_type'],
|
||||
providerId: string,
|
||||
) => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
const currentTools = useMemo<ToolWithProvider[]>(() => {
|
||||
switch (providerType) {
|
||||
case CollectionType.builtIn:
|
||||
return buildInTools || []
|
||||
case CollectionType.custom:
|
||||
return customTools || []
|
||||
case CollectionType.workflow:
|
||||
return workflowTools || []
|
||||
case CollectionType.mcp:
|
||||
return mcpTools || []
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}, [buildInTools, customTools, mcpTools, providerType, workflowTools])
|
||||
|
||||
const currCollection = useMemo(() => {
|
||||
return currentTools.find(item => canFindTool(item.id, providerId))
|
||||
}, [currentTools, providerId])
|
||||
|
||||
return {
|
||||
currentTools,
|
||||
currCollection,
|
||||
}
|
||||
}
|
||||
|
||||
export default useCurrentToolCollection
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ToolNodeType } from './types'
|
||||
import type { ToolNodeType } from '../types'
|
||||
import useConfig from './use-config'
|
||||
|
||||
type Params = {
|
||||
@ -1,15 +1,15 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { ToolNodeType } from './types'
|
||||
import type { ToolNodeType } from '../types'
|
||||
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
|
||||
import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToolIcon } from '@/app/components/workflow/hooks'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log'
|
||||
import { useToolIcon } from '../../hooks'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { VarType } from './types'
|
||||
import { VarType } from '../types'
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
@ -50,9 +50,9 @@ const useSingleRunFormParams = ({
|
||||
|
||||
return p.value as string
|
||||
}))
|
||||
const [inputVarValues, doSetInputVarValues] = useState<Record<string, any>>({})
|
||||
const setInputVarValues = useCallback((value: Record<string, any>) => {
|
||||
doSetInputVarValues(value)
|
||||
const [inputVarValues, setInputVarValues] = useState<Record<string, any>>({})
|
||||
const handleInputVarValuesChange = useCallback((value: Record<string, any>) => {
|
||||
setInputVarValues(value)
|
||||
setRunInputData(value)
|
||||
}, [setRunInputData])
|
||||
|
||||
@ -74,10 +74,10 @@ const useSingleRunFormParams = ({
|
||||
const forms: FormProps[] = [{
|
||||
inputs: varInputs,
|
||||
values: inputVarValuesWithConstantValue(),
|
||||
onChange: setInputVarValues,
|
||||
onChange: handleInputVarValuesChange,
|
||||
}]
|
||||
return forms
|
||||
}, [inputVarValuesWithConstantValue, setInputVarValues, varInputs])
|
||||
}, [handleInputVarValuesChange, inputVarValuesWithConstantValue, varInputs])
|
||||
|
||||
const nodeInfo = useMemo(() => {
|
||||
if (!runResult)
|
||||
@ -2,13 +2,17 @@ import type { FC } from 'react'
|
||||
import type { ToolNodeType } from './types'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation'
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import { isToolAuthorizationRequired } from './auth'
|
||||
import useCurrentToolCollection from './hooks/use-current-tool-collection'
|
||||
|
||||
const Node: FC<NodeProps<ToolNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { tool_configurations, paramSchemas } = data
|
||||
const toolConfigs = Object.keys(tool_configurations || {})
|
||||
const {
|
||||
@ -19,11 +23,13 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
|
||||
onInstallSuccess,
|
||||
shouldDim,
|
||||
} = useNodePluginInstallation(data)
|
||||
const { currCollection } = useCurrentToolCollection(data.provider_type, data.provider_id)
|
||||
const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier
|
||||
const showAuthorizationWarning = isToolAuthorizationRequired(data.provider_type, currCollection)
|
||||
|
||||
const hasConfigs = toolConfigs.length > 0
|
||||
|
||||
if (!showInstallButton && !hasConfigs)
|
||||
if (!showInstallButton && !hasConfigs && !showAuthorizationWarning)
|
||||
return null
|
||||
|
||||
return (
|
||||
@ -43,10 +49,10 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{hasConfigs && (
|
||||
{(hasConfigs || showAuthorizationWarning) && (
|
||||
<div className="space-y-0.5" aria-disabled={shouldDim}>
|
||||
{toolConfigs.map((key, index) => (
|
||||
<div key={index} className="flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary">
|
||||
{hasConfigs && toolConfigs.map(key => (
|
||||
<div key={key} className="flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary">
|
||||
<div title={key} className="max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary">
|
||||
{key}
|
||||
</div>
|
||||
@ -67,6 +73,14 @@ const Node: FC<NodeProps<ToolNodeType>> = ({
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{showAuthorizationWarning && (
|
||||
<div className="flex h-6 items-center rounded-md border-[0.5px] border-state-warning-active bg-state-warning-hover px-1.5">
|
||||
<span className="mr-1 size-[4px] shrink-0 rounded-[2px] bg-text-warning-secondary" />
|
||||
<div className="grow truncate text-text-warning system-xs-medium" title={t('nodes.tool.authorizationRequired', { ns: 'workflow' })}>
|
||||
{t('nodes.tool.authorizationRequired', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -12,7 +12,7 @@ import { wrapStructuredVarItem } from '@/app/components/workflow/utils/tool'
|
||||
import Split from '../_base/components/split'
|
||||
import useMatchSchemaType, { getMatchedSchemaType } from '../_base/components/variable/use-match-schema-type'
|
||||
import ToolForm from './components/tool-form'
|
||||
import useConfig from './use-config'
|
||||
import useConfig from './hooks/use-config'
|
||||
|
||||
const i18nPrefix = 'nodes.tool'
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import type { StructuredOutput } from '@/app/components/workflow/nodes/llm/types
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { isToolAuthorizationRequired } from '@/app/components/workflow/nodes/tool/auth'
|
||||
import { canFindTool } from '@/utils'
|
||||
|
||||
export const getToolCheckParams = (
|
||||
@ -17,7 +18,6 @@ export const getToolCheckParams = (
|
||||
language: string,
|
||||
) => {
|
||||
const { provider_id, provider_type, tool_name } = toolData
|
||||
const isBuiltIn = provider_type === CollectionType.builtIn
|
||||
const currentTools = provider_type === CollectionType.builtIn ? buildInTools : provider_type === CollectionType.custom ? customTools : workflowTools
|
||||
const currCollection = currentTools.find(item => canFindTool(item.id, provider_id))
|
||||
const currTool = currCollection?.tools.find(tool => tool.name === tool_name)
|
||||
@ -38,7 +38,7 @@ export const getToolCheckParams = (
|
||||
})
|
||||
return formInputs
|
||||
})(),
|
||||
notAuthed: isBuiltIn && !!currCollection?.allow_delete && !currCollection?.is_team_authorization,
|
||||
notAuthed: isToolAuthorizationRequired(provider_type, currCollection),
|
||||
toolSettingSchema,
|
||||
language,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user