From eca2d419b222bfa8a9face7042dacebf149ccb86 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Wed, 8 Jul 2026 17:01:30 +0800 Subject: [PATCH] refactor(web): migrate workflow app context consumers (#38552) --- .../__tests__/selection-contextmenu.spec.tsx | 12 +++++++ .../workflow/comment/comment-icon.spec.tsx | 26 ++++++++++++-- .../workflow/comment/comment-icon.tsx | 7 ++-- .../workflow/comment/comment-input.spec.tsx | 23 +++++++++--- .../workflow/comment/comment-input.tsx | 5 +-- .../workflow/comment/thread.spec.tsx | 23 +++++++++--- .../components/workflow/comment/thread.tsx | 13 +++---- .../__tests__/header-in-restoring.spec.tsx | 16 +++++++++ .../header/__tests__/header-layouts.spec.tsx | 16 +++++++++ .../workflow/header/header-in-restoring.tsx | 5 +-- .../workflow/header/online-users.tsx | 6 ++-- .../__tests__/use-workflow-comment.spec.ts | 25 +++++++++---- .../workflow/hooks/use-workflow-comment.ts | 5 +-- .../nodes/_base/__tests__/node.spec.tsx | 19 ++++++++++ .../_base/components/workflow-panel/index.tsx | 5 +-- .../components/workflow/nodes/_base/node.tsx | 5 +-- .../__tests__/email-configure-modal.spec.tsx | 23 ++++++++---- .../__tests__/method-item.spec.tsx | 23 ++++++++---- .../__tests__/test-email-sender.spec.tsx | 36 +++++++++++++------ .../delivery-method/email-configure-modal.tsx | 5 +-- .../delivery-method/method-item.tsx | 5 +-- .../recipient/__tests__/index.spec.tsx | 19 +++++++--- .../delivery-method/recipient/index.tsx | 10 +++--- .../delivery-method/test-email-sender.tsx | 14 ++++---- .../__tests__/integration.spec.tsx | 15 ++++++++ .../components/dataset-list.tsx | 7 ++-- web/app/components/workflow/operator/hooks.ts | 5 +-- .../comments-panel/__tests__/index.spec.tsx | 15 +++++++- .../workflow/panel/comments-panel/index.tsx | 9 ++--- .../__tests__/index.spec.tsx | 18 +++++++++- .../panel/version-history-panel/index.tsx | 5 +-- .../workflow/selection-contextmenu.tsx | 5 +-- 32 files changed, 328 insertions(+), 97 deletions(-) diff --git a/web/app/components/workflow/__tests__/selection-contextmenu.spec.tsx b/web/app/components/workflow/__tests__/selection-contextmenu.spec.tsx index 3ae8851a23f..422a602c2d8 100644 --- a/web/app/components/workflow/__tests__/selection-contextmenu.spec.tsx +++ b/web/app/components/workflow/__tests__/selection-contextmenu.spec.tsx @@ -29,6 +29,18 @@ vi.mock('@/context/app-context', () => ({ }), })) +vi.mock('@/context/app-context-state', async (importOriginal) => { + const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateAtomMock(importOriginal, () => ({ + workspacePermissionKeys: mockWorkspacePermissionKeys.value, + })) +}) + +vi.mock('jotai', async (importOriginal) => { + const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateJotaiMock(importOriginal) +}) + vi.mock('@/app/components/snippets/hooks/use-create-snippet', async () => { const React = await vi.importActual('react') diff --git a/web/app/components/workflow/comment/comment-icon.spec.tsx b/web/app/components/workflow/comment/comment-icon.spec.tsx index df579442fce..017c83b272b 100644 --- a/web/app/components/workflow/comment/comment-icon.spec.tsx +++ b/web/app/components/workflow/comment/comment-icon.spec.tsx @@ -6,6 +6,13 @@ import { CommentIcon } from './comment-icon' type Position = { x: number, y: number } let mockUserId = 'user-1' +const mockAppContextState = vi.hoisted(() => ({ + userProfile: { + id: 'user-1', + name: 'User', + avatar_url: 'avatar', + }, +})) const mockFlowToScreenPosition = vi.fn((position: Position) => position) const mockScreenToFlowPosition = vi.fn((position: Position) => position) @@ -25,13 +32,28 @@ vi.mock('reactflow', () => ({ vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ userProfile: { + ...mockAppContextState.userProfile, id: mockUserId, - name: 'User', - avatar_url: 'avatar', }, }), })) +vi.mock('@/context/app-context-state', async (importOriginal) => { + const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateAtomMock(importOriginal, () => ({ + ...mockAppContextState, + userProfile: { + ...mockAppContextState.userProfile, + id: mockUserId, + }, + })) +}) + +vi.mock('jotai', async (importOriginal) => { + const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateJotaiMock(importOriginal) +}) + vi.mock('@/app/components/base/user-avatar-list', () => ({ UserAvatarList: ({ users }: { users: Array<{ id: string }> }) => (
{users.map(user => user.id).join(',')}
diff --git a/web/app/components/workflow/comment/comment-icon.tsx b/web/app/components/workflow/comment/comment-icon.tsx index 658ad6a2c6b..e46576e145d 100644 --- a/web/app/components/workflow/comment/comment-icon.tsx +++ b/web/app/components/workflow/comment/comment-icon.tsx @@ -2,10 +2,11 @@ import type { FC, PointerEvent as ReactPointerEvent } from 'react' import type { WorkflowCommentList } from '@/app/components/workflow/comment/types' +import { useAtomValue } from 'jotai' import { memo, useCallback, useMemo, useRef, useState } from 'react' import { useReactFlow, useViewport } from 'reactflow' import { UserAvatarList } from '@/app/components/base/user-avatar-list' -import { useAppContext } from '@/context/app-context' +import { userProfileIdAtom } from '@/context/app-context-state' import CommentPreview from './comment-preview' type CommentIconProps = { @@ -18,8 +19,8 @@ type CommentIconProps = { export const CommentIcon: FC = memo(({ comment, onClick, isActive = false, onPositionUpdate }) => { const { flowToScreenPosition, screenToFlowPosition } = useReactFlow() const viewport = useViewport() - const { userProfile } = useAppContext() - const isAuthor = comment.created_by_account?.id === userProfile?.id + const currentUserId = useAtomValue(userProfileIdAtom) + const isAuthor = comment.created_by_account?.id === currentUserId const [showPreview, setShowPreview] = useState(false) const [dragPosition, setDragPosition] = useState<{ x: number, y: number } | null>(null) const [isDragging, setIsDragging] = useState(false) diff --git a/web/app/components/workflow/comment/comment-input.spec.tsx b/web/app/components/workflow/comment/comment-input.spec.tsx index 8796b4ec364..1696f85fa89 100644 --- a/web/app/components/workflow/comment/comment-input.spec.tsx +++ b/web/app/components/workflow/comment/comment-input.spec.tsx @@ -18,6 +18,13 @@ const stableT = (key: string, options?: { ns?: string }) => ( ) let mentionInputProps: MentionInputProps | null = null +const mockAppContextState = vi.hoisted(() => ({ + userProfile: { + id: 'user-1', + name: 'Alice', + avatar_url: 'avatar', + }, +})) vi.mock('react-i18next', () => ({ useTranslation: () => ({ @@ -27,14 +34,20 @@ vi.mock('react-i18next', () => ({ vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ - userProfile: { - id: 'user-1', - name: 'Alice', - avatar_url: 'avatar', - }, + userProfile: mockAppContextState.userProfile, }), })) +vi.mock('@/context/app-context-state', async (importOriginal) => { + const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateAtomMock(importOriginal, () => mockAppContextState) +}) + +vi.mock('jotai', async (importOriginal) => { + const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateJotaiMock(importOriginal) +}) + vi.mock('@langgenius/dify-ui/avatar', () => ({ Avatar: ({ name }: { name: string }) =>
{name}
, default: ({ name }: { name: string }) =>
{name}
, diff --git a/web/app/components/workflow/comment/comment-input.tsx b/web/app/components/workflow/comment/comment-input.tsx index 963111aaa26..2d616e3a798 100644 --- a/web/app/components/workflow/comment/comment-input.tsx +++ b/web/app/components/workflow/comment/comment-input.tsx @@ -1,9 +1,10 @@ import type { FC, PointerEvent as ReactPointerEvent } from 'react' import { Avatar } from '@langgenius/dify-ui/avatar' import { cn } from '@langgenius/dify-ui/cn' +import { useAtomValue } from 'jotai' import { memo, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useAppContext } from '@/context/app-context' +import { userProfileAtom } from '@/context/app-context-state' import { MentionInput } from './mention-input' type CommentInputProps = { @@ -30,7 +31,7 @@ export const CommentInput: FC = memo(({ }) => { const [content, setContent] = useState('') const { t } = useTranslation() - const { userProfile } = useAppContext() + const userProfile = useAtomValue(userProfileAtom) const dragStateRef = useRef<{ pointerId: number | null startPointerX: number diff --git a/web/app/components/workflow/comment/thread.spec.tsx b/web/app/components/workflow/comment/thread.spec.tsx index 996a8e8d344..771443e37ff 100644 --- a/web/app/components/workflow/comment/thread.spec.tsx +++ b/web/app/components/workflow/comment/thread.spec.tsx @@ -4,6 +4,13 @@ import { CommentThread } from './thread' const mockSetCommentPreviewHovering = vi.hoisted(() => vi.fn()) const mockFlowToScreenPosition = vi.hoisted(() => vi.fn(({ x, y }: { x: number, y: number }) => ({ x, y }))) +const mockAppContextState = vi.hoisted(() => ({ + userProfile: { + id: 'user-1', + name: 'Alice', + avatar_url: 'alice.png', + }, +})) const storeState = vi.hoisted(() => ({ mentionableUsersCache: { @@ -33,14 +40,20 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ - userProfile: { - id: 'user-1', - name: 'Alice', - avatar_url: 'alice.png', - }, + userProfile: mockAppContextState.userProfile, }), })) +vi.mock('@/context/app-context-state', async (importOriginal) => { + const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateAtomMock(importOriginal, () => mockAppContextState) +}) + +vi.mock('jotai', async (importOriginal) => { + const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateJotaiMock(importOriginal) +}) + vi.mock('reactflow', () => ({ useReactFlow: () => ({ flowToScreenPosition: mockFlowToScreenPosition, diff --git a/web/app/components/workflow/comment/thread.tsx b/web/app/components/workflow/comment/thread.tsx index 97aadf04b39..1d4b9545449 100644 --- a/web/app/components/workflow/comment/thread.tsx +++ b/web/app/components/workflow/comment/thread.tsx @@ -11,13 +11,14 @@ import { } from '@langgenius/dify-ui/dropdown-menu' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react' +import { useAtomValue } from 'jotai' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useReactFlow, useViewport } from 'reactflow' import Divider from '@/app/components/base/divider' import InlineDeleteConfirm from '@/app/components/base/inline-delete-confirm' import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color' -import { useAppContext } from '@/context/app-context' +import { userProfileAtom, userProfileIdAtom } from '@/context/app-context-state' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import { useParams } from '@/next/navigation' import { useStore } from '../store' @@ -52,8 +53,7 @@ const ThreadMessage: FC<{ className?: string }> = ({ authorId, authorName, avatarUrl, createdAt, content, mentionableNames, className }) => { const { formatTimeFromNow } = useFormatTimeFromNow() - const { userProfile } = useAppContext() - const currentUserId = userProfile?.id + const currentUserId = useAtomValue(userProfileIdAtom) const isCurrentUser = authorId === currentUserId const userColor = isCurrentUser ? undefined : getUserColor(authorId) @@ -175,7 +175,8 @@ export const CommentThread: FC = memo(({ const appId = params.appId as string const { flowToScreenPosition } = useReactFlow() const viewport = useViewport() - const { userProfile } = useAppContext() + const userProfile = useAtomValue(userProfileAtom) + const currentUserId = userProfile.id const { t } = useTranslation() const [replyContent, setReplyContent] = useState('') const [editingCommentContent, setEditingCommentContent] = useState('') @@ -369,7 +370,7 @@ export const CommentThread: FC = memo(({ }, [editingReply, onReplyEdit]) const replies = comment.replies || [] - const isOwnComment = comment.created_by_account?.id === userProfile?.id + const isOwnComment = comment.created_by_account?.id === currentUserId const messageListRef = useRef(null) const previousReplyCountRef = useRef(undefined) const previousCommentIdRef = useRef(undefined) @@ -604,7 +605,7 @@ export const CommentThread: FC = memo(({
{replies.map((reply) => { const isReplyEditing = editingReply?.id === reply.id - const isOwnReply = reply.created_by_account?.id === userProfile?.id + const isOwnReply = reply.created_by_account?.id === currentUserId return (
({ + userProfile: { + id: '', + name: '', + }, +})) + +vi.mock('@/context/app-context-state', async (importOriginal) => { + const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateAtomMock(importOriginal, () => mockAppContextState) +}) + +vi.mock('jotai', async (importOriginal) => { + const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateJotaiMock(importOriginal) +}) vi.mock('@/context/provider-context', () => ({ useProviderContext: () => ({ 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 9b76bc8e46b..a93aaa2a4cd 100644 --- a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx +++ b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx @@ -25,6 +25,22 @@ const mockViewHistory = vi.fn() let mockNodesReadOnly = false let mockTheme: 'light' | 'dark' = 'light' +const mockAppContextState = vi.hoisted(() => ({ + userProfile: { + id: '', + name: '', + }, +})) + +vi.mock('@/context/app-context-state', async (importOriginal) => { + const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateAtomMock(importOriginal, () => mockAppContextState) +}) + +vi.mock('jotai', async (importOriginal) => { + const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateJotaiMock(importOriginal) +}) vi.mock('reactflow', () => ({ useNodes: () => mockUseNodes(), diff --git a/web/app/components/workflow/header/header-in-restoring.tsx b/web/app/components/workflow/header/header-in-restoring.tsx index b098bed6018..cb1ab7c7d64 100644 --- a/web/app/components/workflow/header/header-in-restoring.tsx +++ b/web/app/components/workflow/header/header-in-restoring.tsx @@ -2,6 +2,7 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' import { RiHistoryLine } from '@remixicon/react' +import { useAtomValue } from 'jotai' import { useCallback, useState, @@ -9,7 +10,7 @@ import { import { useTranslation } from 'react-i18next' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' import { Plan } from '@/app/components/billing/type' -import { useSelector as useAppContextSelector } from '@/context/app-context' +import { userProfileAtom } from '@/context/app-context-state' import { useProviderContext } from '@/context/provider-context' import useTheme from '@/hooks/use-theme' import { useInvalidAllLastRun, useResetWorkflowVersionHistory, useRestoreWorkflow } from '@/service/use-workflow' @@ -39,7 +40,7 @@ const HeaderInRestoring = ({ const [isRestorePlanUpgradeModalOpen, setIsRestorePlanUpgradeModalOpen] = useState(false) const { plan, enableBilling } = useProviderContext() const workflowStore = useWorkflowStore() - const userProfile = useAppContextSelector(s => s.userProfile) + const userProfile = useAtomValue(userProfileAtom) const configsMap = useHooksStore(s => s.configsMap) const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId) const { diff --git a/web/app/components/workflow/header/online-users.tsx b/web/app/components/workflow/header/online-users.tsx index efef3868193..2094f69d526 100644 --- a/web/app/components/workflow/header/online-users.tsx +++ b/web/app/components/workflow/header/online-users.tsx @@ -9,10 +9,11 @@ import { PopoverTrigger, } from '@langgenius/dify-ui/popover' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useAtomValue } from 'jotai' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useReactFlow } from 'reactflow' -import { useAppContext } from '@/context/app-context' +import { userProfileIdAtom } from '@/context/app-context-state' import { getAvatar } from '@/service/common' import { useCollaboration } from '../collaboration/hooks/use-collaboration' import { getUserColor } from '../collaboration/utils/user-color' @@ -54,12 +55,11 @@ const OnlineUsers = () => { const { t } = useTranslation() const appId = useStore(s => s.appId) const { onlineUsers, cursors, isEnabled: isCollaborationEnabled } = useCollaboration(appId as string) - const { userProfile } = useAppContext() + const currentUserId = useAtomValue(userProfileIdAtom) const reactFlow = useReactFlow() const [dropdownOpen, setDropdownOpen] = useState(false) const avatarUrls = useAvatarUrls(onlineUsers || []) - const currentUserId = userProfile?.id const fallbackUsername = t('comments.fallback.user', { ns: 'workflow' }) const currentUserSuffix = t('members.you', { ns: 'common' }) diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts index 52cffa1d7ba..548ec5eef25 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts @@ -28,6 +28,14 @@ const commentsUpdateState = vi.hoisted(() => ({ const globalFeatureState = vi.hoisted(() => ({ enableCollaboration: true, })) +const mockAppContextState = vi.hoisted(() => ({ + userProfile: { + id: 'user-1', + name: 'Alice', + email: 'alice@example.com', + avatar_url: 'alice.png', + }, +})) vi.mock('reactflow', () => ({ useReactFlow: () => ({ @@ -43,15 +51,20 @@ vi.mock('@/next/navigation', () => ({ vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ - userProfile: { - id: 'user-1', - name: 'Alice', - email: 'alice@example.com', - avatar_url: 'alice.png', - }, + userProfile: mockAppContextState.userProfile, }), })) +vi.mock('@/context/app-context-state', async (importOriginal) => { + const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateAtomMock(importOriginal, () => mockAppContextState) +}) + +vi.mock('jotai', async (importOriginal) => { + const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateJotaiMock(importOriginal) +}) + vi.mock('@/service/client', () => ({ consoleClient: { systemFeatures: { diff --git a/web/app/components/workflow/hooks/use-workflow-comment.ts b/web/app/components/workflow/hooks/use-workflow-comment.ts index 6373dbed6aa..04b17449771 100644 --- a/web/app/components/workflow/hooks/use-workflow-comment.ts +++ b/web/app/components/workflow/hooks/use-workflow-comment.ts @@ -1,9 +1,10 @@ import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '@/app/components/workflow/comment/types' import { useSuspenseQuery } from '@tanstack/react-query' +import { useAtomValue } from 'jotai' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useReactFlow } from 'reactflow' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' -import { useAppContext } from '@/context/app-context' +import { userProfileAtom } from '@/context/app-context-state' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useParams } from '@/next/navigation' import { consoleClient } from '@/service/client' @@ -67,7 +68,7 @@ export const useWorkflowComment = () => { () => new Map(mentionableUsers.map(user => [user.id, user])), [mentionableUsers], ) - const { userProfile } = useAppContext() + const userProfile = useAtomValue(userProfileAtom) const { data: isCollaborationEnabled } = useSuspenseQuery({ ...systemFeaturesQueryOptions(), select: s => s.enable_collaboration_mode, diff --git a/web/app/components/workflow/nodes/_base/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/_base/__tests__/node.spec.tsx index b6f5c32d025..51a3f8813d8 100644 --- a/web/app/components/workflow/nodes/_base/__tests__/node.spec.tsx +++ b/web/app/components/workflow/nodes/_base/__tests__/node.spec.tsx @@ -11,6 +11,25 @@ const mockHandleNodeIterationChildSizeChange = vi.fn() const mockHandleNodeLoopChildSizeChange = vi.fn() const mockUseNodeResizeObserver = vi.fn() const mockUseCollaboration = vi.fn() +const mockAppContextState = vi.hoisted(() => ({ + userProfile: { + id: 'user-1', + name: 'User', + email: 'user@example.com', + avatar: '', + avatar_url: '', + }, +})) + +vi.mock('@/context/app-context-state', async (importOriginal) => { + const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateAtomMock(importOriginal, () => mockAppContextState) +}) + +vi.mock('jotai', async (importOriginal) => { + const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateJotaiMock(importOriginal) +}) vi.mock('@/app/components/workflow/hooks', () => ({ useNodesReadOnly: () => ({ nodesReadOnly: false }), diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index f30c428890e..515f307db56 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -13,6 +13,7 @@ import { RiPlayLargeLine, } from '@remixicon/react' import { debounce } from 'es-toolkit/compat' +import { useAtomValue } from 'jotai' import * as React from 'react' import { cloneElement, @@ -69,7 +70,7 @@ import { hasRetryNode, isSupportCustomRunForm, } from '@/app/components/workflow/utils' -import { useAppContext } from '@/context/app-context' +import { userProfileAtom } from '@/context/app-context-state' import { useAllBuiltInTools } from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' import { FlowType } from '@/types/common' @@ -114,7 +115,7 @@ const BasePanel: FC = ({ const { t } = useTranslation() const language = useLanguage() const appId = useStore(s => s.appId) - const { userProfile } = useAppContext() + const userProfile = useAtomValue(userProfileAtom) const { isConnected, nodePanelPresence } = useCollaboration(appId as string) const { showMessageLogModal } = useAppStore(useShallow(state => ({ showMessageLogModal: state.showMessageLogModal, diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 9fd1fb6f97e..80eeadad016 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -4,6 +4,7 @@ import type { } from 'react' import type { NodeProps } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' +import { useAtomValue } from 'jotai' import { cloneElement, memo, @@ -28,7 +29,7 @@ import { NodeRunningStatus, } from '@/app/components/workflow/types' import { hasErrorHandleNode, hasRetryNode } from '@/app/components/workflow/utils' -import { useAppContext } from '@/context/app-context' +import { userProfileAtom } from '@/context/app-context-state' import { selectWorkflowNode } from '../../utils/node-navigation' import AddVariablePopupWithPosition from './components/add-variable-popup-with-position' import EntryNodeContainer, { StartNodeTypeEnum } from './components/entry-node-container' @@ -76,7 +77,7 @@ const BaseNode: FC = ({ const { handleNodeIterationChildSizeChange } = useNodeIterationInteractions() const { handleNodeLoopChildSizeChange } = useNodeLoopInteractions() const toolIcon = useToolIcon(data) - const { userProfile } = useAppContext() + const userProfile = useAtomValue(userProfileAtom) const appId = useStore(s => s.appId) const { nodePanelPresence } = useCollaboration(appId as string) const controlMode = useStore(s => s.controlMode) diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/email-configure-modal.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/email-configure-modal.spec.tsx index d9875c75397..7ba7347869f 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/email-configure-modal.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/email-configure-modal.spec.tsx @@ -3,7 +3,11 @@ import { fireEvent, render, screen } from '@testing-library/react' import EmailConfigureModal from '../email-configure-modal' const mockToastError = vi.hoisted(() => vi.fn()) -const mockUseAppContextSelector = vi.hoisted(() => vi.fn()) +const mockAppContextState = vi.hoisted(() => ({ + userProfile: { + email: 'owner@example.com', + }, +})) vi.mock('@langgenius/dify-ui/toast', () => ({ toast: { @@ -13,9 +17,19 @@ vi.mock('@langgenius/dify-ui/toast', () => ({ vi.mock('@/context/app-context', () => ({ useSelector: (selector: (state: { userProfile: { email: string } }) => string) => - mockUseAppContextSelector(selector), + selector({ userProfile: mockAppContextState.userProfile }), })) +vi.mock('@/context/app-context-state', async (importOriginal) => { + const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateAtomMock(importOriginal, () => mockAppContextState) +}) + +vi.mock('jotai', async (importOriginal) => { + const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state') + return createAppContextStateJotaiMock(importOriginal) +}) + vi.mock('../mail-body-input', () => ({ default: ({ value, onChange }: { value: string, onChange: (value: string) => void }) => (