Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox

This commit is contained in:
yyh
2026-03-25 12:45:48 +08:00
55 changed files with 342 additions and 448 deletions

View File

@ -19,11 +19,13 @@ vi.mock('@/app/components/base/file-uploader/hooks', () => ({
useFileSizeLimit: vi.fn(),
}))
vi.mock('@/app/components/base/toast/context', () => ({
useToastContext: () => ({
notify: vi.fn(),
close: vi.fn(),
}),
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
},
}))
const createPayload = (overrides: Partial<UploadFileSetting> = {}): UploadFileSetting => ({

View File

@ -10,7 +10,7 @@ import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import SingleRunForm from '@/app/components/workflow/nodes/human-input/components/single-run-form'
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
@ -126,10 +126,7 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
})
})
if (errMsg) {
Toast.notify({
message: errMsg,
type: 'error',
})
toast.error(errMsg)
return
}
@ -147,10 +144,7 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
})
})
if (parseErrorJsonField) {
Toast.notify({
message: t('errorMsg.invalidJson', { ns: 'workflow', field: parseErrorJsonField }),
type: 'error',
})
toast.error(t('errorMsg.invalidJson', { ns: 'workflow', field: parseErrorJsonField }))
return
}

View File

@ -6,7 +6,7 @@ import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/ty
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import {
useNodesSyncDraft,
} from '@/app/components/workflow/hooks'
@ -183,7 +183,7 @@ const useLastRun = <T>({
return false
const message = warningForNode.errorMessages[0] || 'This node has unresolved checklist issues'
Toast.notify({ type: 'error', message })
toast.error(message)
return true
}, [warningNodes, currentNodeId])
@ -304,13 +304,10 @@ const useLastRun = <T>({
if (!inspectVarValue) {
const nodeLabel = getContextNodeLabel(nodeId)
|| t('nodes.llm.contextUnknownNode', { ns: 'workflow' })
Toast.notify({
type: 'error',
message: t('nodes.llm.contextMissing', {
ns: 'workflow',
nodeName: nodeLabel,
}),
})
toast.error(t('nodes.llm.contextMissing', {
ns: 'workflow',
nodeName: nodeLabel,
}))
return false
}
}
@ -328,13 +325,10 @@ const useLastRun = <T>({
const dependentVars = singleRunParams?.getDependentVars?.()
const nullOutput = getNullDependentOutput(dependentVars)
if (nullOutput) {
Toast.notify({
type: 'error',
message: t('singleRun.subgraph.nullOutputError', {
ns: 'workflow',
output: formatSubgraphOutputLabel(nullOutput),
}),
})
toast.error(t('singleRun.subgraph.nullOutputError', {
ns: 'workflow',
output: formatSubgraphOutputLabel(nullOutput),
}))
return
}
setNodeRunning()
@ -442,13 +436,10 @@ const useLastRun = <T>({
const dependentVars = singleRunParams?.getDependentVars?.()
const nullOutput = getNullDependentOutput(dependentVars)
if (nullOutput) {
Toast.notify({
type: 'error',
message: t('singleRun.subgraph.nullOutputError', {
ns: 'workflow',
output: formatSubgraphOutputLabel(nullOutput),
}),
})
toast.error(t('singleRun.subgraph.nullOutputError', {
ns: 'workflow',
output: formatSubgraphOutputLabel(nullOutput),
}))
return
}
if (blockType === BlockEnum.TriggerWebhook || blockType === BlockEnum.TriggerPlugin || blockType === BlockEnum.TriggerSchedule)

View File

@ -13,7 +13,7 @@ import {
import { useShallow } from 'zustand/react/shallow'
import { trackEvent } from '@/app/components/base/amplitude'
import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import {
useIsChatMode,
useNodeDataUpdate,
@ -429,14 +429,14 @@ const useOneStepRun = <T>({
})
if (!response) {
const message = 'Schedule trigger run failed'
Toast.notify({ type: 'error', message })
const message = t('common.scheduleTriggerRunFailed', { ns: 'workflow' })
toast.error(message)
throw new Error(message)
}
if (response?.status === 'error') {
const message = response?.message || 'Schedule trigger run failed'
Toast.notify({ type: 'error', message })
const message = response?.message || t('common.scheduleTriggerRunFailed', { ns: 'workflow' })
toast.error(message)
throw new Error(message)
}
@ -461,10 +461,10 @@ const useOneStepRun = <T>({
_singleRunningStatus: NodeRunningStatus.Failed,
},
})
Toast.notify({ type: 'error', message: 'Schedule trigger run failed' })
toast.error(t('common.scheduleTriggerRunFailed', { ns: 'workflow' }))
throw error
}
}, [flowId, id, handleNodeDataUpdate, data])
}, [flowId, id, handleNodeDataUpdate, data, t])
const runWebhookSingleRun = useCallback(async (): Promise<any | null> => {
const urlPath = `/apps/${flowId}/workflows/draft/nodes/${id}/trigger/run`
@ -486,8 +486,8 @@ const useOneStepRun = <T>({
return null
if (!response) {
const message = response?.message || 'Webhook debug failed'
Toast.notify({ type: 'error', message })
const message = response?.message || t('common.webhookDebugFailed', { ns: 'workflow' })
toast.error(message)
cancelWebhookSingleRun()
throw new Error(message)
}
@ -514,8 +514,8 @@ const useOneStepRun = <T>({
}
if (response?.status === 'error') {
const message = response.message || 'Webhook debug failed'
Toast.notify({ type: 'error', message })
const message = response.message || t('common.webhookDebugFailed', { ns: 'workflow' })
toast.error(message)
cancelWebhookSingleRun()
throw new Error(message)
}
@ -538,7 +538,7 @@ const useOneStepRun = <T>({
if (controller.signal.aborted)
return null
Toast.notify({ type: 'error', message: 'Webhook debug request failed' })
toast.error(t('common.webhookDebugRequestFailed', { ns: 'workflow' }))
cancelWebhookSingleRun()
if (error instanceof Error)
throw error
@ -550,7 +550,7 @@ const useOneStepRun = <T>({
}
return null
}, [flowId, id, data, handleNodeDataUpdate, cancelWebhookSingleRun])
}, [flowId, id, data, handleNodeDataUpdate, cancelWebhookSingleRun, t])
const runPluginSingleRun = useCallback(async (): Promise<any | null> => {
const urlPath = `/apps/${flowId}/workflows/draft/nodes/${id}/trigger/run`
@ -585,14 +585,14 @@ const useOneStepRun = <T>({
if (controller.signal.aborted)
return null
Toast.notify({ type: 'error', message: requestError.message })
toast.error(requestError.message)
cancelPluginSingleRun()
throw requestError
}
if (!response) {
const message = 'Plugin debug failed'
Toast.notify({ type: 'error', message })
toast.error(message)
cancelPluginSingleRun()
throw new Error(message)
}
@ -619,7 +619,7 @@ const useOneStepRun = <T>({
if (response?.status === 'error') {
const message = response.message || 'Plugin debug failed'
Toast.notify({ type: 'error', message })
toast.error(message)
cancelPluginSingleRun()
throw new Error(message)
}
@ -652,10 +652,8 @@ const useOneStepRun = <T>({
_isSingleRun: false,
},
})
Toast.notify({
type: 'error',
message: res.errorMessage || '',
})
if (res.errorMessage)
toast.error(res.errorMessage)
}
return res
}, [checkValid, data, handleNodeDataUpdate, id, t, moreDataForCheckValid])

View File

@ -1,15 +1,16 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { toast } from '@/app/components/base/ui/toast'
import { BodyPayloadValueType, BodyType } from '../../types'
import CurlPanel from '../curl-panel'
import * as curlParser from '../curl-parser'
const {
mockHandleNodeSelect,
mockNotify,
mockToastError,
} = vi.hoisted(() => ({
mockHandleNodeSelect: vi.fn(),
mockNotify: vi.fn(),
mockToastError: vi.fn(),
}))
vi.mock('@/app/components/workflow/hooks', () => ({
@ -18,9 +19,9 @@ vi.mock('@/app/components/workflow/hooks', () => ({
}),
}))
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: mockNotify,
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
error: mockToastError,
},
}))
@ -131,9 +132,7 @@ describe('curl-panel', () => {
await user.type(screen.getByRole('textbox'), 'invalid')
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
}))
expect(vi.mocked(toast.error)).toHaveBeenCalledWith(expect.stringContaining('Invalid cURL command'))
})
it('should keep the panel open when parsing returns no node and no error', async () => {
@ -159,7 +158,7 @@ describe('curl-panel', () => {
expect(onHide).not.toHaveBeenCalled()
expect(handleCurlImport).not.toHaveBeenCalled()
expect(mockHandleNodeSelect).not.toHaveBeenCalled()
expect(mockNotify).not.toHaveBeenCalled()
expect(vi.mocked(toast.error)).not.toHaveBeenCalled()
})
})
})

View File

@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { useNodesInteractions } from '@/app/components/workflow/hooks'
import { parseCurl } from './curl-parser'
@ -26,10 +26,7 @@ const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {
const handleSave = useCallback(() => {
const { node, error } = parseCurl(inputString)
if (error) {
Toast.notify({
type: 'error',
message: error,
})
toast.error(error)
return
}
if (!node)

View File

@ -12,7 +12,7 @@ import Divider from '@/app/components/base/divider'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import MailBodyInput from './mail-body-input'
import Recipient from './recipient'
@ -45,31 +45,22 @@ const EmailConfigureModal = ({
const checkValidConfig = useCallback(() => {
if (!subject.trim()) {
Toast.notify({
type: 'error',
message: 'subject is required',
})
toast.error(t(`${i18nPrefix}.deliveryMethod.emailConfigure.subjectRequired`, { ns: 'workflow' }))
return false
}
if (!body.trim()) {
Toast.notify({
type: 'error',
message: 'body is required',
})
toast.error(t(`${i18nPrefix}.deliveryMethod.emailConfigure.bodyRequired`, { ns: 'workflow' }))
return false
}
if (!/\{\{#url#\}\}/.test(body.trim())) {
Toast.notify({
type: 'error',
message: `body must contain one ${t('promptEditor.requestURL.item.title', { ns: 'common' })}`,
})
toast.error(t(`${i18nPrefix}.deliveryMethod.emailConfigure.bodyMustContainRequestURL`, {
ns: 'workflow',
field: t('promptEditor.requestURL.item.title', { ns: 'common' }),
}))
return false
}
if (!recipients || (recipients.items.length === 0 && !recipients.whole_workspace)) {
Toast.notify({
type: 'error',
message: 'recipients is required',
})
toast.error(t(`${i18nPrefix}.deliveryMethod.emailConfigure.recipientsRequired`, { ns: 'workflow' }))
return false
}
return true

View File

@ -7,7 +7,7 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import ButtonStyleDropdown from './button-style-dropdown'
const i18nPrefix = 'nodes.humanInput'
@ -47,14 +47,14 @@ const UserActionItem: FC<UserActionItemProps> = ({
.join('')
if (sanitized !== withUnderscores) {
Toast.notify({ type: 'error', message: t(`${i18nPrefix}.userActions.actionIdFormatTip`, { ns: 'workflow' }) })
toast.error(t(`${i18nPrefix}.userActions.actionIdFormatTip`, { ns: 'workflow' }))
return
}
// Limit to 20 characters
if (sanitized.length > ACTION_ID_MAX_LENGTH) {
sanitized = sanitized.slice(0, ACTION_ID_MAX_LENGTH)
Toast.notify({ type: 'error', message: t(`${i18nPrefix}.userActions.actionIdTooLong`, { ns: 'workflow', maxLength: ACTION_ID_MAX_LENGTH }) })
toast.error(t(`${i18nPrefix}.userActions.actionIdTooLong`, { ns: 'workflow', maxLength: ACTION_ID_MAX_LENGTH }))
}
if (sanitized)
@ -65,7 +65,7 @@ const UserActionItem: FC<UserActionItemProps> = ({
let value = e.target.value
if (value.length > BUTTON_TEXT_MAX_LENGTH) {
value = value.slice(0, BUTTON_TEXT_MAX_LENGTH)
Toast.notify({ type: 'error', message: t(`${i18nPrefix}.userActions.buttonTextTooLong`, { ns: 'workflow', maxLength: BUTTON_TEXT_MAX_LENGTH }) })
toast.error(t(`${i18nPrefix}.userActions.buttonTextTooLong`, { ns: 'workflow', maxLength: BUTTON_TEXT_MAX_LENGTH }))
}
onChange({ ...data, title: value })
}

View File

@ -16,8 +16,8 @@ import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import { toast } from '@/app/components/base/ui/toast'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
@ -132,7 +132,7 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
className="flex size-6 cursor-pointer items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover"
onClick={() => {
copy(inputs.form_content)
Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) })
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
}}
>
<RiClipboardLine className="h-4 w-4 text-text-secondary" />

View File

@ -3,7 +3,7 @@ import type { IterationNodeType } from '../types'
import type { PanelProps } from '@/types/workflow'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { ErrorHandleMode } from '@/app/components/workflow/types'
import { BlockEnum, VarType } from '../../../types'
import AddBlock from '../add-block'
@ -15,6 +15,15 @@ const mockHandleNodeAdd = vi.fn()
const mockHandleNodeIterationRerender = vi.fn()
let mockNodesReadOnly = false
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
},
}))
vi.mock('reactflow', async () => {
const actual = await vi.importActual<typeof import('reactflow')>('reactflow')
return {
@ -102,7 +111,7 @@ vi.mock('../use-config', () => ({
}))
const mockUseConfig = vi.mocked(useConfig)
const mockToastNotify = vi.spyOn(Toast, 'notify').mockImplementation(() => ({}))
const mockToastWarning = vi.mocked(toast.warning)
const createData = (overrides: Partial<IterationNodeType> = {}): IterationNodeType => ({
title: 'Iteration',
@ -191,11 +200,7 @@ describe('iteration path', () => {
expect(screen.getByRole('button', { name: 'select-block' })).toBeInTheDocument()
expect(screen.getByTestId('iteration-background-iteration-node')).toBeInTheDocument()
expect(mockHandleNodeIterationRerender).toHaveBeenCalledWith('iteration-node')
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'warning',
message: 'workflow.nodes.iteration.answerNodeWarningDesc',
duration: 5000,
})
expect(mockToastWarning).toHaveBeenCalledWith('workflow.nodes.iteration.answerNodeWarningDesc')
})
it('should wire panel input, output, parallel, numeric, error mode, and flatten actions', async () => {

View File

@ -12,7 +12,7 @@ import {
useNodesInitialized,
useViewport,
} from 'reactflow'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { cn } from '@/utils/classnames'
import { IterationStartNodeDumb } from '../iteration-start'
import AddBlock from './add-block'
@ -34,11 +34,7 @@ const Node: FC<NodeProps<IterationNodeType>> = ({
if (nodesInitialized)
handleNodeIterationRerender(id)
if (data.is_parallel && showTips) {
Toast.notify({
type: 'warning',
message: t(`${i18nPrefix}.answerNodeWarningDesc`, { ns: 'workflow' }),
duration: 5000,
})
toast.warning(t(`${i18nPrefix}.answerNodeWarningDesc`, { ns: 'workflow' }))
setShowTips(false)
}
}, [nodesInitialized, id, handleNodeIterationRerender, data.is_parallel, showTips, t])

View File

@ -4,7 +4,7 @@ import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
import { cn } from '@/utils/classnames'
import { SegmentedControl } from '../../../../../base/segmented-control'
@ -196,10 +196,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
}
else if (currentTab === SchemaView.VisualEditor) {
if (advancedEditing || isAddingNewField) {
Toast.notify({
type: 'warning',
message: t('nodes.llm.jsonSchema.warningTips.saveSchema', { ns: 'workflow' }),
})
toast.warning(t('nodes.llm.jsonSchema.warningTips.saveSchema', { ns: 'workflow' }))
return
}
}

View File

@ -9,7 +9,7 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { STORAGE_KEYS } from '@/config/storage-keys'
@ -110,10 +110,7 @@ const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
const generateSchema = useCallback(async () => {
const { output, error } = await generateStructuredOutputRules({ instruction, model_config: model! })
if (error) {
Toast.notify({
type: 'error',
message: error,
})
toast.error(error)
setSchema(null)
setView(GeneratorView.promptEditor)
return

View File

@ -3,7 +3,8 @@ import type { Field } from '../../../types'
import type { EditData } from './edit-card'
import { noop } from 'es-toolkit/function'
import { produce } from 'immer'
import Toast from '@/app/components/base/toast'
import { useTranslation } from 'react-i18next'
import { toast } from '@/app/components/base/ui/toast'
import { ArrayType, FILE_REF_FORMAT, Type } from '../../../types'
import { findPropertyWithPath } from '../../../utils'
import { useMittContext } from './context'
@ -22,6 +23,7 @@ type AddEventParams = {
export const useSchemaNodeOperations = (props: VisualEditorProps) => {
const { schema: jsonSchema, onChange: doOnChange } = props
const { t } = useTranslation()
const onChange = doOnChange || noop
const backupSchema = useVisualEditorStore(state => state.backupSchema)
const setBackupSchema = useVisualEditorStore(state => state.setBackupSchema)
@ -65,10 +67,7 @@ export const useSchemaNodeOperations = (props: VisualEditorProps) => {
if (schema.type === Type.object) {
const properties = schema.properties || {}
if (properties[newName]) {
Toast.notify({
type: 'error',
message: 'Property name already exists',
})
toast.error(t('nodes.llm.jsonSchema.fieldNameAlreadyExists', { ns: 'workflow' }))
emit('restorePropertyName')
return
}
@ -92,10 +91,7 @@ export const useSchemaNodeOperations = (props: VisualEditorProps) => {
if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
const properties = schema.items.properties || {}
if (properties[newName]) {
Toast.notify({
type: 'error',
message: 'Property name already exists',
})
toast.error(t('nodes.llm.jsonSchema.fieldNameAlreadyExists', { ns: 'workflow' }))
emit('restorePropertyName')
return
}
@ -279,10 +275,7 @@ export const useSchemaNodeOperations = (props: VisualEditorProps) => {
if (oldName !== newName) {
const properties = parentSchema.properties
if (properties[newName]) {
Toast.notify({
type: 'error',
message: 'Property name already exists',
})
toast.error(t('nodes.llm.jsonSchema.fieldNameAlreadyExists', { ns: 'workflow' }))
samePropertyNameError = true
}
@ -382,10 +375,7 @@ export const useSchemaNodeOperations = (props: VisualEditorProps) => {
if (oldName !== newName) {
const properties = parentSchema.items.properties || {}
if (properties[newName]) {
Toast.notify({
type: 'error',
message: 'Property name already exists',
})
toast.error(t('nodes.llm.jsonSchema.fieldNameAlreadyExists', { ns: 'workflow' }))
samePropertyNameError = true
}

View File

@ -8,8 +8,8 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import AddButton2 from '@/app/components/base/button/add-button'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import { toast } from '@/app/components/base/ui/toast'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
import Field from '@/app/components/workflow/nodes/_base/components/field'
@ -151,11 +151,11 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
)
const keys = Object.keys(removedDetails)
if (keys.length)
Toast.notify({ type: 'warning', message: `${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${keys.map(k => `${k} (${removedDetails[k]})`).join(', ')}` })
toast.warning(`${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${keys.map(k => `${k} (${removedDetails[k]})`).join(', ')}`)
handleCompletionParamsChange(filtered)
}
catch {
Toast.notify({ type: 'error', message: t('error', { ns: 'common' }) })
toast.error(t('error', { ns: 'common' }))
handleCompletionParamsChange({})
}
finally {

View File

@ -181,10 +181,12 @@ vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', (
),
}))
vi.mock('@/app/components/base/toast', () => ({
__esModule: true,
default: {
notify: (payload: unknown) => mockToastNotify(payload),
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
},
}))

View File

@ -7,7 +7,7 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { ValueType, VarType } from '@/app/components/workflow/types'
import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
import FormItem from './form-item'
@ -28,10 +28,7 @@ const Item = ({
const checkVariableName = (value: string) => {
const { isValid, errorMessageKey } = checkKeys([value], false)
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('env.modal.name', { ns: 'workflow' }) }),
})
toast.error(t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('env.modal.name', { ns: 'workflow' }) }))
return false
}
return true

View File

@ -6,7 +6,7 @@ import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/
import type { PanelProps } from '@/types/workflow'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import {
useTextGenerationCurrentProviderAndModelAndModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
@ -36,6 +36,15 @@ let mockWorkflowTools: MockToolCollection[] = []
let mockSelectedToolInfo: ToolDefaultValue | undefined
let mockBlockSelectorOpen = false
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
},
}))
vi.mock('@/app/components/workflow/block-selector', () => ({
__esModule: true,
default: ({
@ -254,7 +263,7 @@ vi.mock('../use-config', () => ({
const mockUseTextGeneration = vi.mocked(useTextGenerationCurrentProviderAndModelAndModelList)
const mockUseConfig = vi.mocked(useConfig)
const mockToastNotify = vi.spyOn(Toast, 'notify').mockImplementation(() => ({}))
const mockToastError = vi.mocked(toast.error)
const createToolParameter = (overrides: Partial<ToolParameter> = {}): ToolParameter => ({
name: 'city',
@ -356,7 +365,7 @@ const panelProps: PanelProps = {
describe('parameter-extractor path', () => {
beforeEach(() => {
vi.clearAllMocks()
mockToastNotify.mockClear()
mockToastError.mockClear()
mockBuiltInTools = []
mockCustomTools = []
mockWorkflowTools = []
@ -582,7 +591,7 @@ describe('parameter-extractor path', () => {
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(onSave).not.toHaveBeenCalled()
expect(mockToastNotify).toHaveBeenCalled()
expect(mockToastError).toHaveBeenCalled()
})
it('should render the add trigger for new parameters', () => {
@ -614,7 +623,7 @@ describe('parameter-extractor path', () => {
const descriptionInput = screen.getByPlaceholderText('workflow.nodes.parameterExtractor.addExtractParameterContent.descriptionPlaceholder')
fireEvent.change(nameInput, { target: { value: '1bad' } })
expect(mockToastNotify).toHaveBeenCalled()
expect(mockToastError).toHaveBeenCalled()
expect(nameInput).toHaveValue('')
fireEvent.change(nameInput, { target: { value: 'temporary_name' } })
@ -649,7 +658,7 @@ describe('parameter-extractor path', () => {
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(onSave).not.toHaveBeenCalled()
expect(mockToastNotify).toHaveBeenCalled()
expect(mockToastError).toHaveBeenCalled()
})
it('should keep rename metadata and updated options when editing a select parameter', async () => {

View File

@ -15,7 +15,7 @@ import Modal from '@/app/components/base/modal'
import Select from '@/app/components/base/select'
import Switch from '@/app/components/base/switch'
import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { ChangeType } from '@/app/components/workflow/types'
import { checkKeys } from '@/utils/var'
import { ParamType } from '../../types'
@ -54,10 +54,7 @@ const AddExtractParameter: FC<Props> = ({
if (key === 'name') {
const { isValid, errorKey, errorMessageKey } = checkKeys([value], true)
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey }),
})
toast.error(t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey }))
return
}
}
@ -106,10 +103,7 @@ const AddExtractParameter: FC<Props> = ({
errMessage = t(`${errorI18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.addExtractParameterContent.description`, { ns: 'workflow' }) })
if (errMessage) {
Toast.notify({
type: 'error',
message: errMessage,
})
toast.error(errMessage)
return false
}
return true

View File

@ -7,7 +7,7 @@ import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { ReactSortable } from 'react-sortablejs'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { ChangeType } from '@/app/components/workflow/types'
import { cn } from '@/utils/classnames'
import { hasDuplicateStr } from '@/utils/var'
@ -43,10 +43,7 @@ const VarList: FC<Props> = ({
}
if (errorMsgKey && typeName) {
Toast.notify({
type: 'error',
message: t(errorMsgKey, { ns: 'appDebug', key: t(typeName, { ns: 'appDebug' }) }),
})
toast.error(t(errorMsgKey, { ns: 'appDebug', key: t(typeName, { ns: 'appDebug' }) }))
return false
}
onChange(newList, moreInfo ? { index, payload: moreInfo } : undefined)

View File

@ -4,7 +4,7 @@ import { useBoolean } from 'ahooks'
import { produce } from 'immer'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import {
useIsChatMode,
useNodesReadOnly,
@ -97,10 +97,7 @@ const useConfig = (id: string, payload: StartNodeType) => {
}
if (errorMsgKey && typeName) {
Toast.notify({
type: 'error',
message: t(errorMsgKey, { ns: 'appDebug', key: t(typeName, { ns: 'appDebug' }) }),
})
toast.error(t(errorMsgKey, { ns: 'appDebug', key: t(typeName, { ns: 'appDebug' }) }))
return false
}
setInputs(newInputs)

View File

@ -5,7 +5,7 @@ import { capitalize } from 'es-toolkit/string'
import { produce } from 'immer'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { CollectionType } from '@/app/components/tools/types'
import {
@ -66,10 +66,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
async (value: any) => {
await updateBuiltInToolCredential(currCollection?.name as string, value)
Toast.notify({
type: 'success',
message: t('api.actionSuccess', { ns: 'common' }),
})
toast.success(t('api.actionSuccess', { ns: 'common' }))
invalidToolsByType()
hideSetAuthModal()
},

View File

@ -1,7 +1,7 @@
import type { WebhookTriggerNodeType } from '../types'
import { renderHook } from '@testing-library/react'
import { useStore as useAppStore } from '@/app/components/app/store'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import { fetchWebhookUrl } from '@/service/apps'
import { createNodeCrudModuleMock } from '../../__tests__/use-config-test-utils'
@ -18,10 +18,10 @@ vi.mock('react-i18next', () => ({
}),
}))
vi.mock('@/app/components/base/toast', () => ({
vi.mock('@/app/components/base/ui/toast', () => ({
__esModule: true,
default: {
notify: vi.fn(),
toast: {
error: vi.fn(),
},
}))
@ -42,7 +42,7 @@ vi.mock('@/service/apps', () => ({
}))
const mockedFetchWebhookUrl = vi.mocked(fetchWebhookUrl)
const mockedToastNotify = vi.mocked(Toast.notify)
const mockedToastError = vi.mocked(toast.error)
const createPayload = (overrides: Partial<WebhookTriggerNodeType> = {}): WebhookTriggerNodeType => ({
title: 'Webhook',
@ -148,7 +148,7 @@ describe('useConfig', () => {
}),
]),
}))
expect(mockedToastNotify).toHaveBeenCalledTimes(1)
expect(mockedToastError).toHaveBeenCalledTimes(1)
})
it('should generate webhook urls once and fall back to empty url on request failure', async () => {

View File

@ -8,7 +8,6 @@ import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import InputWithCopy from '@/app/components/base/input-with-copy'
import { SimpleSelect } from '@/app/components/base/select'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import {
NumberField,
@ -18,6 +17,7 @@ import {
NumberFieldIncrement,
NumberFieldInput,
} from '@/app/components/base/ui/number-field'
import { toast } from '@/app/components/base/ui/toast'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import OutputVars from '@/app/components/workflow/nodes/_base/components/output-vars'
import Split from '@/app/components/workflow/nodes/_base/components/split'
@ -103,10 +103,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
placeholder={t(`${i18nPrefix}.webhookUrlPlaceholder`, { ns: 'workflow' })}
readOnly
onCopy={() => {
Toast.notify({
type: 'success',
message: t(`${i18nPrefix}.urlCopied`, { ns: 'workflow' }),
})
toast.success(t(`${i18nPrefix}.urlCopied`, { ns: 'workflow' }))
}}
/>
</div>

View File

@ -2,7 +2,7 @@ import type { HttpMethod, WebhookHeader, WebhookParameter, WebhookTriggerNodeTyp
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { useNodesReadOnly, useWorkflow } from '@/app/components/workflow/hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { fetchWebhookUrl } from '@/service/apps'
@ -33,10 +33,7 @@ export const useConfig = (id: string, payload: WebhookTriggerNodeType) => {
? t(key as never, { ns: 'appDebug', key: fieldLabel })
: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: fieldLabel })
Toast.notify({
type: 'error',
message,
})
toast.error(message)
}, [t])
const handleMethodChange = useCallback((method: HttpMethod) => {

View File

@ -3,7 +3,7 @@ import type { VariableAssignerNodeType } from '../types'
import type { PanelProps } from '@/types/workflow'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import AddVariable from '../components/add-variable'
@ -19,6 +19,15 @@ const mockHandleGroupItemMouseEnter = vi.fn()
const mockHandleGroupItemMouseLeave = vi.fn()
const mockGetAvailableVars = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
},
}))
vi.mock('@/app/components/workflow/nodes/_base/components/add-variable-popup', () => ({
default: ({ onSelect }: any) => (
<button
@ -102,6 +111,7 @@ vi.mock('../use-config', () => ({
}))
const mockUseConfig = vi.mocked(useConfig)
const mockToastError = vi.mocked(toast.error)
const createData = (overrides: Partial<VariableAssignerNodeType> = {}): VariableAssignerNodeType => ({
title: 'Variable Assigner',
@ -168,7 +178,6 @@ describe('variable-assigner path', () => {
},
])
mockUseConfig.mockReturnValue(createConfigResult())
vi.spyOn(Toast, 'notify').mockImplementation(() => ({}))
})
describe('Path Integration', () => {
@ -296,7 +305,7 @@ describe('variable-assigner path', () => {
await user.click(screen.getByText('Group1'))
fireEvent.change(screen.getByDisplayValue('Group1'), { target: { value: '1bad' } })
expect(Toast.notify).toHaveBeenCalled()
expect(mockToastError).toHaveBeenCalled()
fireEvent.change(screen.getByDisplayValue('Group1'), { target: { value: 'Renamed Group' } })
expect(onGroupNameChange).toHaveBeenCalledWith('Renamed_Group')

View File

@ -11,7 +11,7 @@ import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Folder } from '@/app/components/base/icons/src/vender/line/files'
import Toast from '@/app/components/base/toast'
import { toast } from '@/app/components/base/ui/toast'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { VarType } from '@/app/components/workflow/types'
@ -94,10 +94,7 @@ const VarGroupItem: FC<Props> = ({
const value = e.target.value
const { isValid, errorKey, errorMessageKey } = checkKeys([value], false)
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey }),
})
toast.error(t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey }))
return
}
onGroupNameChange?.(value)