refactor(web): migrate workflow app context consumers (#38552)

This commit is contained in:
Stephen Zhou
2026-07-08 17:01:30 +08:00
committed by GitHub
parent a278d21741
commit eca2d419b2
32 changed files with 328 additions and 97 deletions

View File

@ -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<typeof import('react')>('react')

View File

@ -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 }> }) => (
<div data-testid="avatar-list">{users.map(user => user.id).join(',')}</div>

View File

@ -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<CommentIconProps> = 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)

View File

@ -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 }) => <div data-testid="avatar">{name}</div>,
default: ({ name }: { name: string }) => <div data-testid="avatar">{name}</div>,

View File

@ -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<CommentInputProps> = memo(({
}) => {
const [content, setContent] = useState('')
const { t } = useTranslation()
const { userProfile } = useAppContext()
const userProfile = useAtomValue(userProfileAtom)
const dragStateRef = useRef<{
pointerId: number | null
startPointerX: number

View File

@ -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,

View File

@ -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<CommentThreadProps> = 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<CommentThreadProps> = 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<HTMLDivElement>(null)
const previousReplyCountRef = useRef<number | undefined>(undefined)
const previousCommentIdRef = useRef<string | undefined>(undefined)
@ -604,7 +605,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
<div className="mt-2 space-y-3 pt-3">
{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 (
<div
key={reply.id}

View File

@ -13,6 +13,22 @@ const mockHandleLoadBackupDraft = vi.fn()
const mockHandleRefreshWorkflowDraft = vi.fn()
let mockPlanType = Plan.professional
let mockEnableBilling = true
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('@/context/provider-context', () => ({
useProviderContext: () => ({

View File

@ -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(),

View File

@ -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 {

View File

@ -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' })

View File

@ -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: {

View File

@ -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,

View File

@ -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 }),

View File

@ -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<BasePanelProps> = ({
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,

View File

@ -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<BaseNodeProps> = ({
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)

View File

@ -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 }) => (
<textarea
@ -80,11 +94,6 @@ const createEmailConfig = (overrides: Partial<EmailConfig> = {}): EmailConfig =>
describe('human-input/delivery-method/email-configure-modal', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseAppContextSelector.mockImplementation(selector => selector({
userProfile: {
email: 'owner@example.com',
},
}))
})
it('should save a valid email configuration with updated values', () => {

View File

@ -18,15 +18,29 @@ type TestEmailSenderProps = {
jumpToEmailConfigModal: () => void
}
const mockUseAppContextSelector = vi.hoisted(() => vi.fn())
const mockEmailConfigureModal = vi.hoisted(() => vi.fn())
const mockTestEmailSender = vi.hoisted(() => vi.fn())
const mockAppContextState = vi.hoisted(() => ({
userProfile: {
email: 'owner@example.com',
},
}))
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('../email-configure-modal', () => ({
default: (props: EmailConfigureModalProps) => {
mockEmailConfigureModal(props)
@ -107,11 +121,6 @@ const getMethodRow = (label: string) => {
describe('human-input/delivery-method/method-item', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseAppContextSelector.mockImplementation(selector => selector({
userProfile: {
email: 'owner@example.com',
},
}))
})
it('should toggle and delete a webapp delivery method', () => {

View File

@ -21,6 +21,28 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
},
}))
const mockAppContextState = vi.hoisted(() => ({
userProfile: {
id: 'user-1',
email: 'owner@example.com',
name: 'Owner',
},
currentWorkspace: {
id: 'workspace-1',
name: 'Product Team',
},
}))
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)
})
type RecordedRequest = {
url: string
method: string
@ -48,14 +70,11 @@ const renderWithProviders = (ui: ReactNode) => {
value={{
userProfile: {
...userProfilePlaceholder,
id: 'user-1',
email: 'owner@example.com',
name: 'Owner',
...mockAppContextState.userProfile,
},
currentWorkspace: {
...initialWorkspaceInfo,
id: 'workspace-1',
name: 'Product Team',
...mockAppContextState.currentWorkspace,
},
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,
@ -67,14 +86,11 @@ const renderWithProviders = (ui: ReactNode) => {
useSelector: selector => selector({
userProfile: {
...userProfilePlaceholder,
id: 'user-1',
email: 'owner@example.com',
name: 'Owner',
...mockAppContextState.userProfile,
},
currentWorkspace: {
...initialWorkspaceInfo,
id: 'workspace-1',
name: 'Product Team',
...mockAppContextState.currentWorkspace,
},
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: true,

View File

@ -8,10 +8,11 @@ import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgeni
import { Switch } from '@langgenius/dify-ui/switch'
import { toast } from '@langgenius/dify-ui/toast'
import { RiBugLine } from '@remixicon/react'
import { useAtomValue } from 'jotai'
import { memo, useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileEmailAtom } from '@/context/app-context-state'
import MailBodyInput from './mail-body-input'
import Recipient from './recipient'
@ -35,7 +36,7 @@ const EmailConfigureModal = ({
availableNodes = [],
}: EmailConfigureModalProps) => {
const { t } = useTranslation()
const email = useAppContextWithSelector(s => s.userProfile.email)
const email = useAtomValue(userProfileEmailAtom)
const [recipients, setRecipients] = useState(config?.recipients || { whole_workspace: false, items: [] })
const [subject, setSubject] = useState(config?.subject || '')
const [body, setBody] = useState(config?.body || '{{#url#}}')

View File

@ -16,11 +16,12 @@ import {
RiRobot2Fill,
RiSendPlane2Line,
} from '@remixicon/react'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge/index'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileEmailAtom } from '@/context/app-context-state'
import { DeliveryMethodType } from '../../types'
import EmailConfigureModal from './email-configure-modal'
import TestEmailSender from './test-email-sender'
@ -51,7 +52,7 @@ const DeliveryMethodItem: FC<DeliveryMethodItemProps> = ({
readonly,
}) => {
const { t } = useTranslation()
const email = useAppContextWithSelector(s => s.userProfile.email)
const email = useAtomValue(userProfileEmailAtom)
const [isHovering, setIsHovering] = useState(false)
const [showEmailModal, setShowEmailModal] = useState(false)
const [showTestEmailModal, setShowTestEmailModal] = useState(false)

View File

@ -4,6 +4,10 @@ import Recipient from '../index'
const mockUseTranslation = vi.hoisted(() => vi.fn())
const mockUseAppContext = vi.hoisted(() => vi.fn())
const mockUseMembers = vi.hoisted(() => vi.fn())
const mockAppContextState = vi.hoisted(() => ({
userProfile: { email: 'owner@example.com' },
currentWorkspace: { name: 'Dify\'s Lab' },
}))
vi.mock('react-i18next', () => ({
useTranslation: () => mockUseTranslation(),
@ -13,6 +17,16 @@ vi.mock('@/context/app-context', () => ({
useAppContext: () => mockUseAppContext(),
}))
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/use-common', () => ({
useMembers: () => mockUseMembers(),
}))
@ -69,10 +83,7 @@ describe('Recipient', () => {
mockUseTranslation.mockReturnValue({
t: (key: string, options?: { workspaceName?: string }) => options?.workspaceName ?? key,
})
mockUseAppContext.mockReturnValue({
userProfile: { email: 'owner@example.com' },
currentWorkspace: { name: 'Dify\'s Lab' },
})
mockUseAppContext.mockReturnValue(mockAppContextState)
mockUseMembers.mockReturnValue({
data: {
accounts: [

View File

@ -3,9 +3,10 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Switch } from '@langgenius/dify-ui/switch'
import { RiGroupLine } from '@remixicon/react'
import { produce } from 'immer'
import { useAtomValue } from 'jotai'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import { currentWorkspaceAtom, userProfileEmailAtom } from '@/context/app-context-state'
import { useMembers } from '@/service/use-common'
import EmailInput from './email-input'
import MemberSelector from './member-selector'
@ -22,7 +23,8 @@ const Recipient = ({
onChange,
}: Props) => {
const { t } = useTranslation()
const { userProfile, currentWorkspace } = useAppContext()
const userProfileEmail = useAtomValue(userProfileEmailAtom)
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
const { data: members } = useMembers()
const accounts = members?.accounts || []
@ -70,14 +72,14 @@ const Recipient = ({
<div className="w-[86px]">
<MemberSelector
value={data.items}
email={userProfile.email}
email={userProfileEmail}
list={accounts}
onSelect={handleMemberSelect}
/>
</div>
</div>
<EmailInput
email={userProfile.email}
email={userProfileEmail}
value={data.items}
list={accounts}
onDelete={handleDelete}

View File

@ -10,6 +10,7 @@ import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgeni
import { toast } from '@langgenius/dify-ui/toast'
import { RiArrowRightSFill } from '@remixicon/react'
import { noop, unionBy } from 'es-toolkit/compat'
import { useAtomValue } from 'jotai'
import { memo, useCallback, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
@ -24,7 +25,7 @@ import {
isSystemVar,
} from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { InputVarType, VarType } from '@/app/components/workflow/types'
import { useAppContext } from '@/context/app-context'
import { currentWorkspaceAtom, userProfileEmailAtom } from '@/context/app-context-state'
import { useMembers } from '@/service/use-common'
import { useTestEmailSender } from '@/service/use-workflow'
import { getHumanInputFormDependencySelectors, isOutput } from '../../utils'
@ -123,7 +124,8 @@ const EmailSenderModal = ({
availableNodes = [],
}: EmailSenderModalProps) => {
const { t } = useTranslation()
const { userProfile, currentWorkspace } = useAppContext()
const userProfileEmail = useAtomValue(userProfileEmailAtom)
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
const appDetail = useAppStore(state => state.appDetail)
const { mutateAsync: testEmailSender } = useTestEmailSender()
@ -236,7 +238,7 @@ const EmailSenderModal = ({
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugDone`}
ns="workflow"
components={{ email: <span className="system-md-semibold text-text-secondary"></span> }}
values={{ email: userProfile.email }}
values={{ email: userProfileEmail }}
/>
</div>
)}
@ -268,7 +270,7 @@ const EmailSenderModal = ({
<div className="mt-4">
<EmailInput
disabled
email={userProfile.email}
email={userProfileEmail}
value={config?.recipients?.items}
list={accounts}
onDelete={noop}
@ -308,7 +310,7 @@ const EmailSenderModal = ({
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip2`}
ns="workflow"
components={{ email: <span className="system-sm-semibold text-text-primary"></span> }}
values={{ email: userProfile.email }}
values={{ email: userProfileEmail }}
/>
</div>
</>
@ -342,7 +344,7 @@ const EmailSenderModal = ({
<div className="mt-4">
<EmailInput
disabled
email={userProfile.email}
email={userProfileEmail}
value={config?.recipients?.items}
list={accounts}
onDelete={noop}

View File

@ -141,6 +141,21 @@ vi.mock('@/context/app-context', () => ({
}),
}))
const mockAppContextState = vi.hoisted(() => ({
userProfile: { id: 'user-1' },
workspacePermissionKeys: [] as string[],
}))
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('@/utils/permission', () => ({
DatasetACLPermission: {
Readonly: 'dataset.acl.readonly',

View File

@ -2,10 +2,11 @@
import type { FC } from 'react'
import type { DataSet } from '@/models/datasets'
import { produce } from 'immer'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { getDatasetACLCapabilities } from '@/utils/permission'
import Item from './dataset-item'
@ -29,8 +30,8 @@ const DatasetList: FC<Props> = ({
settingsModalHeight,
}) => {
const { t } = useTranslation()
const currentUserId = useAppContextSelector(s => s.userProfile?.id)
const workspacePermissionKeys = useAppContextSelector(s => s.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const handleRemove = useCallback((index: number) => {
return () => {

View File

@ -1,6 +1,7 @@
import type { NoteNodeType } from '../note-node/types'
import { useAtomValue } from 'jotai'
import { useCallback } from 'react'
import { useAppContext } from '@/context/app-context'
import { userProfileAtom } from '@/context/app-context-state'
import {
CUSTOM_NOTE_NODE,
} from '../note-node/constants'
@ -11,7 +12,7 @@ import { generateNewNode } from '../utils'
export const useOperator = () => {
const workflowStore = useWorkflowStore()
const { userProfile } = useAppContext()
const userProfile = useAtomValue(userProfileAtom)
const showAuthorStorage = useWorkflowNoteShowAuthorValue()
const handleAddNote = useCallback(() => {

View File

@ -7,6 +7,9 @@ const mockHandleCommentResolve = vi.hoisted(() => vi.fn())
const mockSetActiveCommentId = vi.hoisted(() => vi.fn())
const mockSetControlMode = vi.hoisted(() => vi.fn())
const mockSetShowResolvedComments = vi.hoisted(() => vi.fn())
const mockAppContextState = vi.hoisted(() => ({
userProfile: { id: 'user-1' },
}))
const commentFixtures: WorkflowCommentList[] = [
{
@ -70,10 +73,20 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
userProfile: { id: 'user-1' },
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('@/app/components/workflow/store', () => ({
useStore: (selector: (state: WorkflowStoreSelectionState) => unknown) => selector({
activeCommentId: storeState.activeCommentId,

View File

@ -2,6 +2,7 @@ import type { WorkflowCommentList } from '@/app/components/workflow/comment/type
import { cn } from '@langgenius/dify-ui/cn'
import { Switch } from '@langgenius/dify-ui/switch'
import { RiCheckboxCircleFill, RiCheckboxCircleLine, RiCheckLine, RiCloseLine, RiFilter3Line } from '@remixicon/react'
import { useAtomValue } from 'jotai'
import { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
@ -9,7 +10,7 @@ import { UserAvatarList } from '@/app/components/base/user-avatar-list'
import { useWorkflowComment } from '@/app/components/workflow/hooks/use-workflow-comment'
import { useStore } from '@/app/components/workflow/store'
import { ControlMode } from '@/app/components/workflow/types'
import { useAppContext } from '@/context/app-context'
import { userProfileIdAtom } from '@/context/app-context-state'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
const CommentsPanel = () => {
@ -29,16 +30,16 @@ const CommentsPanel = () => {
handleCommentIconClick(comment)
}, [handleCommentIconClick])
const { userProfile } = useAppContext()
const currentUserId = useAtomValue(userProfileIdAtom)
const filteredSorted = useMemo(() => {
let data = comments
if (!showResolvedComments)
data = data.filter(c => !c.resolved)
if (showOnlyMine)
data = data.filter(c => c.created_by === userProfile?.id)
data = data.filter(c => c.created_by === currentUserId)
return data
}, [comments, showOnlyMine, showResolvedComments, userProfile?.id])
}, [comments, currentUserId, showOnlyMine, showResolvedComments])
const handleResolve = useCallback(async (comment: WorkflowCommentList) => {
if (comment.resolved)

View File

@ -18,6 +18,12 @@ const mockEmitRestoreComplete = vi.fn()
const mockEmitWorkflowUpdate = vi.fn()
let mockPlanType = Plan.professional
let mockEnableBilling = true
const mockAppContextState = vi.hoisted(() => ({
userProfile: {
id: 'test-user-id',
name: 'Test User',
},
}))
const createVersionHistory = (overrides: Partial<VersionHistory> = {}): VersionHistory => ({
id: 'version-id',
@ -60,9 +66,19 @@ type MockVersionHistoryItemProps = {
}
vi.mock('@/context/app-context', () => ({
useSelector: () => ({ id: 'test-user-id' }),
useSelector: () => 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('@/context/provider-context', () => ({
useProviderContext: () => ({
plan: { type: mockPlanType },

View File

@ -3,6 +3,7 @@ import type { VersionHistory } from '@/types/workflow'
import { toast } from '@langgenius/dify-ui/toast'
import { RiArrowDownDoubleLine, RiCloseLine, RiLoader2Line } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -10,7 +11,7 @@ import VersionInfoModal from '@/app/components/app/app-publisher/version-info-mo
import Divider from '@/app/components/base/divider'
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 { useDeleteWorkflow, useInvalidAllLastRun, useResetWorkflowVersionHistory, useRestoreWorkflow, useUpdateWorkflow, useWorkflowVersionHistory } from '@/service/use-workflow'
import { useDSL, useWorkflowRefreshDraft, useWorkflowRun } from '../../hooks'
@ -58,7 +59,7 @@ export const VersionHistoryPanel = ({
const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel)
const currentVersion = useStore(s => s.currentVersion)
const setCurrentVersion = useStore(s => s.setCurrentVersion)
const userProfile = useAppContextSelector(s => s.userProfile)
const userProfile = useAtomValue(userProfileAtom)
const configsMap = useHooksStore(s => s.configsMap)
const canImportExportDSL = useHooksStore(s => s.accessControl.canImportExportDSL)
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)

View File

@ -7,6 +7,7 @@ import {
ContextMenuSeparator,
} from '@langgenius/dify-ui/context-menu'
import { produce } from 'immer'
import { useAtomValue } from 'jotai'
import {
useCallback,
} from 'react'
@ -15,7 +16,7 @@ import { useStore as useReactFlowStore } from 'reactflow'
import { useCreateSnippetFromSelection } from '@/app/components/snippets/hooks/use-create-snippet-from-selection'
import { canCreateAndModifySnippets } from '@/app/components/snippets/utils/permission'
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useNodesInteractions, useNodesReadOnly, useNodesSyncDraft } from './hooks'
import { useWorkflowHistory, WorkflowHistoryEvent } from './hooks/use-workflow-history'
import { ShortcutKbd } from './shortcuts/shortcut-kbd'
@ -235,7 +236,7 @@ export function SelectionContextmenu({
}) {
const { t } = useTranslation()
const { getNodesReadOnly } = useNodesReadOnly()
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { handleNodesCopy, handleNodesDelete, handleNodesDuplicate } = useNodesInteractions()
const isSelectionContextMenu = useStore(s => s.contextMenuTarget?.type === 'selection')