diff --git a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/tips.spec.tsx b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/tips.spec.tsx
index 58171322548..30116606f03 100644
--- a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/tips.spec.tsx
+++ b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/tips.spec.tsx
@@ -1,5 +1,6 @@
import { screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
+import { renderWithAccountProfile } from '@/test/console/account-profile'
import { render } from '@/test/console/render'
import Tips from '../tips'
@@ -9,11 +10,6 @@ const mockConsoleState = vi.hoisted(() => ({
},
}))
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => mockConsoleState)
-})
-
describe('Tips', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -29,7 +25,10 @@ describe('Tips', () => {
})
it('should render email tip in debug mode', () => {
- render()
+ renderWithAccountProfile(
+ ,
+ { accountProfile: mockConsoleState.userProfile },
+ )
expect(screen.getByText('workflow.common.humanInputEmailTipInDebugMode')).toBeInTheDocument()
expect(screen.queryByText('workflow.common.humanInputEmailTip')).not.toBeInTheDocument()
diff --git a/web/app/components/base/chat/chat/answer/human-input-content/tips.tsx b/web/app/components/base/chat/chat/answer/human-input-content/tips.tsx
index 2b1812f55e5..9045ee7789c 100644
--- a/web/app/components/base/chat/chat/answer/human-input-content/tips.tsx
+++ b/web/app/components/base/chat/chat/answer/human-input-content/tips.tsx
@@ -1,8 +1,8 @@
-import { useAtomValue } from 'jotai'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { memo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
-import { userProfileEmailAtom } from '@/context/account-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
type TipsProps = {
showEmailTip: boolean
@@ -10,9 +10,26 @@ type TipsProps = {
showDebugModeTip: boolean
}
+const EmailDebugTip = () => {
+ const { data: email } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile.email,
+ })
+
+ return (
+
+ $['common.humanInputEmailTipInDebugMode']}
+ ns="workflow"
+ components={{ email: }}
+ values={{ email }}
+ />
+
+ )
+}
+
const Tips = ({ showEmailTip, isEmailDebugMode, showDebugModeTip }: TipsProps) => {
const { t } = useTranslation()
- const email = useAtomValue(userProfileEmailAtom)
return (
<>
@@ -23,16 +40,7 @@ const Tips = ({ showEmailTip, isEmailDebugMode, showDebugModeTip }: TipsProps) =
{t(($) => $['common.humanInputEmailTip'], { ns: 'workflow' })}
)}
- {showEmailTip && isEmailDebugMode && (
-
- $['common.humanInputEmailTipInDebugMode']}
- ns="workflow"
- components={{ email: }}
- values={{ email }}
- />
-
- )}
+ {showEmailTip && isEmailDebugMode && }
{showDebugModeTip && (
{t(($) => $['common.humanInputWebappTip'], { ns: 'workflow' })}
diff --git a/web/app/components/workflow-app/__tests__/index.spec.tsx b/web/app/components/workflow-app/__tests__/index.spec.tsx
index 5febdaf4a61..889b6a63490 100644
--- a/web/app/components/workflow-app/__tests__/index.spec.tsx
+++ b/web/app/components/workflow-app/__tests__/index.spec.tsx
@@ -1,6 +1,7 @@
-import type { ReactNode } from 'react'
+import type { ReactElement, ReactNode } from 'react'
import { screen, waitFor } from '@testing-library/react'
-import { render } from '@/test/console/render'
+import { createAccountProfileQueryWrapper } from '@/test/console/account-profile'
+import { render as renderWithConsoleState } from '@/test/console/render'
import { AppACLPermission } from '@/utils/permission'
import WorkflowApp from '../index'
@@ -59,6 +60,11 @@ let appTriggersState: {
let searchParamsValue: string | null = null
+const render = (ui: ReactElement) =>
+ renderWithConsoleState(ui, {
+ wrapper: createAccountProfileQueryWrapper(consoleState.userProfile),
+ })
+
const mockWorkflowStore = {
setState: mockWorkflowStoreSetState,
getState: () => ({
@@ -85,15 +91,6 @@ vi.mock('@/app/components/workflow/store/trigger-status', () => ({
}),
}))
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => ({
- isLoadingCurrentWorkspace: consoleState.isLoadingCurrentWorkspace,
- currentWorkspace: consoleState.currentWorkspace,
- userProfile: consoleState.userProfile,
- workspacePermissionKeys: consoleState.workspacePermissionKeys,
- }))
-})
vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => ({
diff --git a/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx b/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx
index 8eeec984d0e..e57af4ad4a7 100644
--- a/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx
+++ b/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx
@@ -1,15 +1,15 @@
+import type { ReactElement } from 'react'
import { screen } from '@testing-library/react'
import { StrictMode } from 'react'
import { useStore as useAppStore } from '@/app/components/app/store'
-import { render } from '@/test/console/render'
+import { createAccountProfileQueryWrapper } from '@/test/console/account-profile'
+import { render as renderWithConsoleState } from '@/test/console/render'
import WorkflowApp from '../index'
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => ({
- userProfile: { id: 'user-1' },
- }))
-})
+const render = (ui: ReactElement) =>
+ renderWithConsoleState(ui, {
+ wrapper: createAccountProfileQueryWrapper(),
+ })
vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
diff --git a/web/app/components/workflow-app/components/workflow-main.tsx b/web/app/components/workflow-app/components/workflow-main.tsx
index 0952a7fec58..14bb178ff12 100644
--- a/web/app/components/workflow-app/components/workflow-main.tsx
+++ b/web/app/components/workflow-app/components/workflow-main.tsx
@@ -4,6 +4,7 @@ import type { CollaborationUpdate } from '@/app/components/workflow/collaboratio
import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store/store'
import type { Edge, Node } from '@/app/components/workflow/types'
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -18,8 +19,8 @@ import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-update'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
-import { userProfileIdAtom } from '@/context/account-state'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import { fetchWorkflowDraft } from '@/service/workflow'
import { getAppACLCapabilities } from '@/utils/permission'
import { useAvailableNodesMetaData } from '../hooks/use-available-nodes-meta-data'
@@ -89,7 +90,10 @@ const WorkflowMain = ({ nodes, edges, viewport }: WorkflowMainProps) => {
const filteredCursors = Object.fromEntries(
Object.entries(cursors).filter(([userId]) => userId !== myUserId),
)
- const currentUserId = useAtomValue(userProfileIdAtom)
+ const { data: currentUserId } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile.id,
+ })
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const appACLCapabilities = useMemo(
() =>
diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts
index 32dbeda469d..f73df34a5f9 100644
--- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts
+++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts
@@ -1,7 +1,8 @@
import { waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { BlockEnum } from '@/app/components/workflow/types'
-import { renderHook } from '@/test/console/render'
+import { createAccountProfileQueryWrapper } from '@/test/console/account-profile'
+import { renderHook as renderHookWithConsoleState } from '@/test/console/render'
import { AppACLPermission } from '@/utils/permission'
import { useWorkflowInit } from '../use-workflow-init'
@@ -17,6 +18,11 @@ const mockFetchNodesDefaultConfigs = vi.fn()
const mockFetchPublishedWorkflow = vi.fn()
const mockSyncWorkflowDraft = vi.fn()
+const renderHook =
(callback: () => Result) =>
+ renderHookWithConsoleState(callback, {
+ wrapper: createAccountProfileQueryWrapper({ id: 'user-1' }),
+ })
+
let appStoreState: {
appDetail: {
id: string
@@ -46,13 +52,6 @@ vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: typeof appStoreState) => T): T => selector(appStoreState),
}))
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => ({
- userProfile: { id: 'user-1' },
- workspacePermissionKeys: ['app.create_and_management'],
- }))
-})
vi.mock('@/context/permission-state', async () => {
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
return createPermissionStateModuleMock(() => ({
diff --git a/web/app/components/workflow-app/hooks/use-workflow-init.ts b/web/app/components/workflow-app/hooks/use-workflow-init.ts
index 1109744fec7..624211adc4d 100644
--- a/web/app/components/workflow-app/hooks/use-workflow-init.ts
+++ b/web/app/components/workflow-app/hooks/use-workflow-init.ts
@@ -1,13 +1,14 @@
import type { Edge, Node } from '@/app/components/workflow/types'
import type { FileUploadConfigResponse } from '@/models/common'
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
-import { userProfileIdAtom } from '@/context/account-state'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import { useWorkflowConfig } from '@/service/use-workflow'
import {
fetchNodesDefaultConfigs,
@@ -61,7 +62,10 @@ export const useWorkflowInit = () => {
const workflowStore = useWorkflowStore()
const { nodes: nodesTemplate, edges: edgesTemplate } = useWorkflowTemplate()
const appDetail = useAppStore((state) => state.appDetail)!
- const currentUserId = useAtomValue(userProfileIdAtom)
+ const { data: currentUserId } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile.id,
+ })
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const appACLCapabilities = useMemo(
() =>
diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx
index 8724ab59c3c..29c2b93de9d 100644
--- a/web/app/components/workflow-app/index.tsx
+++ b/web/app/components/workflow-app/index.tsx
@@ -2,6 +2,7 @@
import type { Features as FeaturesData } from '@/app/components/base/features/types'
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useEffect, useMemo } from 'react'
import { useStore as useAppStore } from '@/app/components/app/store'
@@ -12,9 +13,9 @@ import { WorkflowContextProvider } from '@/app/components/workflow/context'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status'
import { initialEdges, initialNodes } from '@/app/components/workflow/utils'
-import { userProfileIdAtom } from '@/context/account-state'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { currentWorkspaceAtom, currentWorkspaceLoadingAtom } from '@/context/workspace-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import { useSearchParams } from '@/next/navigation'
import { fetchRunDetail } from '@/service/log'
import { useAppTriggers } from '@/service/use-tools'
@@ -31,7 +32,10 @@ const WorkflowAppWithAdditionalContext = () => {
const workflowStore = useWorkflowStore()
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
- const currentUserId = useAtomValue(userProfileIdAtom)
+ const { data: currentUserId } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile.id,
+ })
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
// Initialize trigger status at application level
diff --git a/web/app/components/workflow/comment/comment-icon.spec.tsx b/web/app/components/workflow/comment/comment-icon.spec.tsx
index ba07479f082..b89b70b9872 100644
--- a/web/app/components/workflow/comment/comment-icon.spec.tsx
+++ b/web/app/components/workflow/comment/comment-icon.spec.tsx
@@ -1,7 +1,9 @@
+import type { ReactElement } from 'react'
import type { WorkflowCommentList } from '@/app/components/workflow/comment/types'
import { fireEvent, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render } from '@/test/console/render'
+import { createAccountProfileQueryWrapper } from '@/test/console/account-profile'
+import { render as renderWithConsoleState } from '@/test/console/render'
import { CommentIcon } from './comment-icon'
type Position = { x: number; y: number }
@@ -18,6 +20,14 @@ const mockConsoleState = vi.hoisted(() => ({
const mockFlowToScreenPosition = vi.fn((position: Position) => position)
const mockScreenToFlowPosition = vi.fn((position: Position) => position)
+const render = (ui: ReactElement) =>
+ renderWithConsoleState(ui, {
+ wrapper: createAccountProfileQueryWrapper({
+ ...mockConsoleState.userProfile,
+ id: mockUserId,
+ }),
+ })
+
vi.mock('reactflow', () => ({
useReactFlow: () => ({
flowToScreenPosition: mockFlowToScreenPosition,
@@ -30,17 +40,6 @@ vi.mock('reactflow', () => ({
}),
}))
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => ({
- ...mockConsoleState,
- userProfile: {
- ...mockConsoleState.userProfile,
- id: mockUserId,
- },
- }))
-})
-
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 42c9e82af6f..e96886de2a9 100644
--- a/web/app/components/workflow/comment/comment-icon.tsx
+++ b/web/app/components/workflow/comment/comment-icon.tsx
@@ -2,11 +2,11 @@
import type { FC, PointerEvent as ReactPointerEvent } from 'react'
import type { WorkflowCommentList } from '@/app/components/workflow/comment/types'
-import { useAtomValue } from 'jotai'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { memo, useCallback, useMemo, useRef, useState } from 'react'
import { useReactFlow, useViewport } from 'reactflow'
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
-import { userProfileIdAtom } from '@/context/account-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import CommentPreview from './comment-preview'
type CommentIconProps = {
@@ -20,7 +20,10 @@ export const CommentIcon: FC = memo(
({ comment, onClick, isActive = false, onPositionUpdate }) => {
const { flowToScreenPosition, screenToFlowPosition } = useReactFlow()
const viewport = useViewport()
- const currentUserId = useAtomValue(userProfileIdAtom)
+ const { data: currentUserId } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile.id,
+ })
const isAuthor = comment.created_by_account?.id === currentUserId
const [showPreview, setShowPreview] = useState(false)
const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null)
diff --git a/web/app/components/workflow/comment/comment-input.spec.tsx b/web/app/components/workflow/comment/comment-input.spec.tsx
index 0ec7cdcd4cb..e1e8107f331 100644
--- a/web/app/components/workflow/comment/comment-input.spec.tsx
+++ b/web/app/components/workflow/comment/comment-input.spec.tsx
@@ -1,7 +1,8 @@
-import type { FC } from 'react'
+import type { FC, ReactElement } from 'react'
import { fireEvent, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render } from '@/test/console/render'
+import { createAccountProfileQueryWrapper } from '@/test/console/account-profile'
+import { render as renderWithConsoleState } from '@/test/console/render'
import { CommentInput } from './comment-input'
type MentionInputProps = {
@@ -26,6 +27,11 @@ const mockConsoleState = vi.hoisted(() => ({
},
}))
+const render = (ui: ReactElement) =>
+ renderWithConsoleState(ui, {
+ wrapper: createAccountProfileQueryWrapper(mockConsoleState.userProfile),
+ })
+
vi.mock('react-i18next', async () => {
const { withSelectorKey } = await import('@/test/i18n-mock')
return {
@@ -35,11 +41,6 @@ vi.mock('react-i18next', async () => {
}
})
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => mockConsoleState)
-})
-
vi.mock('./mention-input', () => ({
MentionInput: ((props: MentionInputProps) => {
mentionInputProps = props
diff --git a/web/app/components/workflow/comment/comment-input.tsx b/web/app/components/workflow/comment/comment-input.tsx
index 4f98189b461..07b1e5a145e 100644
--- a/web/app/components/workflow/comment/comment-input.tsx
+++ b/web/app/components/workflow/comment/comment-input.tsx
@@ -1,10 +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 { useSuspenseQuery } from '@tanstack/react-query'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { userProfileAtom } from '@/context/account-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import { MentionInput } from './mention-input'
type CommentInputProps = {
@@ -25,7 +25,10 @@ export const CommentInput: FC = memo(
({ position, onSubmit, onCancel, autoFocus = true, disabled = false, onPositionChange }) => {
const [content, setContent] = useState('')
const { t } = useTranslation()
- const userProfile = useAtomValue(userProfileAtom)
+ const { data: userProfile } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile,
+ })
const dragStateRef = useRef<
{
pointerId: number | null
diff --git a/web/app/components/workflow/comment/thread.spec.tsx b/web/app/components/workflow/comment/thread.spec.tsx
index 124d5ea96b9..f56f0b5edc5 100644
--- a/web/app/components/workflow/comment/thread.spec.tsx
+++ b/web/app/components/workflow/comment/thread.spec.tsx
@@ -1,7 +1,9 @@
+import type { ReactElement } from 'react'
import type { WorkflowCommentDetail } from '@/app/components/workflow/comment/types'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
-import { render } from '@/test/console/render'
+import { createAccountProfileQueryWrapper } from '@/test/console/account-profile'
+import { render as renderWithConsoleState } from '@/test/console/render'
import { CommentThread } from './thread'
const mockSetCommentPreviewHovering = vi.hoisted(() => vi.fn())
@@ -16,6 +18,11 @@ const mockConsoleState = vi.hoisted(() => ({
},
}))
+const render = (ui: ReactElement) =>
+ renderWithConsoleState(ui, {
+ wrapper: createAccountProfileQueryWrapper(mockConsoleState.userProfile),
+ })
+
const storeState = vi.hoisted(() => ({
mentionableUsersCache: {
'app-1': [
@@ -35,11 +42,6 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({
}),
}))
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => mockConsoleState)
-})
-
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 08a4feb2c85..c2b9aea236d 100644
--- a/web/app/components/workflow/comment/thread.tsx
+++ b/web/app/components/workflow/comment/thread.tsx
@@ -22,14 +22,14 @@ import {
RiDeleteBinLine,
RiMoreFill,
} from '@remixicon/react'
-import { useAtomValue } from 'jotai'
+import { useSuspenseQuery } from '@tanstack/react-query'
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 { userProfileAtom, userProfileIdAtom } from '@/context/account-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useParams } from '@/next/navigation'
import { useStore } from '../store'
@@ -68,7 +68,10 @@ const ThreadMessage: FC<{
className?: string
}> = ({ authorId, authorName, avatarUrl, createdAt, content, mentionableNames, className }) => {
const { formatTimeFromNow } = useFormatTimeFromNow()
- const currentUserId = useAtomValue(userProfileIdAtom)
+ const { data: currentUserId } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile.id,
+ })
const isCurrentUser = authorId === currentUserId
const userColor = isCurrentUser ? undefined : getUserColor(authorId)
@@ -179,7 +182,10 @@ export const CommentThread: FC = memo(
const appId = params.appId as string
const { flowToScreenPosition } = useReactFlow()
const viewport = useViewport()
- const userProfile = useAtomValue(userProfileAtom)
+ const { data: userProfile } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile,
+ })
const currentUserId = userProfile.id
const { t } = useTranslation()
const [replyContent, setReplyContent] = useState('')
diff --git a/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx b/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx
index 06d5070759b..20eb4fbcfde 100644
--- a/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx
+++ b/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx
@@ -13,17 +13,6 @@ const mockHandleLoadBackupDraft = vi.fn()
const mockHandleRefreshWorkflowDraft = vi.fn()
let mockPlanType = Plan.professional
let mockEnableBilling = true
-const mockConsoleState = vi.hoisted(() => ({
- userProfile: {
- id: '',
- name: '',
- },
-}))
-
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => mockConsoleState)
-})
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 1c63792f9bb..e02950f0e9d 100644
--- a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx
+++ b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx
@@ -1,5 +1,6 @@
import type { Shape } from '../../store/workflow'
import { fireEvent, screen, waitFor } from '@testing-library/react'
+import { createAccountProfileQueryClient } from '@/test/console/account-profile'
import { FlowType } from '@/types/common'
import { renderWorkflowComponent } from '../../__tests__/workflow-test-env'
import { WorkflowVersion } from '../../types'
@@ -25,18 +26,6 @@ const mockViewHistory = vi.fn()
let mockNodesReadOnly = false
let mockTheme: 'light' | 'dark' = 'light'
-const mockConsoleState = vi.hoisted(() => ({
- userProfile: {
- id: '',
- name: '',
- },
-}))
-
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => mockConsoleState)
-})
-
vi.mock('reactflow', () => ({
useNodes: () => mockUseNodes(),
}))
@@ -298,6 +287,7 @@ describe('Header layout components', () => {
const onRestoreSettled = vi.fn()
const deleteAllInspectVars = vi.fn()
const currentVersion = createCurrentVersion()
+ const currentUser = { id: 'user-1', name: 'Alice' }
const { store } = renderWorkflowComponent(
,
@@ -316,6 +306,7 @@ describe('Header layout components', () => {
fileSettings: {},
},
},
+ queryClient: createAccountProfileQueryClient(currentUser),
},
)
@@ -337,8 +328,8 @@ describe('Header layout components', () => {
expect(mockEmitRestoreIntent).toHaveBeenCalledWith({
versionId: currentVersion.id,
versionName: currentVersion.marked_name,
- initiatorUserId: '',
- initiatorName: '',
+ initiatorUserId: currentUser.id,
+ initiatorName: currentUser.name,
})
expect(mockEmitRestoreComplete).toHaveBeenCalledWith({
versionId: currentVersion.id,
diff --git a/web/app/components/workflow/header/header-in-restoring.tsx b/web/app/components/workflow/header/header-in-restoring.tsx
index c700fadccab..61962ff9036 100644
--- a/web/app/components/workflow/header/header-in-restoring.tsx
+++ b/web/app/components/workflow/header/header-in-restoring.tsx
@@ -2,13 +2,13 @@ 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 { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
import { Plan } from '@/app/components/billing/type'
-import { userProfileAtom } from '@/context/account-state'
import { useProviderContext } from '@/context/provider-context'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import useTheme from '@/hooks/use-theme'
import {
useInvalidAllLastRun,
@@ -32,7 +32,10 @@ const HeaderInRestoring = ({ onRestoreSettled }: HeaderInRestoringProps) => {
const [isRestorePlanUpgradeModalOpen, setIsRestorePlanUpgradeModalOpen] = useState(false)
const { plan, enableBilling } = useProviderContext()
const workflowStore = useWorkflowStore()
- const userProfile = useAtomValue(userProfileAtom)
+ const { data: userProfile } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile,
+ })
const configsMap = useHooksStore((s) => s.configsMap)
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
const { deleteAllInspectVars } = workflowStore.getState()
diff --git a/web/app/components/workflow/header/online-users.tsx b/web/app/components/workflow/header/online-users.tsx
index 5721b16b5d3..427b9423e63 100644
--- a/web/app/components/workflow/header/online-users.tsx
+++ b/web/app/components/workflow/header/online-users.tsx
@@ -1,15 +1,16 @@
'use client'
+
import type { OnlineUser } from '../collaboration/types/collaboration'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { AvatarFallback, AvatarImage, AvatarRoot } from '@langgenius/dify-ui/avatar'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
-import { useAtomValue } from 'jotai'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useReactFlow } from 'reactflow'
-import { userProfileIdAtom } from '@/context/account-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import { getAvatar } from '@/service/common'
import { useCollaboration } from '../collaboration/hooks/use-collaboration'
import { getUserColor } from '../collaboration/utils/user-color'
@@ -53,7 +54,10 @@ const OnlineUsers = () => {
cursors,
isEnabled: isCollaborationEnabled,
} = useCollaboration(appId as string)
- const currentUserId = useAtomValue(userProfileIdAtom)
+ const { data: currentUserId } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile.id,
+ })
const reactFlow = useReactFlow()
const [dropdownOpen, setDropdownOpen] = useState(false)
const avatarUrls = useAvatarUrls(onlineUsers || [])
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 7f9593a74a8..7f20afb3075 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
@@ -3,6 +3,7 @@ import type {
WorkflowCommentList,
} from '@/app/components/workflow/comment/types'
import { act, waitFor } from '@testing-library/react'
+import { seedAccountProfileQuery } from '@/test/console/account-profile'
import { createConsoleQueryClient, seedSystemFeatures } from '@/test/console/query-data'
import { renderWorkflowHook } from '../../__tests__/workflow-test-env'
import { ControlMode } from '../../types'
@@ -54,36 +55,36 @@ vi.mock('@/next/navigation', () => ({
useParams: () => ({ appId: 'app-1' }),
}))
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => mockConsoleState)
-})
+vi.mock('@/service/client', async (importOriginal) => {
+ const actual = await importOriginal()
-vi.mock('@/service/client', () => ({
- consoleClient: {
- systemFeatures: {
- get: () => ({
- enable_collaboration_mode: globalFeatureState.enableCollaboration,
- }),
- },
- apps: {
- byAppId: {
- workflow: {
- comments: {
- get: (...args: unknown[]) => mockFetchWorkflowComments(...args),
- post: (...args: unknown[]) => mockCreateWorkflowComment(...args),
- byCommentId: {
- delete: (...args: unknown[]) => mockDeleteWorkflowComment(...args),
- get: (...args: unknown[]) => mockFetchWorkflowComment(...args),
- put: (...args: unknown[]) => mockUpdateWorkflowComment(...args),
- resolve: {
- post: (...args: unknown[]) => mockResolveWorkflowComment(...args),
- },
- replies: {
- post: (...args: unknown[]) => mockCreateWorkflowCommentReply(...args),
- byReplyId: {
- delete: (...args: unknown[]) => mockDeleteWorkflowCommentReply(...args),
- put: (...args: unknown[]) => mockUpdateWorkflowCommentReply(...args),
+ return {
+ ...actual,
+ consoleClient: {
+ systemFeatures: {
+ get: () => ({
+ enable_collaboration_mode: globalFeatureState.enableCollaboration,
+ }),
+ },
+ apps: {
+ byAppId: {
+ workflow: {
+ comments: {
+ get: (...args: unknown[]) => mockFetchWorkflowComments(...args),
+ post: (...args: unknown[]) => mockCreateWorkflowComment(...args),
+ byCommentId: {
+ delete: (...args: unknown[]) => mockDeleteWorkflowComment(...args),
+ get: (...args: unknown[]) => mockFetchWorkflowComment(...args),
+ put: (...args: unknown[]) => mockUpdateWorkflowComment(...args),
+ resolve: {
+ post: (...args: unknown[]) => mockResolveWorkflowComment(...args),
+ },
+ replies: {
+ post: (...args: unknown[]) => mockCreateWorkflowCommentReply(...args),
+ byReplyId: {
+ delete: (...args: unknown[]) => mockDeleteWorkflowCommentReply(...args),
+ put: (...args: unknown[]) => mockUpdateWorkflowCommentReply(...args),
+ },
},
},
},
@@ -91,19 +92,9 @@ vi.mock('@/service/client', () => ({
},
},
},
- },
- consoleQuery: {
- systemFeatures: {
- get: {
- queryKey: () => ['console', 'systemFeatures', 'get'],
- queryOptions: (options: Record = {}) => ({
- queryKey: ['console', 'systemFeatures', 'get'],
- ...options,
- }),
- },
- },
- },
-}))
+ consoleQuery: actual.consoleQuery,
+ }
+})
vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () => ({
collaborationManager: {
@@ -156,6 +147,7 @@ const baseCommentDetail = (): WorkflowCommentDetail => ({
const createSeededQueryClient = () => {
const queryClient = createConsoleQueryClient()
+ seedAccountProfileQuery(queryClient, mockConsoleState.userProfile)
seedSystemFeatures(queryClient, {
enable_collaboration_mode: globalFeatureState.enableCollaboration,
})
diff --git a/web/app/components/workflow/hooks/use-workflow-comment.ts b/web/app/components/workflow/hooks/use-workflow-comment.ts
index 34a75525f50..a95554108ee 100644
--- a/web/app/components/workflow/hooks/use-workflow-comment.ts
+++ b/web/app/components/workflow/hooks/use-workflow-comment.ts
@@ -4,11 +4,10 @@ import type {
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 { userProfileAtom } from '@/context/account-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useParams } from '@/next/navigation'
import { consoleClient } from '@/service/client'
@@ -69,7 +68,10 @@ export const useWorkflowComment = () => {
() => new Map(mentionableUsers.map((user) => [user.id, user])),
[mentionableUsers],
)
- const userProfile = useAtomValue(userProfileAtom)
+ const { data: userProfile } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile,
+ })
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 5c495c7fa56..b046d0f078e 100644
--- a/web/app/components/workflow/nodes/_base/__tests__/node.spec.tsx
+++ b/web/app/components/workflow/nodes/_base/__tests__/node.spec.tsx
@@ -11,20 +11,6 @@ const mockHandleNodeIterationChildSizeChange = vi.fn()
const mockHandleNodeLoopChildSizeChange = vi.fn()
const mockUseNodeResizeObserver = vi.fn()
const mockUseCollaboration = vi.fn()
-const mockConsoleState = vi.hoisted(() => ({
- userProfile: {
- id: 'user-1',
- name: 'User',
- email: 'user@example.com',
- avatar: '',
- avatar_url: '',
- },
-}))
-
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => mockConsoleState)
-})
vi.mock('../../../hooks/use-tool-icon', async (importOriginal) => {
const actual = await importOriginal()
diff --git a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx
index 0f16c6f4917..457132a169d 100644
--- a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx
+++ b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx
@@ -1,4 +1,5 @@
'use client'
+
import type { FC } from 'react'
import type { ResourceVarInputs } from '../types'
import type {
@@ -18,7 +19,7 @@ import {
SelectItemText,
SelectTrigger,
} from '@langgenius/dify-ui/select'
-import { useAtomValue } from 'jotai'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import { CheckboxList } from '@/app/components/base/checkbox-list'
import Input from '@/app/components/base/input'
@@ -32,7 +33,7 @@ import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/compone
import ToolDatePicker from '@/app/components/workflow/nodes/tool/components/tool-date-picker'
import ToolDateRangePicker from '@/app/components/workflow/nodes/tool/components/tool-date-range-picker'
import { VarType } from '@/app/components/workflow/types'
-import { userProfileAtom } from '@/context/account-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
import { VarKindType } from '../types'
@@ -94,7 +95,10 @@ const FormInputItem: FC = ({
inPanel,
}) => {
const language = useLanguage()
- const userProfile = useAtomValue(userProfileAtom)
+ const { data: userProfile } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile,
+ })
const timezone = userProfile.timezone ?? 'UTC'
const [toolsOptions, setToolsOptions] = useState(null)
const [isLoadingToolsOptions, setIsLoadingToolsOptions] = useState(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 9a401905759..8956c99e41c 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
@@ -5,8 +5,8 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiCloseLine, RiPlayLargeLine } from '@remixicon/react'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { debounce } from 'es-toolkit/compat'
-import { useAtomValue } from 'jotai'
import { useQueryState } from 'nuqs'
import * as React from 'react'
import { cloneElement, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -47,7 +47,7 @@ import {
hasRetryNode,
isSupportCustomRunForm,
} from '@/app/components/workflow/utils'
-import { userProfileAtom } from '@/context/account-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import { useAllBuiltInTools } from '@/service/use-tools'
import { useAllTriggerPlugins } from '@/service/use-triggers'
import { FlowType } from '@/types/common'
@@ -96,7 +96,10 @@ const BasePanel: FC = ({ id, data, children }) => {
const { t } = useTranslation()
const language = useLanguage()
const appId = useStore((s) => s.appId)
- const userProfile = useAtomValue(userProfileAtom)
+ const { data: userProfile } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile,
+ })
const { isConnected, nodePanelPresence } = useCollaboration(appId as string)
const { showMessageLogModal } = useAppStore(
useShallow((state) => ({
diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx
index 90cd79a78d6..5990ddad373 100644
--- a/web/app/components/workflow/nodes/_base/node.tsx
+++ b/web/app/components/workflow/nodes/_base/node.tsx
@@ -2,7 +2,7 @@ import type { FC, ReactElement } from 'react'
import type { WorkflowTranslator } from './node-sections'
import type { NodeProps } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
-import { useAtomValue } from 'jotai'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { cloneElement, memo, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
@@ -15,7 +15,7 @@ import CopyID from '@/app/components/workflow/nodes/tool/components/copy-id'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum, ControlMode, NodeRunningStatus } from '@/app/components/workflow/types'
import { hasErrorHandleNode, hasRetryNode } from '@/app/components/workflow/utils'
-import { userProfileAtom } from '@/context/account-state'
+import { userProfileQueryOptions } from '@/features/account-profile/client'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
import { useNodePluginInstallation } from '../../hooks/use-node-plugin-installation'
import { useToolIcon } from '../../hooks/use-tool-icon'
@@ -57,7 +57,10 @@ const BaseNode: FC = ({ id, data, children }) => {
const { handleNodeIterationChildSizeChange } = useNodeIterationInteractions()
const { handleNodeLoopChildSizeChange } = useNodeLoopInteractions()
const toolIcon = useToolIcon(data)
- const userProfile = useAtomValue(userProfileAtom)
+ const { data: userProfile } = useSuspenseQuery({
+ ...userProfileQueryOptions(),
+ select: (data) => data.profile,
+ })
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 1b7b3300cc5..31189fc3f7b 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
@@ -1,6 +1,8 @@
+import type { ReactElement } from 'react'
import type { EmailConfig } from '../../../types'
import { fireEvent, screen } from '@testing-library/react'
-import { render } from '@/test/console/render'
+import { createAccountProfileQueryWrapper } from '@/test/console/account-profile'
+import { render as renderWithConsoleState } from '@/test/console/render'
import EmailConfigureModal from '../email-configure-modal'
const mockToastError = vi.hoisted(() => vi.fn())
@@ -10,17 +12,17 @@ const mockConsoleState = vi.hoisted(() => ({
},
}))
+const render = (ui: ReactElement) =>
+ renderWithConsoleState(ui, {
+ wrapper: createAccountProfileQueryWrapper(mockConsoleState.userProfile),
+ })
+
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
error: (message: string) => mockToastError(message),
},
}))
-vi.mock('@/context/account-state', async () => {
- const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
- return createAccountStateModuleMock(() => mockConsoleState)
-})
-
vi.mock('../mail-body-input', () => ({
default: ({ value, onChange }: { value: string; onChange: (value: string) => void }) => (