mirror of
https://github.com/langgenius/dify.git
synced 2026-01-19 03:35:06 +08:00
test(web): enhance unit tests for Icon and AppPicker components
- Refactored and improved test cases for the Icon component, ensuring proper rendering and behavior for various src configurations. - Updated AppPicker tests to utilize React context for managing portal open state, enhancing test reliability and clarity. - Adjusted type definitions in several components to use more specific types, improving type safety and code maintainability.
This commit is contained in:
@ -998,58 +998,59 @@ describe('Icon', () => {
|
||||
render(<Icon src={{ content: '', background: '#ffffff' }} />)
|
||||
|
||||
expect(screen.getByTestId('app-icon')).toBeInTheDocument()
|
||||
it('should not render status indicators when src is object with installed=true', () => {
|
||||
render(<Icon src={{ content: '🎉', background: '#fff' }} installed={true} />)
|
||||
})
|
||||
|
||||
// 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(<Icon src={{ content: '🎉', background: '#fff' }} installed={true} />)
|
||||
|
||||
it('should not render status indicators when src is object with installFailed=true', () => {
|
||||
render(<Icon src={{ content: '🎉', background: '#fff' }} installFailed={true} />)
|
||||
// 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(<Icon src={{ content: '🎉', background: '#fff' }} installFailed={true} />)
|
||||
|
||||
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(<Icon src={{ content: '🔗', background: '#fff' }} size={size} />)
|
||||
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(
|
||||
<Icon src={{ content: '🎉', background: '#fff' }} className="custom-object-icon" />,
|
||||
)
|
||||
|
||||
expect(container.querySelector('.custom-object-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass correct props to AppIcon for object src', () => {
|
||||
render(<Icon src={{ content: '😀', background: '#123456' }} />)
|
||||
|
||||
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(<Icon src={{ content: '🔗', background: '#fff' }} />)
|
||||
expect(screen.getByTestId('inner-icon')).toBeInTheDocument()
|
||||
sizes.forEach((size) => {
|
||||
const { unmount } = render(<Icon src={{ content: '🔗', background: '#fff' }} size={size} />)
|
||||
expect(screen.getByTestId('app-icon')).toHaveAttribute('data-size', size)
|
||||
unmount()
|
||||
|
||||
// Test without MCP icon content
|
||||
render(<Icon src={{ content: '🎉', background: '#fff' }} />)
|
||||
expect(screen.queryByTestId('inner-icon')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render object src with custom className', () => {
|
||||
const { container } = render(
|
||||
<Icon src={{ content: '🎉', background: '#fff' }} className="custom-object-icon" />,
|
||||
)
|
||||
|
||||
expect(container.querySelector('.custom-object-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass correct props to AppIcon for object src', () => {
|
||||
render(<Icon src={{ content: '😀', background: '#123456' }} />)
|
||||
|
||||
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(<Icon src={{ content: '🔗', background: '#fff' }} />)
|
||||
expect(screen.getByTestId('inner-icon')).toBeInTheDocument()
|
||||
unmount()
|
||||
|
||||
// Test without MCP icon content
|
||||
render(<Icon src={{ content: '🎉', background: '#fff' }} />)
|
||||
expect(screen.queryByTestId('inner-icon')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
|
||||
@ -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 (
|
||||
<div data-testid="portal-to-follow-elem" data-open={open}>
|
||||
vi.mock('@/app/components/base/portal-to-follow-elem', () => {
|
||||
// Context reference shared across mock components
|
||||
let sharedContext: React.Context<boolean> | null = null
|
||||
|
||||
// Lazily get or create the context
|
||||
const getContext = (): React.Context<boolean> => {
|
||||
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
|
||||
}) => (
|
||||
<div data-testid="portal-trigger" onClick={onClick} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
PortalToFollowElemTrigger: ({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
}) => (
|
||||
<div data-testid="portal-trigger" onClick={onClick} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
PortalToFollowElemContent: ({ children, className }: { children: ReactNode, className?: string }) => {
|
||||
if (!mockPortalOpenState)
|
||||
return null
|
||||
return (
|
||||
<div data-testid="portal-content" className={className}>{children}</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
),
|
||||
PortalToFollowElemContent: ({ children, className }: { children: ReactNode, className?: string }) => {
|
||||
const Context = getContext()
|
||||
const isOpen = React.useContext(Context)
|
||||
if (!isOpen)
|
||||
return null
|
||||
return (
|
||||
<div data-testid="portal-content" className={className}>{children}</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// 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<string, unknown> = {}): 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(<AppPicker {...defaultProps} isShow={true} />)
|
||||
expect(screen.getByText('App 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('App 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show loading indicator when isLoading is true', () => {
|
||||
mockPortalOpenState = true
|
||||
render(<AppPicker {...defaultProps} isShow={true} isLoading={true} />)
|
||||
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(<AppPicker {...defaultProps} isShow={true} onSelect={onSelect} />)
|
||||
|
||||
@ -489,7 +509,6 @@ describe('AppPicker', () => {
|
||||
})
|
||||
|
||||
it('should call onSearchChange when typing in search input', () => {
|
||||
mockPortalOpenState = true
|
||||
const onSearchChange = vi.fn()
|
||||
render(<AppPicker {...defaultProps} isShow={true} onSearchChange={onSearchChange} />)
|
||||
|
||||
@ -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(<AppPicker {...defaultProps} isShow={true} apps={apps} />)
|
||||
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(<AppPicker {...defaultProps} isShow={true} apps={apps} />)
|
||||
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(<AppPicker {...defaultProps} isShow={true} apps={apps} />)
|
||||
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(<AppPicker {...defaultProps} isShow={true} apps={apps} />)
|
||||
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(<AppPicker {...defaultProps} isShow={true} apps={apps} />)
|
||||
expect(screen.getByText('completion')).toBeInTheDocument()
|
||||
@ -554,13 +568,11 @@ describe('AppPicker', () => {
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty apps array', () => {
|
||||
mockPortalOpenState = true
|
||||
render(<AppPicker {...defaultProps} isShow={true} apps={[]} />)
|
||||
expect(screen.queryByRole('listitem')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle search text with value', () => {
|
||||
mockPortalOpenState = true
|
||||
render(<AppPicker {...defaultProps} isShow={true} searchText="test search" />)
|
||||
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(<AppPicker {...defaultProps} isShow={true} searchText="test" onSearchChange={onSearchChange} />)
|
||||
|
||||
@ -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(<AppPicker {...defaultProps} isShow={true} hasMore={true} isLoading={true} onLoadMore={onLoadMore} />)
|
||||
@ -594,7 +604,6 @@ describe('AppPicker', () => {
|
||||
})
|
||||
|
||||
it('should not call onLoadMore when hasMore is false', () => {
|
||||
mockPortalOpenState = true
|
||||
const onLoadMore = vi.fn()
|
||||
|
||||
render(<AppPicker {...defaultProps} isShow={true} hasMore={false} onLoadMore={onLoadMore} />)
|
||||
@ -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(<AppPicker {...defaultProps} isShow={true} hasMore={true} isLoading={false} onLoadMore={onLoadMore} />)
|
||||
@ -619,7 +627,6 @@ describe('AppPicker', () => {
|
||||
})
|
||||
|
||||
it('should not call onLoadMore when target is not intersecting', () => {
|
||||
mockPortalOpenState = true
|
||||
const onLoadMore = vi.fn()
|
||||
|
||||
render(<AppPicker {...defaultProps} isShow={true} hasMore={true} isLoading={false} onLoadMore={onLoadMore} />)
|
||||
@ -631,7 +638,6 @@ describe('AppPicker', () => {
|
||||
})
|
||||
|
||||
it('should handle observer target ref', () => {
|
||||
mockPortalOpenState = true
|
||||
render(<AppPicker {...defaultProps} isShow={true} hasMore={true} />)
|
||||
|
||||
// The component should render without errors
|
||||
@ -642,11 +648,9 @@ describe('AppPicker', () => {
|
||||
const { rerender } = render(<AppPicker {...defaultProps} isShow={false} />)
|
||||
|
||||
// Change isShow to true
|
||||
mockPortalOpenState = true
|
||||
rerender(<AppPicker {...defaultProps} isShow={true} />)
|
||||
|
||||
// Then back to false
|
||||
mockPortalOpenState = false
|
||||
rerender(<AppPicker {...defaultProps} isShow={false} />)
|
||||
|
||||
// Should not crash
|
||||
@ -654,8 +658,6 @@ describe('AppPicker', () => {
|
||||
})
|
||||
|
||||
it('should setup intersection observer when isShow is true', () => {
|
||||
mockPortalOpenState = true
|
||||
|
||||
render(<AppPicker {...defaultProps} isShow={true} hasMore={true} />)
|
||||
|
||||
// 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(<AppPicker {...defaultProps} isShow={true} />)
|
||||
|
||||
// Verify observer was set up
|
||||
expect(intersectionObserverCallback).not.toBeNull()
|
||||
|
||||
// Change to not shown - should disconnect observer (lines 74-75)
|
||||
mockPortalOpenState = false
|
||||
rerender(<AppPicker {...defaultProps} isShow={false} />)
|
||||
|
||||
// Component should render without errors
|
||||
@ -678,7 +678,6 @@ describe('AppPicker', () => {
|
||||
})
|
||||
|
||||
it('should cleanup observer on component unmount', () => {
|
||||
mockPortalOpenState = true
|
||||
const { unmount } = render(<AppPicker {...defaultProps} isShow={true} />)
|
||||
|
||||
// Unmount should trigger cleanup without throwing
|
||||
@ -686,8 +685,6 @@ describe('AppPicker', () => {
|
||||
})
|
||||
|
||||
it('should handle MutationObserver callback when target becomes available', () => {
|
||||
mockPortalOpenState = true
|
||||
|
||||
render(<AppPicker {...defaultProps} isShow={true} hasMore={true} />)
|
||||
|
||||
// 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(<AppPicker {...defaultProps} isShow={false} />)
|
||||
|
||||
// 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(<AppPicker {...defaultProps} isShow={true} hasMore={true} isLoading={false} onLoadMore={onLoadMore} />)
|
||||
@ -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 }],
|
||||
}
|
||||
|
||||
@ -23,8 +23,8 @@ const PAGE_SIZE = 20
|
||||
type Props = {
|
||||
value?: {
|
||||
app_id: string
|
||||
inputs: Record<string, any>
|
||||
files?: any[]
|
||||
inputs: Record<string, unknown>
|
||||
files?: unknown[]
|
||||
}
|
||||
scope?: string
|
||||
disabled?: boolean
|
||||
@ -32,8 +32,8 @@ type Props = {
|
||||
offset?: OffsetOptions
|
||||
onSelect: (app: {
|
||||
app_id: string
|
||||
inputs: Record<string, any>
|
||||
files?: any[]
|
||||
inputs: Record<string, unknown>
|
||||
files?: unknown[]
|
||||
}) => void
|
||||
supportAddCustomTool?: boolean
|
||||
}
|
||||
@ -63,12 +63,12 @@ const AppSelector: FC<Props> = ({
|
||||
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<Props> = ({
|
||||
setIsShowChooseApp(false)
|
||||
}
|
||||
|
||||
const handleFormChange = (inputs: Record<string, any>) => {
|
||||
const handleFormChange = (inputs: Record<string, unknown>) => {
|
||||
const newFiles = inputs['#image#']
|
||||
delete inputs['#image#']
|
||||
const newValue = {
|
||||
|
||||
@ -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<string, ReasoningConfigInput>
|
||||
|
||||
type Props = {
|
||||
value: Record<string, any>
|
||||
onChange: (val: Record<string, any>) => void
|
||||
value: ReasoningConfigValue
|
||||
onChange: (val: ReasoningConfigValue) => void
|
||||
schemas: ToolFormSchema[]
|
||||
nodeOutputVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
@ -62,7 +75,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
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<Props> = ({
|
||||
},
|
||||
})
|
||||
}
|
||||
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<Props> = ({
|
||||
}
|
||||
}, [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<Props> = ({
|
||||
const handleAppChange = useCallback((variable: string) => {
|
||||
return (app: {
|
||||
app_id: string
|
||||
inputs: Record<string, any>
|
||||
files?: any[]
|
||||
inputs: Record<string, unknown>
|
||||
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<string, unknown>) => {
|
||||
const newValue = produce(value, (draft: ToolVarInputs) => {
|
||||
const currentValue = draft[variable].value as Record<string, unknown> | undefined
|
||||
draft[variable].value = {
|
||||
...draft[variable].value,
|
||||
...currentValue,
|
||||
...model,
|
||||
} as any
|
||||
}
|
||||
})
|
||||
onChange(newValue)
|
||||
}
|
||||
@ -196,17 +210,17 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
}
|
||||
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<Props> = ({
|
||||
<Input
|
||||
className="h-8 grow"
|
||||
type="number"
|
||||
value={varInput?.value || ''}
|
||||
value={(varInput?.value as string | number) || ''}
|
||||
onChange={e => handleValueChange(variable, type)(e.target.value)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
@ -280,10 +294,10 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
{isSelect && options && (
|
||||
<SimpleSelect
|
||||
wrapperClassName="h-8 grow"
|
||||
defaultValue={varInput?.value}
|
||||
defaultValue={varInput?.value as string | number | undefined}
|
||||
items={options.filter((option) => {
|
||||
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<Props> = ({
|
||||
<div className="mt-1 w-full">
|
||||
<CodeEditor
|
||||
title="JSON"
|
||||
value={varInput?.value as any}
|
||||
value={varInput?.value as string}
|
||||
isExpand
|
||||
isInNode
|
||||
height={100}
|
||||
@ -310,7 +324,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
<AppSelector
|
||||
disabled={false}
|
||||
scope={scope || 'all'}
|
||||
value={varInput as any}
|
||||
value={varInput as { app_id: string, inputs: Record<string, unknown>, files?: unknown[] } | undefined}
|
||||
onSelect={handleAppChange(variable)}
|
||||
/>
|
||||
)}
|
||||
@ -331,7 +345,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
readonly={false}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.value || []}
|
||||
value={(varInput?.value as string | ValueSelector) || []}
|
||||
onChange={handleVariableSelectorChange(variable)}
|
||||
filterVar={getFilterVar()}
|
||||
schema={schema as Partial<CredentialFormSchema>}
|
||||
|
||||
@ -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<string, any>) => void
|
||||
onSaved: (value: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
const ToolCredentialForm: FC<Props> = ({
|
||||
@ -29,9 +30,9 @@ const ToolCredentialForm: FC<Props> = ({
|
||||
}) => {
|
||||
const getValueFromI18nObject = useRenderI18nObject()
|
||||
const { t } = useTranslation()
|
||||
const [credentialSchema, setCredentialSchema] = useState<any>(null)
|
||||
const [credentialSchema, setCredentialSchema] = useState<ToolCredentialFormSchema[] | null>(null)
|
||||
const { name: collectionName } = collection
|
||||
const [tempCredential, setTempCredential] = React.useState<any>({})
|
||||
const [tempCredential, setTempCredential] = React.useState<Record<string, unknown>>({})
|
||||
useEffect(() => {
|
||||
fetchBuiltInToolCredentialSchema(collectionName).then(async (res) => {
|
||||
const toolCredentialSchemas = toolCredentialToFormSchemas(res)
|
||||
@ -44,6 +45,8 @@ const ToolCredentialForm: FC<Props> = ({
|
||||
}, [])
|
||||
|
||||
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) }) })
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<string, any>
|
||||
settingsValue: ToolVarInputs
|
||||
showTabSlider: boolean
|
||||
userSettingsOnly: boolean
|
||||
reasoningConfigOnly: boolean
|
||||
nodeOutputVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
onCurrTypeChange: (type: TabType) => void
|
||||
onSettingsFormChange: (v: Record<string, any>) => void
|
||||
onParamsFormChange: (v: Record<string, any>) => void
|
||||
onSettingsFormChange: (v: ToolVarInputs) => void
|
||||
onParamsFormChange: (v: ReasoningConfigValue) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,7 +142,7 @@ const ToolSettingsPanel: FC<ToolSettingsPanelProps> = ({
|
||||
{/* Reasoning config form */}
|
||||
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
|
||||
<ReasoningConfigForm
|
||||
value={value?.parameters || {}}
|
||||
value={(value?.parameters || {}) as ReasoningConfigValue}
|
||||
onChange={onParamsFormChange}
|
||||
schemas={paramsFormSchemas}
|
||||
nodeOutputVars={nodeOutputVars}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
'use client'
|
||||
import type { ReasoningConfigValue } from '../components/reasoning-config-form'
|
||||
import type { ToolParameter } from '@/app/components/tools/types'
|
||||
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
|
||||
import type { ResourceVarInputs } from '@/app/components/workflow/nodes/_base/types'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
@ -107,11 +110,11 @@ export const useToolSelectorState = ({
|
||||
const getToolValue = useCallback((tool: ToolDefaultValue): ToolValue => {
|
||||
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<string, any>) => {
|
||||
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<string, any>) => {
|
||||
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<string, { value: unknown }>) as ResourceVarInputs
|
||||
}, [value?.settings])
|
||||
|
||||
return {
|
||||
|
||||
@ -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' } } } }),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -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<string, any>, formSchemas: { variable: string, type: string, default?: any }[]) => {
|
||||
export const addDefaultValue = (value: Record<string, unknown>, 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<string, any>, 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<string, any>, formSchemas: { variable: string, default?: any, type: string }[], isReasoning = false) => {
|
||||
const newValues = {} as any
|
||||
export const generateFormValue = (value: Record<string, unknown>, formSchemas: { variable: string, default?: unknown, type: string }[], isReasoning = false) => {
|
||||
const newValues: Record<string, unknown> = {}
|
||||
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<string, any>) => {
|
||||
const plainValue = { ...value }
|
||||
Object.keys(plainValue).forEach((key) => {
|
||||
export const getPlainValue = (value: Record<string, { value: unknown }>) => {
|
||||
const plainValue: Record<string, unknown> = {}
|
||||
Object.keys(value).forEach((key) => {
|
||||
plainValue[key] = {
|
||||
...value[key].value,
|
||||
...(value[key].value as object),
|
||||
}
|
||||
})
|
||||
return plainValue
|
||||
}
|
||||
|
||||
export const getStructureValue = (value: Record<string, any>) => {
|
||||
const newValue = { ...value } as any
|
||||
Object.keys(newValue).forEach((key) => {
|
||||
export const getStructureValue = (value: Record<string, unknown>): Record<string, { value: unknown }> => {
|
||||
const newValue: Record<string, { value: unknown }> = {}
|
||||
Object.keys(value).forEach((key) => {
|
||||
newValue[key] = {
|
||||
value: value[key],
|
||||
}
|
||||
@ -195,17 +224,17 @@ export const getStructureValue = (value: Record<string, any>) => {
|
||||
return newValue
|
||||
}
|
||||
|
||||
export const getConfiguredValue = (value: Record<string, any>, formSchemas: { variable: string, type: string, default?: any }[]) => {
|
||||
const newValues = { ...value }
|
||||
export const getConfiguredValue = (value: Record<string, unknown>, formSchemas: { variable: string, type: string, default?: unknown }[]) => {
|
||||
const newValues: Record<string, unknown> = { ...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<string, any>, formSchemas: { variable: string, default?: any, type: string }[], isReasoning = false) => {
|
||||
const newValues = {} as any
|
||||
export const generateAgentToolValue = (value: Record<string, { value?: unknown, auto?: 0 | 1 }>, formSchemas: { variable: string, default?: unknown, type: string }[], isReasoning = false) => {
|
||||
const newValues: Record<string, { value: FormValueInput | null, auto?: 0 | 1 }> = {}
|
||||
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<string, any>, formSchemas:
|
||||
else {
|
||||
newValues[formSchema.variable] = {
|
||||
auto: 0,
|
||||
value: itemValue.value || {
|
||||
value: (itemValue?.value as FormValueInput) || {
|
||||
type: getVarKindType(formSchema.type as FormTypeEnum),
|
||||
value: null,
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<ToolCredential[]>(`/workspaces/current/tool-provider/builtin/${collectionName}/credentials`)
|
||||
return get<Record<string, unknown>>(`/workspaces/current/tool-provider/builtin/${collectionName}/credentials`)
|
||||
}
|
||||
export const updateBuiltInToolCredential = (collectionName: string, credential: Record<string, any>) => {
|
||||
export const updateBuiltInToolCredential = (collectionName: string, credential: Record<string, unknown>) => {
|
||||
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<string, string>
|
||||
}) => {
|
||||
return post('/workspaces/current/tool-provider/api/test/pre', {
|
||||
body: {
|
||||
...payload,
|
||||
|
||||
Reference in New Issue
Block a user