diff --git a/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx index 9cd66e37ea..5deed8174d 100644 --- a/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/configure-button.spec.tsx @@ -49,9 +49,12 @@ vi.mock('@/service/use-tools', () => ({ // Mock Toast - need to verify notification calls const mockToastNotify = vi.fn() -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: (options: { type: string, message: string }) => mockToastNotify(options), +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 }), }, })) diff --git a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts index ad0dd2eff2..ac61872a18 100644 --- a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts +++ b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts @@ -33,9 +33,12 @@ vi.mock('@/service/use-tools', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: (options: { type: string, message: string }) => mockToastNotify(options), +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 }), }, })) diff --git a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts index 701ae8fd01..142b0c2397 100644 --- a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts +++ b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts @@ -3,7 +3,7 @@ import type { InputVar, Variable } from '@/app/components/workflow/types' import type { PublishWorkflowParams } from '@/types/workflow' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useAppContext } from '@/context/app-context' import { useRouter } from '@/next/navigation' import { createWorkflowToolProvider, saveWorkflowToolProvider } from '@/service/tools' @@ -188,14 +188,11 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { invalidateAllWorkflowTools() onRefreshData?.() invalidateDetail(workflowAppId) - Toast.notify({ - type: 'success', - message: t('api.actionSuccess', { ns: 'common' }), - }) + toast.success(t('api.actionSuccess', { ns: 'common' })) setShowModal(false) } catch (e) { - Toast.notify({ type: 'error', message: (e as Error).message }) + toast.error((e as Error).message) } } @@ -209,14 +206,11 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { onRefreshData?.() invalidateAllWorkflowTools() invalidateDetail(workflowAppId) - Toast.notify({ - type: 'success', - message: t('api.actionSuccess', { ns: 'common' }), - }) + toast.success(t('api.actionSuccess', { ns: 'common' })) setShowModal(false) } catch (e) { - Toast.notify({ type: 'error', message: (e as Error).message }) + toast.error((e as Error).message) } } diff --git a/web/app/components/tools/workflow-tool/index.tsx b/web/app/components/tools/workflow-tool/index.tsx index 78375857ea..06aeb1ba79 100644 --- a/web/app/components/tools/workflow-tool/index.tsx +++ b/web/app/components/tools/workflow-tool/index.tsx @@ -12,8 +12,8 @@ import Drawer from '@/app/components/base/drawer-plus' import EmojiPicker from '@/app/components/base/emoji-picker' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' -import Toast from '@/app/components/base/toast' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import LabelSelector from '@/app/components/tools/labels/selector' import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal' import MethodSelector from '@/app/components/tools/workflow-tool/method-selector' @@ -129,10 +129,7 @@ const WorkflowToolAsModal: FC = ({ errorMessage = t('createTool.nameForToolCall', { ns: 'tools' }) + t('createTool.nameForToolCallTip', { ns: 'tools' }) if (errorMessage) { - Toast.notify({ - type: 'error', - message: errorMessage, - }) + toast.error(errorMessage) return } diff --git a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx index 47ad2fad02..737481601c 100644 --- a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx @@ -114,9 +114,12 @@ vi.mock('@/service/use-tools', () => ({ useInvalidateAllMCPTools: vi.fn(), })) -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: (payload: unknown) => mockNotify(payload), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: (message: string) => mockNotify({ type: 'success', message }), + error: (message: string) => mockNotify({ type: 'error', message }), + warning: (message: string) => mockNotify({ type: 'warning', message }), + info: (message: string) => mockNotify({ type: 'info', message }), }, })) diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index d9ce065dde..cf48488415 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -16,7 +16,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 SearchBox from '@/app/components/plugins/marketplace/search-box' import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' import AllTools from '@/app/components/workflow/block-selector/all-tools' @@ -137,10 +137,7 @@ const ToolPicker: FC = ({ const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => { await createCustomCollection(data) - Toast.notify({ - type: 'success', - message: t('api.actionSuccess', { ns: 'common' }), - }) + toast.success(t('api.actionSuccess', { ns: 'common' })) hideEditCustomCollectionModal() handleAddedCustomTool() } diff --git a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx index dc00d61301..d092e769d6 100644 --- a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx +++ b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx @@ -60,9 +60,12 @@ vi.mock('@/service/use-workflow', () => ({ }), })) -vi.mock('../../../base/toast', () => ({ - default: { - notify: (payload: unknown) => mockNotify(payload), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: (message: string) => mockNotify({ type: 'success', message }), + error: (message: string) => mockNotify({ type: 'error', message }), + warning: (message: string) => mockNotify({ type: 'warning', message }), + info: (message: string) => mockNotify({ type: 'info', message }), }, })) diff --git a/web/app/components/workflow/header/__tests__/run-mode.spec.tsx b/web/app/components/workflow/header/__tests__/run-mode.spec.tsx index cb5214544a..74dc529a62 100644 --- a/web/app/components/workflow/header/__tests__/run-mode.spec.tsx +++ b/web/app/components/workflow/header/__tests__/run-mode.spec.tsx @@ -46,10 +46,13 @@ vi.mock('../../hooks/use-dynamic-test-run-options', () => ({ useDynamicTestRunOptions: () => mockDynamicOptions, })) -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ - notify: mockNotify, - }), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: (message: string) => mockNotify({ type: 'success', message }), + error: (message: string) => mockNotify({ type: 'error', message }), + warning: (message: string) => mockNotify({ type: 'warning', message }), + info: (message: string) => mockNotify({ type: 'info', message }), + }, })) vi.mock('@/app/components/base/amplitude', () => ({ diff --git a/web/app/components/workflow/header/header-in-restoring.tsx b/web/app/components/workflow/header/header-in-restoring.tsx index 2c5b4b9f08..d32e2c7fb9 100644 --- a/web/app/components/workflow/header/header-in-restoring.tsx +++ b/web/app/components/workflow/header/header-in-restoring.tsx @@ -4,11 +4,11 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' +import { toast } from '@/app/components/base/ui/toast' import useTheme from '@/hooks/use-theme' import { useInvalidAllLastRun, useRestoreWorkflow } from '@/service/use-workflow' import { getFlowPrefix } from '@/service/utils' import { cn } from '@/utils/classnames' -import Toast from '../../base/toast' import { useWorkflowRefreshDraft, useWorkflowRun, @@ -65,18 +65,12 @@ const HeaderInRestoring = ({ workflowStore.setState({ isRestoring: false }) workflowStore.setState({ backupDraft: undefined }) handleRefreshWorkflowDraft() - Toast.notify({ - type: 'success', - message: t('versionHistory.action.restoreSuccess', { ns: 'workflow' }), - }) + toast.success(t('versionHistory.action.restoreSuccess', { ns: 'workflow' })) deleteAllInspectVars() invalidAllLastRun() } catch { - Toast.notify({ - type: 'error', - message: t('versionHistory.action.restoreFailure', { ns: 'workflow' }), - }) + toast.error(t('versionHistory.action.restoreFailure', { ns: 'workflow' })) } finally { onRestoreSettled?.() diff --git a/web/app/components/workflow/header/run-mode.tsx b/web/app/components/workflow/header/run-mode.tsx index 86f998e0b7..8f802fcec5 100644 --- a/web/app/components/workflow/header/run-mode.tsx +++ b/web/app/components/workflow/header/run-mode.tsx @@ -5,7 +5,7 @@ import { useCallback, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' -import { useToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { useWorkflowRun, useWorkflowRunValidation, useWorkflowStartRun } from '@/app/components/workflow/hooks' import ShortcutsName from '@/app/components/workflow/shortcuts-name' import { useStore } from '@/app/components/workflow/store' @@ -41,7 +41,6 @@ const RunMode = ({ const dynamicOptions = useDynamicTestRunOptions() const testRunMenuRef = useRef(null) - const { notify } = useToastContext() useEffect(() => { // @ts-expect-error - Dynamic property for backward compatibility with keyboard shortcuts @@ -66,7 +65,7 @@ const RunMode = ({ isValid = false }) if (!isValid) { - notify({ type: 'error', message: t('panel.checklistTip', { ns: 'workflow' }) }) + toast.error(t('panel.checklistTip', { ns: 'workflow' })) return } @@ -98,7 +97,7 @@ const RunMode = ({ // Placeholder for trigger-specific execution logic for schedule, webhook, plugin types console.log('TODO: Handle trigger execution for type:', option.type, 'nodeId:', option.nodeId) } - }, [warningNodes, notify, t, handleWorkflowStartRunInWorkflow, handleWorkflowTriggerScheduleRunInWorkflow, handleWorkflowTriggerWebhookRunInWorkflow, handleWorkflowTriggerPluginRunInWorkflow, handleWorkflowRunAllTriggersInWorkflow]) + }, [warningNodes, t, handleWorkflowStartRunInWorkflow, handleWorkflowTriggerScheduleRunInWorkflow, handleWorkflowTriggerWebhookRunInWorkflow, handleWorkflowTriggerPluginRunInWorkflow, handleWorkflowRunAllTriggersInWorkflow]) const { eventEmitter } = useEventEmitterContextContext() eventEmitter?.useSubscription((v: any) => { diff --git a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts index a11fe2c981..891007ff0e 100644 --- a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts @@ -89,8 +89,13 @@ vi.mock('../index', () => ({ useNodesMetaData: () => ({ nodes: [], nodesMap: mockNodesMap }), })) -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ notify: vi.fn() }), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + }, })) vi.mock('@/context/i18n', () => ({ diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 029892c4d1..99536653ce 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -27,7 +27,7 @@ import { import { useTranslation } from 'react-i18next' import { useEdges, useStoreApi } from 'reactflow' import { useStore as useAppStore } from '@/app/components/app/store' -import { useToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' @@ -325,7 +325,6 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { export const useChecklistBeforePublish = () => { const { t } = useTranslation() const language = useGetLanguage() - const { notify } = useToastContext() const queryClient = useQueryClient() const store = useStoreApi() const { nodesMap: nodesExtraData } = useNodesMetaData() @@ -390,7 +389,7 @@ export const useChecklistBeforePublish = () => { const { validNodes, maxDepth } = getValidTreeNodes(filteredNodes, edges) if (maxDepth > MAX_TREE_DEPTH) { - notify({ type: 'error', message: t('common.maxTreeDepth', { ns: 'workflow', depth: MAX_TREE_DEPTH }) }) + toast.error(t('common.maxTreeDepth', { ns: 'workflow', depth: MAX_TREE_DEPTH })) return false } @@ -488,7 +487,7 @@ export const useChecklistBeforePublish = () => { isModelProviderInstalled: isLLMModelProviderInstalled(modelProvider, installedPluginIds), }) if (modelIssue === LLMModelIssueCode.providerPluginUnavailable) { - notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.configureModel', { ns: 'workflow' })}` }) + toast.error(`[${node.data.title}] ${t('errorMsg.configureModel', { ns: 'workflow' })}`) return false } } @@ -497,7 +496,7 @@ export const useChecklistBeforePublish = () => { const { errorMessage } = nodesExtraData![node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid) if (errorMessage) { - notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` }) + toast.error(`[${node.data.title}] ${errorMessage}`) return false } @@ -510,12 +509,12 @@ export const useChecklistBeforePublish = () => { if (usedNode) { const usedVar = usedNode.vars.find(v => v.variable === variable?.[1]) if (!usedVar) { - notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}` }) + toast.error(`[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}`) return false } } else { - notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}` }) + toast.error(`[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}`) return false } } @@ -526,7 +525,7 @@ export const useChecklistBeforePublish = () => { const isUnconnected = !validNodes.some(n => n.id === node.id) if (isUnconnected && !canSkipConnectionCheck) { - notify({ type: 'error', message: `[${node.data.title}] ${t('common.needConnectTip', { ns: 'workflow' })}` }) + toast.error(`[${node.data.title}] ${t('common.needConnectTip', { ns: 'workflow' })}`) return false } } @@ -534,7 +533,7 @@ export const useChecklistBeforePublish = () => { if (shouldCheckStartNode) { const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum)) if (startNodesFiltered.length === 0) { - notify({ type: 'error', message: t('common.needStartNode', { ns: 'workflow' }) }) + toast.error(t('common.needStartNode', { ns: 'workflow' })) return false } } @@ -545,13 +544,13 @@ export const useChecklistBeforePublish = () => { const type = isRequiredNodesType[i] if (!filteredNodes.some(node => node.data.type === type)) { - notify({ type: 'error', message: t('common.needAdd', { ns: 'workflow', node: t(`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>, { ns: 'workflow' }) }) }) + toast.error(t('common.needAdd', { ns: 'workflow', node: t(`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>, { ns: 'workflow' }) })) return false } } return true - }, [store, workflowStore, getNodesAvailableVarList, shouldCheckStartNode, nodesExtraData, notify, t, updateDatasetsDetail, buildInTools, customTools, workflowTools, language, getCheckData, queryClient, strategyProviders, modelProviders]) + }, [store, workflowStore, getNodesAvailableVarList, shouldCheckStartNode, nodesExtraData, t, updateDatasetsDetail, buildInTools, customTools, workflowTools, language, getCheckData, queryClient, strategyProviders, modelProviders]) return { handleCheckBeforePublish, @@ -563,15 +562,14 @@ export const useWorkflowRunValidation = () => { const nodes = useNodes() const edges = useEdges() const needWarningNodes = useChecklist(nodes, edges) - const { notify } = useToastContext() const validateBeforeRun = useCallback(() => { if (needWarningNodes.length > 0) { - notify({ type: 'error', message: t('panel.checklistTip', { ns: 'workflow' }) }) + toast.error(t('panel.checklistTip', { ns: 'workflow' })) return false } return true - }, [needWarningNodes, notify, t]) + }, [needWarningNodes, t]) return { validateBeforeRun, diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx index ffe1e80bb0..b58b045f92 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx @@ -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 => ({ diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx index 4b7f65bcc1..fadac55cb8 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx @@ -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 = ({ }) }) if (errMsg) { - Toast.notify({ - message: errMsg, - type: 'error', - }) + toast.error(errMsg) return } @@ -147,10 +144,7 @@ const BeforeRunForm: FC = ({ }) }) if (parseErrorJsonField) { - Toast.notify({ - message: t('errorMsg.invalidJson', { ns: 'workflow', field: parseErrorJsonField }), - type: 'error', - }) + toast.error(t('errorMsg.invalidJson', { ns: 'workflow', field: parseErrorJsonField })) return } diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts index db7833af2b..d58c787bd8 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts @@ -3,7 +3,7 @@ import type { Params as OneStepRunParams } from '@/app/components/workflow/nodes // import import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types' import { useCallback, useEffect, useState } from 'react' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useNodesSyncDraft, } from '@/app/components/workflow/hooks' @@ -163,7 +163,7 @@ const useLastRun = ({ return false const message = warningForNode.errorMessages[0] || 'This node has unresolved checklist issues' - Toast.notify({ type: 'error', message }) + toast.error(message) return true }, [warningNodes, id]) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index 06843eacef..c634fd92f4 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -12,7 +12,7 @@ import { } from 'reactflow' 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, @@ -410,14 +410,14 @@ const useOneStepRun = ({ }) 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) } @@ -442,10 +442,10 @@ const useOneStepRun = ({ _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 => { const urlPath = `/apps/${flowId}/workflows/draft/nodes/${id}/trigger/run` @@ -467,8 +467,8 @@ const useOneStepRun = ({ 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) } @@ -495,8 +495,8 @@ const useOneStepRun = ({ } 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) } @@ -519,7 +519,7 @@ const useOneStepRun = ({ 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 @@ -531,7 +531,7 @@ const useOneStepRun = ({ } return null - }, [flowId, id, data, handleNodeDataUpdate, cancelWebhookSingleRun]) + }, [flowId, id, data, handleNodeDataUpdate, cancelWebhookSingleRun, t]) const runPluginSingleRun = useCallback(async (): Promise => { const urlPath = `/apps/${flowId}/workflows/draft/nodes/${id}/trigger/run` @@ -566,14 +566,14 @@ const useOneStepRun = ({ 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) } @@ -600,7 +600,7 @@ const useOneStepRun = ({ if (response?.status === 'error') { const message = response.message || 'Plugin debug failed' - Toast.notify({ type: 'error', message }) + toast.error(message) cancelPluginSingleRun() throw new Error(message) } @@ -633,10 +633,8 @@ const useOneStepRun = ({ _isSingleRun: false, }, }) - Toast.notify({ - type: 'error', - message: res.errorMessage || '', - }) + if (res.errorMessage) + toast.error(res.errorMessage) } return res } diff --git a/web/app/components/workflow/nodes/http/components/__tests__/curl-panel.spec.tsx b/web/app/components/workflow/nodes/http/components/__tests__/curl-panel.spec.tsx index 1d11b9b882..f42e98f605 100644 --- a/web/app/components/workflow/nodes/http/components/__tests__/curl-panel.spec.tsx +++ b/web/app/components/workflow/nodes/http/components/__tests__/curl-panel.spec.tsx @@ -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() }) }) }) diff --git a/web/app/components/workflow/nodes/http/components/curl-panel.tsx b/web/app/components/workflow/nodes/http/components/curl-panel.tsx index 7b6a26cc29..b08d3f0a7f 100644 --- a/web/app/components/workflow/nodes/http/components/curl-panel.tsx +++ b/web/app/components/workflow/nodes/http/components/curl-panel.tsx @@ -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 = ({ 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) diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx index fa5cbfd3a2..0aa8b1f640 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx @@ -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 diff --git a/web/app/components/workflow/nodes/human-input/components/user-action.tsx b/web/app/components/workflow/nodes/human-input/components/user-action.tsx index d124a80051..ca0398eb7f 100644 --- a/web/app/components/workflow/nodes/human-input/components/user-action.tsx +++ b/web/app/components/workflow/nodes/human-input/components/user-action.tsx @@ -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 = ({ .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 = ({ 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 }) } diff --git a/web/app/components/workflow/nodes/human-input/panel.tsx b/web/app/components/workflow/nodes/human-input/panel.tsx index 525821d042..c209c6451e 100644 --- a/web/app/components/workflow/nodes/human-input/panel.tsx +++ b/web/app/components/workflow/nodes/human-input/panel.tsx @@ -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> = ({ 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' })) }} > diff --git a/web/app/components/workflow/nodes/iteration/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/iteration/__tests__/integration.spec.tsx index 67de8b188b..dc7538144e 100644 --- a/web/app/components/workflow/nodes/iteration/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/iteration/__tests__/integration.spec.tsx @@ -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('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 => ({ 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 () => { diff --git a/web/app/components/workflow/nodes/iteration/node.tsx b/web/app/components/workflow/nodes/iteration/node.tsx index 476266211a..667c68144f 100644 --- a/web/app/components/workflow/nodes/iteration/node.tsx +++ b/web/app/components/workflow/nodes/iteration/node.tsx @@ -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> = ({ 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]) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx index a19dccad78..164e7c3c29 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx @@ -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 = ({ } 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 } } diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx index 6a34925275..9e2fba5fa2 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx @@ -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 useTheme from '@/hooks/use-theme' @@ -112,10 +112,7 @@ const JsonSchemaGenerator: FC = ({ 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 diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts index 6159028c21..4820b5a9dc 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts @@ -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, 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 } @@ -267,10 +263,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 } @@ -358,10 +351,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 } diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index 7a7640948d..c3e6d0fee2 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -7,8 +7,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 Field from '@/app/components/workflow/nodes/_base/components/field' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' @@ -98,11 +98,11 @@ const Panel: FC> = ({ ) 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 { diff --git a/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx index 10b8dad885..99ce377b99 100644 --- a/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx @@ -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 }), }, })) diff --git a/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx b/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx index 9ceb92f432..7bd7e09c1b 100644 --- a/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx +++ b/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx @@ -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 diff --git a/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx index 3eeb59e620..60b9d65260 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx @@ -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 => ({ 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 () => { diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx index 5a4113848a..e1b9c1574f 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx @@ -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 = ({ 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 = ({ 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 diff --git a/web/app/components/workflow/nodes/start/components/var-list.tsx b/web/app/components/workflow/nodes/start/components/var-list.tsx index 21fe5e2bb5..a6158864ac 100644 --- a/web/app/components/workflow/nodes/start/components/var-list.tsx +++ b/web/app/components/workflow/nodes/start/components/var-list.tsx @@ -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 = ({ } 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) diff --git a/web/app/components/workflow/nodes/start/use-config.ts b/web/app/components/workflow/nodes/start/use-config.ts index 232c788b6d..12ec1575c9 100644 --- a/web/app/components/workflow/nodes/start/use-config.ts +++ b/web/app/components/workflow/nodes/start/use-config.ts @@ -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) diff --git a/web/app/components/workflow/nodes/tool/hooks/use-config.ts b/web/app/components/workflow/nodes/tool/hooks/use-config.ts index 6ebe7bea26..5e3f928dcb 100644 --- a/web/app/components/workflow/nodes/tool/hooks/use-config.ts +++ b/web/app/components/workflow/nodes/tool/hooks/use-config.ts @@ -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() }, diff --git a/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx index 46d0490b65..92a0457598 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx @@ -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 => ({ 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 () => { diff --git a/web/app/components/workflow/nodes/trigger-webhook/panel.tsx b/web/app/components/workflow/nodes/trigger-webhook/panel.tsx index 839ca6875f..f600fa516d 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/panel.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/panel.tsx @@ -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' @@ -102,10 +102,7 @@ const Panel: FC> = ({ 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' })) }} /> diff --git a/web/app/components/workflow/nodes/trigger-webhook/use-config.ts b/web/app/components/workflow/nodes/trigger-webhook/use-config.ts index 15ebff7736..7924a35ba0 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/use-config.ts +++ b/web/app/components/workflow/nodes/trigger-webhook/use-config.ts @@ -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) => { diff --git a/web/app/components/workflow/nodes/variable-assigner/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/variable-assigner/__tests__/integration.spec.tsx index 2769e867a5..e3e9661cb9 100644 --- a/web/app/components/workflow/nodes/variable-assigner/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/__tests__/integration.spec.tsx @@ -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) => (