mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
feat(web): restrictions of evalution workflow available nodes
This commit is contained in:
parent
7c05a68876
commit
ed3db06154
@ -517,4 +517,24 @@ describe('AppPublisher', () => {
|
|||||||
tipKey: 'common.switchToStandardWorkflowTip',
|
tipKey: 'common.switchToStandardWorkflowTip',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should block switching to evaluation workflow when restricted nodes exist', async () => {
|
||||||
|
render(
|
||||||
|
<AppPublisher
|
||||||
|
publishedAt={Date.now()}
|
||||||
|
hasHumanInputNode
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.publish'))
|
||||||
|
fireEvent.click(screen.getByText('publisher-switch-workflow-type'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockToastError).toHaveBeenCalledWith('common.switchToEvaluationWorkflowDisabledTip')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockConvertWorkflowType).not.toHaveBeenCalled()
|
||||||
|
expect(sectionProps.summary?.workflowTypeSwitchDisabled).toBe(true)
|
||||||
|
expect(sectionProps.summary?.workflowTypeSwitchDisabledReason).toBe('common.switchToEvaluationWorkflowDisabledTip')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -185,6 +185,37 @@ describe('app-publisher sections', () => {
|
|||||||
expect(onWorkflowTypeSwitch).toHaveBeenCalledTimes(1)
|
expect(onWorkflowTypeSwitch).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should disable workflow type switch when a disabled reason is provided', () => {
|
||||||
|
render(
|
||||||
|
<PublisherSummarySection
|
||||||
|
debugWithMultipleModel={false}
|
||||||
|
draftUpdatedAt={Date.now()}
|
||||||
|
formatTimeFromNow={() => '1 minute ago'}
|
||||||
|
handlePublish={vi.fn()}
|
||||||
|
handleRestore={vi.fn()}
|
||||||
|
isChatApp={false}
|
||||||
|
multipleModelConfigs={[]}
|
||||||
|
onWorkflowTypeSwitch={vi.fn()}
|
||||||
|
publishDisabled={false}
|
||||||
|
published={false}
|
||||||
|
publishedAt={undefined}
|
||||||
|
publishShortcut={['ctrl', '⇧', 'P']}
|
||||||
|
startNodeLimitExceeded={false}
|
||||||
|
upgradeHighlightStyle={{}}
|
||||||
|
workflowTypeSwitchConfig={{
|
||||||
|
targetType: 'evaluation',
|
||||||
|
publishLabelKey: 'common.publishAsEvaluationWorkflow',
|
||||||
|
switchLabelKey: 'common.switchToEvaluationWorkflow',
|
||||||
|
tipKey: 'common.switchToEvaluationWorkflowTip',
|
||||||
|
}}
|
||||||
|
workflowTypeSwitchDisabled
|
||||||
|
workflowTypeSwitchDisabledReason="common.switchToEvaluationWorkflowDisabledTip"
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: /common\.publishAsEvaluationWorkflow/i })).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
it('should render loading access state and access mode labels when enabled', () => {
|
it('should render loading access state and access mode labels when enabled', () => {
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<PublisherAccessSection
|
<PublisherAccessSection
|
||||||
|
|||||||
@ -138,6 +138,15 @@ const AppPublisher = ({
|
|||||||
? WORKFLOW_TYPE_SWITCH_CONFIG[appDetail.type]
|
? WORKFLOW_TYPE_SWITCH_CONFIG[appDetail.type]
|
||||||
: undefined
|
: undefined
|
||||||
const isEvaluationWorkflowType = appDetail?.type === AppTypeEnum.EVALUATION
|
const isEvaluationWorkflowType = appDetail?.type === AppTypeEnum.EVALUATION
|
||||||
|
const workflowTypeSwitchDisabledReason = useMemo(() => {
|
||||||
|
if (workflowTypeSwitchConfig?.targetType !== AppTypeEnum.EVALUATION)
|
||||||
|
return undefined
|
||||||
|
|
||||||
|
if (!hasHumanInputNode && !hasTriggerNode)
|
||||||
|
return undefined
|
||||||
|
|
||||||
|
return t('common.switchToEvaluationWorkflowDisabledTip', { ns: 'workflow' })
|
||||||
|
}, [hasHumanInputNode, hasTriggerNode, t, workflowTypeSwitchConfig?.targetType])
|
||||||
|
|
||||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
|
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
|
||||||
const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
||||||
@ -228,6 +237,10 @@ const AppPublisher = ({
|
|||||||
const handleWorkflowTypeSwitch = useCallback(async () => {
|
const handleWorkflowTypeSwitch = useCallback(async () => {
|
||||||
if (!appDetail?.id || !workflowTypeSwitchConfig)
|
if (!appDetail?.id || !workflowTypeSwitchConfig)
|
||||||
return
|
return
|
||||||
|
if (workflowTypeSwitchDisabledReason) {
|
||||||
|
toast.error(workflowTypeSwitchDisabledReason)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await convertWorkflowType({
|
await convertWorkflowType({
|
||||||
@ -252,7 +265,7 @@ const AppPublisher = ({
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}, [appDetail?.id, convertWorkflowType, handlePublish, publishedAt, setAppDetail, workflowTypeSwitchConfig])
|
}, [appDetail?.id, convertWorkflowType, handlePublish, publishedAt, setAppDetail, workflowTypeSwitchConfig, workflowTypeSwitchDisabledReason])
|
||||||
|
|
||||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
|
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -310,7 +323,8 @@ const AppPublisher = ({
|
|||||||
startNodeLimitExceeded={startNodeLimitExceeded}
|
startNodeLimitExceeded={startNodeLimitExceeded}
|
||||||
upgradeHighlightStyle={upgradeHighlightStyle}
|
upgradeHighlightStyle={upgradeHighlightStyle}
|
||||||
workflowTypeSwitchConfig={workflowTypeSwitchConfig}
|
workflowTypeSwitchConfig={workflowTypeSwitchConfig}
|
||||||
workflowTypeSwitchDisabled={publishDisabled || published || isConvertingWorkflowType}
|
workflowTypeSwitchDisabled={publishDisabled || published || isConvertingWorkflowType || Boolean(workflowTypeSwitchDisabledReason)}
|
||||||
|
workflowTypeSwitchDisabledReason={workflowTypeSwitchDisabledReason}
|
||||||
onWorkflowTypeSwitch={handleWorkflowTypeSwitch}
|
onWorkflowTypeSwitch={handleWorkflowTypeSwitch}
|
||||||
/>
|
/>
|
||||||
{!isEvaluationWorkflowType && (
|
{!isEvaluationWorkflowType && (
|
||||||
|
|||||||
@ -44,6 +44,7 @@ type SummarySectionProps = Pick<AppPublisherProps, | 'debugWithMultipleModel'
|
|||||||
tipKey: WorkflowTypeSwitchLabelKey
|
tipKey: WorkflowTypeSwitchLabelKey
|
||||||
}
|
}
|
||||||
workflowTypeSwitchDisabled: boolean
|
workflowTypeSwitchDisabled: boolean
|
||||||
|
workflowTypeSwitchDisabledReason?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessSectionProps = {
|
type AccessSectionProps = {
|
||||||
@ -100,6 +101,28 @@ export const AccessModeDisplay = ({ mode }: { mode?: keyof typeof ACCESS_MODE_MA
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ActionTooltip = ({
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
disabled: boolean
|
||||||
|
tooltip?: ReactNode
|
||||||
|
children: ReactNode
|
||||||
|
}) => {
|
||||||
|
if (!disabled || !tooltip)
|
||||||
|
return <>{children}</>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger render={<div className="flex">{children}</div>} />
|
||||||
|
<TooltipContent>
|
||||||
|
{tooltip}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const PublisherSummarySection = ({
|
export const PublisherSummarySection = ({
|
||||||
debugWithMultipleModel = false,
|
debugWithMultipleModel = false,
|
||||||
draftUpdatedAt,
|
draftUpdatedAt,
|
||||||
@ -117,6 +140,7 @@ export const PublisherSummarySection = ({
|
|||||||
upgradeHighlightStyle,
|
upgradeHighlightStyle,
|
||||||
workflowTypeSwitchConfig,
|
workflowTypeSwitchConfig,
|
||||||
workflowTypeSwitchDisabled,
|
workflowTypeSwitchDisabled,
|
||||||
|
workflowTypeSwitchDisabledReason,
|
||||||
}: SummarySectionProps) => {
|
}: SummarySectionProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -178,43 +202,45 @@ export const PublisherSummarySection = ({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{workflowTypeSwitchConfig && (
|
{workflowTypeSwitchConfig && (
|
||||||
<button
|
<ActionTooltip disabled={workflowTypeSwitchDisabled} tooltip={workflowTypeSwitchDisabledReason}>
|
||||||
type="button"
|
<button
|
||||||
className="flex h-8 w-full items-center justify-center gap-0.5 rounded-lg px-3 py-2 system-sm-medium text-text-tertiary hover:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50"
|
type="button"
|
||||||
onClick={() => void onWorkflowTypeSwitch()}
|
className="flex h-8 w-full items-center justify-center gap-0.5 rounded-lg px-3 py-2 system-sm-medium text-text-tertiary hover:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
disabled={workflowTypeSwitchDisabled}
|
onClick={() => void onWorkflowTypeSwitch()}
|
||||||
>
|
disabled={workflowTypeSwitchDisabled}
|
||||||
<span className="px-0.5">
|
>
|
||||||
{t(
|
<span className="px-0.5">
|
||||||
publishedAt
|
{t(
|
||||||
? workflowTypeSwitchConfig.switchLabelKey
|
publishedAt
|
||||||
: workflowTypeSwitchConfig.publishLabelKey,
|
? workflowTypeSwitchConfig.switchLabelKey
|
||||||
{ ns: 'workflow' },
|
: workflowTypeSwitchConfig.publishLabelKey,
|
||||||
)}
|
{ ns: 'workflow' },
|
||||||
</span>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger
|
|
||||||
render={(
|
|
||||||
<span
|
|
||||||
className="flex h-4 w-4 items-center justify-center text-text-quaternary hover:text-text-tertiary"
|
|
||||||
aria-label={t(workflowTypeSwitchConfig.tipKey, { ns: 'workflow' })}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="i-ri-question-line h-3.5 w-3.5" />
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</span>
|
||||||
<TooltipContent
|
<Tooltip>
|
||||||
placement="top"
|
<TooltipTrigger
|
||||||
popupClassName="w-[180px]"
|
render={(
|
||||||
>
|
<span
|
||||||
{t(workflowTypeSwitchConfig.tipKey, { ns: 'workflow' })}
|
className="flex h-4 w-4 items-center justify-center text-text-quaternary hover:text-text-tertiary"
|
||||||
</TooltipContent>
|
aria-label={t(workflowTypeSwitchConfig.tipKey, { ns: 'workflow' })}
|
||||||
</Tooltip>
|
onClick={(e) => {
|
||||||
</button>
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="i-ri-question-line h-3.5 w-3.5" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<TooltipContent
|
||||||
|
placement="top"
|
||||||
|
popupClassName="w-[180px]"
|
||||||
|
>
|
||||||
|
{t(workflowTypeSwitchConfig.tipKey, { ns: 'workflow' })}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</button>
|
||||||
|
</ActionTooltip>
|
||||||
)}
|
)}
|
||||||
{startNodeLimitExceeded && (
|
{startNodeLimitExceeded && (
|
||||||
<div className="mt-3 flex flex-col items-stretch">
|
<div className="mt-3 flex flex-col items-stretch">
|
||||||
@ -279,28 +305,6 @@ export const PublisherAccessSection = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActionTooltip = ({
|
|
||||||
disabled,
|
|
||||||
tooltip,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
disabled: boolean
|
|
||||||
tooltip?: ReactNode
|
|
||||||
children: ReactNode
|
|
||||||
}) => {
|
|
||||||
if (!disabled || !tooltip)
|
|
||||||
return <>{children}</>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger render={<div className="flex">{children}</div>} />
|
|
||||||
<TooltipContent>
|
|
||||||
{tooltip}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PublisherActionsSection = ({
|
export const PublisherActionsSection = ({
|
||||||
appDetail,
|
appDetail,
|
||||||
appURL,
|
appURL,
|
||||||
|
|||||||
@ -2,8 +2,21 @@ import type { ReactNode } from 'react'
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
|
import { AppTypeEnum } from '@/types/app'
|
||||||
import WorkflowOnboardingModal from '../index'
|
import WorkflowOnboardingModal from '../index'
|
||||||
|
|
||||||
|
const mockAppType = vi.hoisted<{ current?: string }>(() => ({
|
||||||
|
current: 'workflow',
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/app/store', () => ({
|
||||||
|
useStore: (selector: (state: { appDetail: { type?: string } }) => unknown) => selector({
|
||||||
|
appDetail: {
|
||||||
|
type: mockAppType.current,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/app/components/workflow/block-selector', () => ({
|
vi.mock('@/app/components/workflow/block-selector', () => ({
|
||||||
default: function MockNodeSelector({
|
default: function MockNodeSelector({
|
||||||
open,
|
open,
|
||||||
@ -44,6 +57,7 @@ describe('WorkflowOnboardingModal', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
mockAppType.current = AppTypeEnum.WORKFLOW
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderComponent = (props = {}) => {
|
const renderComponent = (props = {}) => {
|
||||||
@ -91,6 +105,15 @@ describe('WorkflowOnboardingModal', () => {
|
|||||||
expect(getTriggerHeading()).toBeInTheDocument()
|
expect(getTriggerHeading()).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should hide the trigger starter in evaluation workflows', () => {
|
||||||
|
mockAppType.current = AppTypeEnum.EVALUATION
|
||||||
|
|
||||||
|
renderComponent()
|
||||||
|
|
||||||
|
expect(getUserInputHeading()).toBeInTheDocument()
|
||||||
|
expect(screen.queryByRole('heading', { name: 'workflow.onboarding.trigger' })).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('should render ESC tip when shown', () => {
|
it('should render ESC tip when shown', () => {
|
||||||
renderComponent({ isShow: true })
|
renderComponent({ isShow: true })
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,21 @@ import { render, screen, waitFor } from '@testing-library/react'
|
|||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
|
import { AppTypeEnum } from '@/types/app'
|
||||||
import StartNodeSelectionPanel from '../start-node-selection-panel'
|
import StartNodeSelectionPanel from '../start-node-selection-panel'
|
||||||
|
|
||||||
|
const mockAppType = vi.hoisted<{ current?: string }>(() => ({
|
||||||
|
current: 'workflow',
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/app/store', () => ({
|
||||||
|
useStore: (selector: (state: { appDetail: { type?: string } }) => unknown) => selector({
|
||||||
|
appDetail: {
|
||||||
|
type: mockAppType.current,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
// Mock NodeSelector component
|
// Mock NodeSelector component
|
||||||
vi.mock('@/app/components/workflow/block-selector', () => ({
|
vi.mock('@/app/components/workflow/block-selector', () => ({
|
||||||
default: function MockNodeSelector({
|
default: function MockNodeSelector({
|
||||||
@ -61,6 +74,7 @@ describe('StartNodeSelectionPanel', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
mockAppType.current = AppTypeEnum.WORKFLOW
|
||||||
})
|
})
|
||||||
|
|
||||||
// Helper function to render component
|
// Helper function to render component
|
||||||
@ -96,6 +110,15 @@ describe('StartNodeSelectionPanel', () => {
|
|||||||
expect(screen.getByText('workflow.onboarding.triggerDescription')).toBeInTheDocument()
|
expect(screen.getByText('workflow.onboarding.triggerDescription')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should hide the trigger option in evaluation workflows', () => {
|
||||||
|
mockAppType.current = AppTypeEnum.EVALUATION
|
||||||
|
|
||||||
|
renderComponent()
|
||||||
|
|
||||||
|
expect(screen.queryByText('workflow.onboarding.trigger')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('node-selector')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('should render node selector component', () => {
|
it('should render node selector component', () => {
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
renderComponent()
|
renderComponent()
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import type { FC } from 'react'
|
|||||||
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
|
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { Home, TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
import { Home, TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||||
import NodeSelector from '@/app/components/workflow/block-selector'
|
import NodeSelector from '@/app/components/workflow/block-selector'
|
||||||
import { TabsEnum } from '@/app/components/workflow/block-selector/types'
|
import { TabsEnum } from '@/app/components/workflow/block-selector/types'
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
|
import { isEvaluationWorkflow } from '@/app/components/workflow/utils/evaluation-workflow'
|
||||||
import StartNodeOption from './start-node-option'
|
import StartNodeOption from './start-node-option'
|
||||||
|
|
||||||
type StartNodeSelectionPanelProps = {
|
type StartNodeSelectionPanelProps = {
|
||||||
@ -19,7 +21,9 @@ const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
|
|||||||
onSelectTrigger,
|
onSelectTrigger,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const appType = useAppStore(s => s.appDetail?.type)
|
||||||
const [showTriggerSelector, setShowTriggerSelector] = useState(false)
|
const [showTriggerSelector, setShowTriggerSelector] = useState(false)
|
||||||
|
const isEvaluationWorkflowType = isEvaluationWorkflow(appType)
|
||||||
|
|
||||||
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
|
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
|
||||||
setShowTriggerSelector(false)
|
setShowTriggerSelector(false)
|
||||||
@ -39,34 +43,36 @@ const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
|
|||||||
onClick={onSelectUserInput}
|
onClick={onSelectUserInput}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NodeSelector
|
{!isEvaluationWorkflowType && (
|
||||||
open={showTriggerSelector}
|
<NodeSelector
|
||||||
onOpenChange={setShowTriggerSelector}
|
open={showTriggerSelector}
|
||||||
onSelect={handleTriggerSelect}
|
onOpenChange={setShowTriggerSelector}
|
||||||
placement="right"
|
onSelect={handleTriggerSelect}
|
||||||
offset={-200}
|
placement="right"
|
||||||
noBlocks={true}
|
offset={-200}
|
||||||
showStartTab={true}
|
noBlocks={true}
|
||||||
defaultActiveTab={TabsEnum.Start}
|
showStartTab={true}
|
||||||
forceShowStartContent={true}
|
defaultActiveTab={TabsEnum.Start}
|
||||||
availableBlocksTypes={[
|
forceShowStartContent={true}
|
||||||
BlockEnum.TriggerSchedule,
|
availableBlocksTypes={[
|
||||||
BlockEnum.TriggerWebhook,
|
BlockEnum.TriggerSchedule,
|
||||||
BlockEnum.TriggerPlugin,
|
BlockEnum.TriggerWebhook,
|
||||||
]}
|
BlockEnum.TriggerPlugin,
|
||||||
trigger={() => (
|
]}
|
||||||
<StartNodeOption
|
trigger={() => (
|
||||||
icon={(
|
<StartNodeOption
|
||||||
<div className="flex h-9 w-9 items-center justify-center radius-lg border-[0.5px] border-transparent bg-util-colors-blue-brand-blue-brand-500 p-2">
|
icon={(
|
||||||
<TriggerAll className="h-5 w-5 text-white" />
|
<div className="flex h-9 w-9 items-center justify-center radius-lg border-[0.5px] border-transparent bg-util-colors-blue-brand-blue-brand-500 p-2">
|
||||||
</div>
|
<TriggerAll className="h-5 w-5 text-white" />
|
||||||
)}
|
</div>
|
||||||
title={t('onboarding.trigger', { ns: 'workflow' })}
|
)}
|
||||||
description={t('onboarding.triggerDescription', { ns: 'workflow' })}
|
title={t('onboarding.trigger', { ns: 'workflow' })}
|
||||||
onClick={() => setShowTriggerSelector(true)}
|
description={t('onboarding.triggerDescription', { ns: 'workflow' })}
|
||||||
/>
|
onClick={() => setShowTriggerSelector(true)}
|
||||||
)}
|
/>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,25 @@
|
|||||||
import { renderHook } from '@testing-library/react'
|
import { renderHook } from '@testing-library/react'
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
|
import { AppTypeEnum } from '@/types/app'
|
||||||
import { useAvailableNodesMetaData } from '../use-available-nodes-meta-data'
|
import { useAvailableNodesMetaData } from '../use-available-nodes-meta-data'
|
||||||
|
|
||||||
const mockUseIsChatMode = vi.fn()
|
const mockUseIsChatMode = vi.fn()
|
||||||
|
const mockAppType = vi.hoisted<{ current?: string }>(() => ({
|
||||||
|
current: 'workflow',
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/app/components/workflow-app/hooks/use-is-chat-mode', () => ({
|
vi.mock('@/app/components/workflow-app/hooks/use-is-chat-mode', () => ({
|
||||||
useIsChatMode: () => mockUseIsChatMode(),
|
useIsChatMode: () => mockUseIsChatMode(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/app/store', () => ({
|
||||||
|
useStore: (selector: (state: { appDetail: { type?: string } }) => unknown) => selector({
|
||||||
|
appDetail: {
|
||||||
|
type: mockAppType.current,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/i18n', () => ({
|
vi.mock('@/context/i18n', () => ({
|
||||||
useDocLink: () => (path: string) => `/docs${path}`,
|
useDocLink: () => (path: string) => `/docs${path}`,
|
||||||
}))
|
}))
|
||||||
@ -15,6 +27,7 @@ vi.mock('@/context/i18n', () => ({
|
|||||||
describe('useAvailableNodesMetaData', () => {
|
describe('useAvailableNodesMetaData', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
mockAppType.current = AppTypeEnum.WORKFLOW
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should include chat-specific nodes and make the start node undeletable in chat mode', () => {
|
it('should include chat-specific nodes and make the start node undeletable in chat mode', () => {
|
||||||
@ -46,4 +59,18 @@ describe('useAvailableNodesMetaData', () => {
|
|||||||
title: 'workflow.blocks.start',
|
title: 'workflow.blocks.start',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should exclude human input and trigger nodes in evaluation workflows', () => {
|
||||||
|
mockUseIsChatMode.mockReturnValue(false)
|
||||||
|
mockAppType.current = AppTypeEnum.EVALUATION
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||||
|
|
||||||
|
expect(result.current.nodesMap?.[BlockEnum.Start]).toBeDefined()
|
||||||
|
expect(result.current.nodesMap?.[BlockEnum.End]).toBeDefined()
|
||||||
|
expect(result.current.nodesMap?.[BlockEnum.HumanInput]).toBeUndefined()
|
||||||
|
expect(result.current.nodesMap?.[BlockEnum.TriggerWebhook]).toBeUndefined()
|
||||||
|
expect(result.current.nodesMap?.[BlockEnum.TriggerSchedule]).toBeUndefined()
|
||||||
|
expect(result.current.nodesMap?.[BlockEnum.TriggerPlugin]).toBeUndefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-sto
|
|||||||
import type { DocPathWithoutLang } from '@/types/doc-paths'
|
import type { DocPathWithoutLang } from '@/types/doc-paths'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
|
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
|
||||||
import AnswerDefault from '@/app/components/workflow/nodes/answer/default'
|
import AnswerDefault from '@/app/components/workflow/nodes/answer/default'
|
||||||
import EndDefault from '@/app/components/workflow/nodes/end/default'
|
import EndDefault from '@/app/components/workflow/nodes/end/default'
|
||||||
@ -10,13 +11,16 @@ import TriggerPluginDefault from '@/app/components/workflow/nodes/trigger-plugin
|
|||||||
import TriggerScheduleDefault from '@/app/components/workflow/nodes/trigger-schedule/default'
|
import TriggerScheduleDefault from '@/app/components/workflow/nodes/trigger-schedule/default'
|
||||||
import TriggerWebhookDefault from '@/app/components/workflow/nodes/trigger-webhook/default'
|
import TriggerWebhookDefault from '@/app/components/workflow/nodes/trigger-webhook/default'
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
|
import { isEvaluationWorkflow, isEvaluationWorkflowRestrictedNodeType } from '@/app/components/workflow/utils/evaluation-workflow'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
import { useIsChatMode } from './use-is-chat-mode'
|
import { useIsChatMode } from './use-is-chat-mode'
|
||||||
|
|
||||||
export const useAvailableNodesMetaData = () => {
|
export const useAvailableNodesMetaData = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isChatMode = useIsChatMode()
|
const isChatMode = useIsChatMode()
|
||||||
|
const appType = useAppStore(s => s.appDetail?.type)
|
||||||
const docLink = useDocLink()
|
const docLink = useDocLink()
|
||||||
|
const isEvaluationWorkflowType = isEvaluationWorkflow(appType)
|
||||||
|
|
||||||
const startNodeMetaData = useMemo(() => ({
|
const startNodeMetaData = useMemo(() => ({
|
||||||
...StartDefault,
|
...StartDefault,
|
||||||
@ -26,20 +30,27 @@ export const useAvailableNodesMetaData = () => {
|
|||||||
},
|
},
|
||||||
}), [isChatMode])
|
}), [isChatMode])
|
||||||
|
|
||||||
const mergedNodesMetaData = useMemo(() => [
|
const mergedNodesMetaData = useMemo(() => {
|
||||||
...WORKFLOW_COMMON_NODES,
|
const nodes = [
|
||||||
startNodeMetaData,
|
...WORKFLOW_COMMON_NODES,
|
||||||
...(
|
startNodeMetaData,
|
||||||
isChatMode
|
...(
|
||||||
? [AnswerDefault]
|
isChatMode
|
||||||
: [
|
? [AnswerDefault]
|
||||||
EndDefault,
|
: [
|
||||||
TriggerWebhookDefault,
|
EndDefault,
|
||||||
TriggerScheduleDefault,
|
TriggerWebhookDefault,
|
||||||
TriggerPluginDefault,
|
TriggerScheduleDefault,
|
||||||
]
|
TriggerPluginDefault,
|
||||||
),
|
]
|
||||||
], [isChatMode, startNodeMetaData])
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!isEvaluationWorkflowType)
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
return nodes.filter(node => !isEvaluationWorkflowRestrictedNodeType(node.metaData.type))
|
||||||
|
}, [isChatMode, isEvaluationWorkflowType, startNodeMetaData])
|
||||||
|
|
||||||
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
|
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
|
||||||
const { metaData } = node
|
const { metaData } = node
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { useGetLanguage, useLocale } from '@/context/i18n'
|
|||||||
import useTheme from '@/hooks/use-theme'
|
import useTheme from '@/hooks/use-theme'
|
||||||
import { useFeaturedTriggersRecommendations } from '@/service/use-plugins'
|
import { useFeaturedTriggersRecommendations } from '@/service/use-plugins'
|
||||||
import { useAllTriggerPlugins, useInvalidateAllTriggerPlugins } from '@/service/use-triggers'
|
import { useAllTriggerPlugins, useInvalidateAllTriggerPlugins } from '@/service/use-triggers'
|
||||||
import { Theme } from '@/types/app'
|
import { AppTypeEnum, Theme } from '@/types/app'
|
||||||
import { defaultSystemFeatures } from '@/types/feature'
|
import { defaultSystemFeatures } from '@/types/feature'
|
||||||
import { useAvailableNodesMetaData } from '../../../workflow-app/hooks'
|
import { useAvailableNodesMetaData } from '../../../workflow-app/hooks'
|
||||||
import useNodes from '../../store/workflow/use-nodes'
|
import useNodes from '../../store/workflow/use-nodes'
|
||||||
@ -19,6 +19,18 @@ vi.mock('@/context/global-public-context', () => ({
|
|||||||
useGlobalPublicStore: vi.fn(),
|
useGlobalPublicStore: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const mockAppType = vi.hoisted<{ current?: string }>(() => ({
|
||||||
|
current: 'workflow',
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/app/store', () => ({
|
||||||
|
useStore: (selector: (state: { appDetail: { type?: string } }) => unknown) => selector({
|
||||||
|
appDetail: {
|
||||||
|
type: mockAppType.current,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/i18n', () => ({
|
vi.mock('@/context/i18n', () => ({
|
||||||
useGetLanguage: vi.fn(),
|
useGetLanguage: vi.fn(),
|
||||||
useLocale: vi.fn(),
|
useLocale: vi.fn(),
|
||||||
@ -179,6 +191,7 @@ const createAvailableNodesMetaData = (): ReturnType<typeof useAvailableNodesMeta
|
|||||||
describe('AllStartBlocks', () => {
|
describe('AllStartBlocks', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
mockAppType.current = AppTypeEnum.WORKFLOW
|
||||||
mockUseGlobalPublicStore.mockImplementation(selector => selector(createGlobalPublicStoreState(false)))
|
mockUseGlobalPublicStore.mockImplementation(selector => selector(createGlobalPublicStoreState(false)))
|
||||||
mockUseGetLanguage.mockReturnValue('en_US')
|
mockUseGetLanguage.mockReturnValue('en_US')
|
||||||
mockUseLocale.mockReturnValue('en_US')
|
mockUseLocale.mockReturnValue('en_US')
|
||||||
@ -238,6 +251,26 @@ describe('AllStartBlocks', () => {
|
|||||||
|
|
||||||
expect(await screen.findByRole('link', { name: /plugin\.findMoreInMarketplace/ })).toHaveAttribute('href', 'https://marketplace.test/start')
|
expect(await screen.findByRole('link', { name: /plugin\.findMoreInMarketplace/ })).toHaveAttribute('href', 'https://marketplace.test/start')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should hide trigger options in evaluation workflows', async () => {
|
||||||
|
mockAppType.current = AppTypeEnum.EVALUATION
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AllStartBlocks
|
||||||
|
searchText=""
|
||||||
|
onSelect={vi.fn()}
|
||||||
|
availableBlocksTypes={[BlockEnum.Start, BlockEnum.TriggerPlugin]}
|
||||||
|
allowUserInputSelection
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.queryByText('Provider One')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('workflow.tabs.allTriggers')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Empty filter states should surface the request-to-community fallback.
|
// Empty filter states should surface the request-to-community fallback.
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { useStoreApi } from 'reactflow'
|
||||||
|
import { AppTypeEnum } from '@/types/app'
|
||||||
|
import { BlockEnum } from '../../types'
|
||||||
|
import Blocks from '../blocks'
|
||||||
|
|
||||||
|
const mockGetNodes = vi.fn(() => [])
|
||||||
|
const mockAppType = vi.hoisted<{ current?: string }>(() => ({
|
||||||
|
current: 'workflow',
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('reactflow', () => ({
|
||||||
|
useStoreApi: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/app/store', () => ({
|
||||||
|
useStore: (selector: (state: { appDetail: { type?: string } }) => unknown) => selector({
|
||||||
|
appDetail: {
|
||||||
|
type: mockAppType.current,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockUseStoreApi = vi.mocked(useStoreApi)
|
||||||
|
|
||||||
|
describe('Blocks', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mockAppType.current = AppTypeEnum.WORKFLOW
|
||||||
|
mockUseStoreApi.mockReturnValue({
|
||||||
|
getState: () => ({
|
||||||
|
getNodes: mockGetNodes,
|
||||||
|
}),
|
||||||
|
} as unknown as ReturnType<typeof useStoreApi>)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide human input in evaluation workflows', () => {
|
||||||
|
mockAppType.current = AppTypeEnum.EVALUATION
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Blocks
|
||||||
|
searchText=""
|
||||||
|
onSelect={vi.fn()}
|
||||||
|
availableBlocksTypes={[BlockEnum.HumanInput, BlockEnum.LLM]}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByText('workflow.blocks.human-input')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByText('workflow.blocks.llm')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -14,9 +14,11 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
||||||
|
import { filterEvaluationWorkflowRestrictedBlockTypes, isEvaluationWorkflow } from '@/app/components/workflow/utils/evaluation-workflow'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import Link from '@/next/link'
|
import Link from '@/next/link'
|
||||||
import { useFeaturedTriggersRecommendations } from '@/service/use-plugins'
|
import { useFeaturedTriggersRecommendations } from '@/service/use-plugins'
|
||||||
@ -54,13 +56,21 @@ const AllStartBlocks = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false)
|
const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false)
|
||||||
const [hasPluginContent, setHasPluginContent] = useState(false)
|
const [hasPluginContent, setHasPluginContent] = useState(false)
|
||||||
|
const appType = useAppStore(s => s.appDetail?.type)
|
||||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||||
const pluginRef = useRef<ListRef>(null)
|
const pluginRef = useRef<ListRef>(null)
|
||||||
const wrapElemRef = useRef<HTMLDivElement>(null)
|
const wrapElemRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const entryNodeTypes = availableBlocksTypes?.length
|
const entryNodeTypes = useMemo(() => {
|
||||||
? availableBlocksTypes
|
const blockTypes = availableBlocksTypes?.length
|
||||||
: ENTRY_NODE_TYPES
|
? availableBlocksTypes
|
||||||
|
: [...ENTRY_NODE_TYPES]
|
||||||
|
|
||||||
|
if (!isEvaluationWorkflow(appType))
|
||||||
|
return blockTypes
|
||||||
|
|
||||||
|
return filterEvaluationWorkflowRestrictedBlockTypes(blockTypes)
|
||||||
|
}, [appType, availableBlocksTypes])
|
||||||
const enableTriggerPlugin = entryNodeTypes.includes(BlockEnumValue.TriggerPlugin)
|
const enableTriggerPlugin = entryNodeTypes.includes(BlockEnumValue.TriggerPlugin)
|
||||||
const { data: triggerProviders = [] } = useAllTriggerPlugins(enableTriggerPlugin)
|
const { data: triggerProviders = [] } = useAllTriggerPlugins(enableTriggerPlugin)
|
||||||
const providerMap = useMemo(() => {
|
const providerMap = useMemo(() => {
|
||||||
@ -94,7 +104,8 @@ const AllStartBlocks = ({
|
|||||||
const shouldShowFeatured = enableTriggerPlugin
|
const shouldShowFeatured = enableTriggerPlugin
|
||||||
&& enable_marketplace
|
&& enable_marketplace
|
||||||
&& !hasFilter
|
&& !hasFilter
|
||||||
const shouldShowTriggerListTitle = hasStartBlocksContent || hasPluginContent
|
const hasTriggerOptions = entryNodeTypes.some(type => type !== BlockEnumValue.Start)
|
||||||
|
const shouldShowTriggerListTitle = hasTriggerOptions && (hasStartBlocksContent || hasPluginContent)
|
||||||
const shouldShowMarketplaceFooter = enable_marketplace && !hasFilter
|
const shouldShowMarketplaceFooter = enable_marketplace && !hasFilter
|
||||||
|
|
||||||
const handleStartBlocksContentChange = useCallback((hasContent: boolean) => {
|
const handleStartBlocksContentChange = useCallback((hasContent: boolean) => {
|
||||||
|
|||||||
@ -8,8 +8,10 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useStoreApi } from 'reactflow'
|
import { useStoreApi } from 'reactflow'
|
||||||
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import Badge from '@/app/components/base/badge'
|
import Badge from '@/app/components/base/badge'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import { filterEvaluationWorkflowRestrictedBlockTypes, isEvaluationWorkflow } from '@/app/components/workflow/utils/evaluation-workflow'
|
||||||
import BlockIcon from '../block-icon'
|
import BlockIcon from '../block-icon'
|
||||||
import { BlockEnum } from '../types'
|
import { BlockEnum } from '../types'
|
||||||
import { BLOCK_CLASSIFICATIONS } from './constants'
|
import { BLOCK_CLASSIFICATIONS } from './constants'
|
||||||
@ -29,7 +31,14 @@ const Blocks = ({
|
|||||||
}: BlocksProps) => {
|
}: BlocksProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const store = useStoreApi()
|
const store = useStoreApi()
|
||||||
|
const appType = useAppStore(s => s.appDetail?.type)
|
||||||
const blocksFromHooks = useBlocks()
|
const blocksFromHooks = useBlocks()
|
||||||
|
const filteredAvailableBlocksTypes = useMemo(() => {
|
||||||
|
if (!isEvaluationWorkflow(appType))
|
||||||
|
return availableBlocksTypes
|
||||||
|
|
||||||
|
return filterEvaluationWorkflowRestrictedBlockTypes(availableBlocksTypes)
|
||||||
|
}, [appType, availableBlocksTypes])
|
||||||
|
|
||||||
// Use external blocks if provided, otherwise fallback to hook-based blocks
|
// Use external blocks if provided, otherwise fallback to hook-based blocks
|
||||||
const blocks = blocksFromProps || blocksFromHooks.map(block => ({
|
const blocks = blocksFromProps || blocksFromHooks.map(block => ({
|
||||||
@ -57,7 +66,7 @@ const Blocks = ({
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return block.metaData.title.toLowerCase().includes(searchText.toLowerCase()) && availableBlocksTypes.includes(block.metaData.type)
|
return block.metaData.title.toLowerCase().includes(searchText.toLowerCase()) && filteredAvailableBlocksTypes.includes(block.metaData.type)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -65,7 +74,7 @@ const Blocks = ({
|
|||||||
[classification]: list,
|
[classification]: list,
|
||||||
}
|
}
|
||||||
}, {} as Record<string, typeof blocks>)
|
}, {} as Record<string, typeof blocks>)
|
||||||
}, [blocks, searchText, availableBlocksTypes])
|
}, [blocks, filteredAvailableBlocksTypes, searchText])
|
||||||
const isEmpty = Object.values(groups).every(list => !list.length)
|
const isEmpty = Object.values(groups).every(list => !list.length)
|
||||||
|
|
||||||
const renderGroup = useCallback((classification: BlockClassificationEnum) => {
|
const renderGroup = useCallback((classification: BlockClassificationEnum) => {
|
||||||
|
|||||||
20
web/app/components/workflow/utils/evaluation-workflow.ts
Normal file
20
web/app/components/workflow/utils/evaluation-workflow.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { AppTypeEnum } from '@/types/app'
|
||||||
|
import { BlockEnum, TRIGGER_NODE_TYPES } from '../types'
|
||||||
|
|
||||||
|
const EVALUATION_WORKFLOW_RESTRICTED_NODE_TYPES = new Set<string>([
|
||||||
|
BlockEnum.HumanInput,
|
||||||
|
...TRIGGER_NODE_TYPES,
|
||||||
|
])
|
||||||
|
|
||||||
|
export const isEvaluationWorkflow = (appType?: string) => appType === AppTypeEnum.EVALUATION
|
||||||
|
|
||||||
|
export const isEvaluationWorkflowRestrictedNodeType = (nodeType?: string) => {
|
||||||
|
if (!nodeType)
|
||||||
|
return false
|
||||||
|
|
||||||
|
return EVALUATION_WORKFLOW_RESTRICTED_NODE_TYPES.has(nodeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filterEvaluationWorkflowRestrictedBlockTypes = (blockTypes: BlockEnum[]) => {
|
||||||
|
return blockTypes.filter(blockType => !isEvaluationWorkflowRestrictedNodeType(blockType))
|
||||||
|
}
|
||||||
@ -216,6 +216,7 @@
|
|||||||
"common.setVarValuePlaceholder": "Set variable",
|
"common.setVarValuePlaceholder": "Set variable",
|
||||||
"common.showRunHistory": "Show Run History",
|
"common.showRunHistory": "Show Run History",
|
||||||
"common.switchToEvaluationWorkflow": "Switch to Evaluation Workflow",
|
"common.switchToEvaluationWorkflow": "Switch to Evaluation Workflow",
|
||||||
|
"common.switchToEvaluationWorkflowDisabledTip": "Evaluation workflows do not support Human Input nodes or Trigger start nodes.",
|
||||||
"common.switchToEvaluationWorkflowTip": "Turns this workflow into a custom evaluator for batch testing. Disables public Web App access.",
|
"common.switchToEvaluationWorkflowTip": "Turns this workflow into a custom evaluator for batch testing. Disables public Web App access.",
|
||||||
"common.switchToStandardWorkflow": "Switch to Standard Workflow",
|
"common.switchToStandardWorkflow": "Switch to Standard Workflow",
|
||||||
"common.switchToStandardWorkflowTip": "Turns this evaluator back into a standard workflow and restores public Web App access.",
|
"common.switchToStandardWorkflowTip": "Turns this evaluator back into a standard workflow and restores public Web App access.",
|
||||||
|
|||||||
@ -216,6 +216,7 @@
|
|||||||
"common.setVarValuePlaceholder": "设置变量值",
|
"common.setVarValuePlaceholder": "设置变量值",
|
||||||
"common.showRunHistory": "显示运行历史",
|
"common.showRunHistory": "显示运行历史",
|
||||||
"common.switchToEvaluationWorkflow": "切换为评测工作流",
|
"common.switchToEvaluationWorkflow": "切换为评测工作流",
|
||||||
|
"common.switchToEvaluationWorkflowDisabledTip": "评测工作流不支持 Human Input 节点或 Trigger 开始节点。",
|
||||||
"common.switchToEvaluationWorkflowTip": "将当前工作流转换为批量测试用的自定义评测器,并禁用公开 Web App 访问。",
|
"common.switchToEvaluationWorkflowTip": "将当前工作流转换为批量测试用的自定义评测器,并禁用公开 Web App 访问。",
|
||||||
"common.switchToStandardWorkflow": "切换为标准工作流",
|
"common.switchToStandardWorkflow": "切换为标准工作流",
|
||||||
"common.switchToStandardWorkflowTip": "将当前评测器转换回标准工作流,并恢复公开 Web App 访问。",
|
"common.switchToStandardWorkflowTip": "将当前评测器转换回标准工作流,并恢复公开 Web App 访问。",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user