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 }) => (