diff --git a/web/__tests__/billing/billing-integration.test.tsx b/web/__tests__/billing/billing-integration.test.tsx index c10d35c7c90..8fd5afeb2b4 100644 --- a/web/__tests__/billing/billing-integration.test.tsx +++ b/web/__tests__/billing/billing-integration.test.tsx @@ -44,6 +44,7 @@ const render = (ui: ReactElement, options: RenderOptions = {}, vectorSpaceUsageU }) seedEducationStatus(queryClient, mockEducationStatus) const { wrapper } = createConsoleQueryWrapper({ + accountProfile: mockConsoleState.userProfile as { email?: string }, systemFeatures: { deployment_edition: 'CLOUD' }, queryClient, }) @@ -56,10 +57,6 @@ vi.mock('@/context/provider-context', () => ({ useProviderContext: () => mockProviderCtx, })) -vi.mock('@/context/account-state', async () => { - const { createAccountStateModuleMock } = await import('@/test/console/state-fixture') - return createAccountStateModuleMock(() => mockConsoleState) -}) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState) diff --git a/web/__tests__/billing/education-verification-flow.test.tsx b/web/__tests__/billing/education-verification-flow.test.tsx index 290a66711b8..e1e4ead6884 100644 --- a/web/__tests__/billing/education-verification-flow.test.tsx +++ b/web/__tests__/billing/education-verification-flow.test.tsx @@ -31,6 +31,7 @@ const render = (ui: ReactElement, options: RenderOptions = {}) => { }) seedEducationStatus(queryClient, mockEducationStatus) const { wrapper } = createConsoleQueryWrapper({ + accountProfile: mockConsoleState.userProfile as { email?: string }, systemFeatures: { deployment_edition: 'CLOUD' }, queryClient, }) @@ -45,10 +46,6 @@ vi.mock('@/context/provider-context', () => ({ useProviderContext: () => mockProviderCtx, })) -vi.mock('@/context/account-state', async () => { - const { createAccountStateModuleMock } = await import('@/test/console/state-fixture') - return createAccountStateModuleMock(() => mockConsoleState) -}) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState) diff --git a/web/__tests__/billing/pricing-modal-flow.test.tsx b/web/__tests__/billing/pricing-modal-flow.test.tsx index 08978362099..29be63615ef 100644 --- a/web/__tests__/billing/pricing-modal-flow.test.tsx +++ b/web/__tests__/billing/pricing-modal-flow.test.tsx @@ -24,7 +24,10 @@ let mockEducationStatus = { is_student: false, allow_refresh: false, expire_at: const mockFetchSubscriptionUrls = vi.hoisted(() => vi.fn()) const render = (ui: React.ReactElement) => { - const { wrapper } = createConsoleQueryWrapper({ educationStatus: mockEducationStatus }) + const { wrapper } = createConsoleQueryWrapper({ + accountProfile: mockConsoleState.userProfile as { email?: string }, + educationStatus: mockEducationStatus, + }) return renderWithConsoleState(ui, { wrapper }) } @@ -33,10 +36,6 @@ vi.mock('@/context/provider-context', () => ({ useProviderContext: () => mockProviderCtx, })) -vi.mock('@/context/account-state', async () => { - const { createAccountStateModuleMock } = await import('@/test/console/state-fixture') - return createAccountStateModuleMock(() => mockConsoleState) -}) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState) diff --git a/web/app/account/(commonLayout)/delete-account/components/check-email.tsx b/web/app/account/(commonLayout)/delete-account/components/check-email.tsx index 8dbd69da8af..63d07d562de 100644 --- a/web/app/account/(commonLayout)/delete-account/components/check-email.tsx +++ b/web/app/account/(commonLayout)/delete-account/components/check-email.tsx @@ -1,10 +1,10 @@ 'use client' import { Button } from '@langgenius/dify-ui/button' import { Input } from '@langgenius/dify-ui/input' -import { useAtomValue } from 'jotai' +import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { userProfileEmailAtom } from '@/context/account-state' +import { userProfileQueryOptions } from '@/features/account-profile/client' import Link from '@/next/link' import { useSendDeleteAccountEmail } from '../state' @@ -15,7 +15,10 @@ type DeleteAccountProps = { export default function CheckEmail(props: DeleteAccountProps) { const { t } = useTranslation() - const userProfileEmail = useAtomValue(userProfileEmailAtom) + const { data: userProfileEmail } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile.email, + }) const [userInputEmail, setUserInputEmail] = useState('') const { isPending: isSendingEmail, mutateAsync: getDeleteEmailVerifyCode } = diff --git a/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx b/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx index 027907487fd..5b1016f658e 100644 --- a/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx +++ b/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx @@ -3,10 +3,10 @@ import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { Textarea } from '@langgenius/dify-ui/textarea' import { toast } from '@langgenius/dify-ui/toast' -import { useAtomValue } from 'jotai' +import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { userProfileEmailAtom } from '@/context/account-state' +import { userProfileQueryOptions } from '@/features/account-profile/client' import { useRouter } from '@/next/navigation' import { useLogout } from '@/service/use-common' import { useDeleteAccountFeedback } from '../state' @@ -18,7 +18,10 @@ type DeleteAccountProps = { export default function FeedBack(props: DeleteAccountProps) { const { t } = useTranslation() - const userProfileEmail = useAtomValue(userProfileEmailAtom) + const { data: userProfileEmail } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile.email, + }) const router = useRouter() const [userFeedback, setUserFeedback] = useState('') const { isPending, mutateAsync: sendFeedback } = useDeleteAccountFeedback() diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx index 5be40dee395..a057f2e6e22 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx @@ -326,7 +326,6 @@ const mockConsoleState = { isCurrentWorkspaceManager: false, isCurrentWorkspaceOwner: false, isCurrentWorkspaceDatasetOperator: false, - refreshUserProfile: vi.fn(), } const { mockConsoleStateReader } = vi.hoisted(() => ({ diff --git a/web/app/components/billing/apps-full-in-dialog/index.tsx b/web/app/components/billing/apps-full-in-dialog/index.tsx index 639a325f5cc..68d24c158da 100644 --- a/web/app/components/billing/apps-full-in-dialog/index.tsx +++ b/web/app/components/billing/apps-full-in-dialog/index.tsx @@ -4,21 +4,25 @@ import type { FC } from 'react' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Meter, MeterIndicator, MeterTrack } from '@langgenius/dify-ui/meter' +import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useTranslation } from 'react-i18next' import { Plan } from '@/app/components/billing/type' import { mailToSupport } from '@/app/components/header/utils/util' -import { userProfileEmailAtom } from '@/context/account-state' import { useProviderContext } from '@/context/provider-context' import { langGeniusCurrentVersionAtom } from '@/context/version-state' +import { userProfileQueryOptions } from '@/features/account-profile/client' import UpgradeBtn from '../upgrade-btn' import s from './style.module.css' const AppsFull: FC<{ loc: string; className?: string }> = ({ loc, className }) => { const { t } = useTranslation() const { plan } = useProviderContext() - const userProfileEmail = useAtomValue(userProfileEmailAtom) + const { data: userProfileEmail } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile.email, + }) const currentVersion = useAtomValue(langGeniusCurrentVersionAtom) const isTeam = plan.type === Plan.team const usage = plan.usage.buildApps diff --git a/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx b/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx index 5141dbd1523..a9f8cc4e0fc 100644 --- a/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx +++ b/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx @@ -169,7 +169,6 @@ const createProviderContext = ({ const createConsoleState = (overrides: Partial = {}): ConsoleStateFixture => { return { userProfile: testUserProfile, - refreshUserProfile: vi.fn(), isCurrentWorkspaceManager: true, isCurrentWorkspaceOwner: false, isCurrentWorkspaceEditor: false, diff --git a/web/app/components/header/account-setting/__tests__/index.spec.tsx b/web/app/components/header/account-setting/__tests__/index.spec.tsx index 7c0ed3d82d9..b8301eadeea 100644 --- a/web/app/components/header/account-setting/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/index.spec.tsx @@ -20,10 +20,6 @@ vi.mock('@/context/provider-context', async (importOriginal) => { } }) -vi.mock('@/context/account-state', async () => { - const { createAccountStateModuleMock } = await import('@/test/console/state-fixture') - return createAccountStateModuleMock(() => mockConsoleState.current ?? {}) -}) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState.current ?? {}) @@ -151,7 +147,6 @@ const baseConsoleState: ConsoleStateFixture = { avatar_url: '', is_password_set: false, }, - refreshUserProfile: vi.fn(), currentWorkspace: { id: '1', name: 'Workspace', @@ -214,6 +209,7 @@ describe('AccountSetting', () => { } return renderWithConsoleQuery(, { + accountProfile: (mockConsoleState.current as ConsoleStateFixture).userProfile, systemFeatures: { deployment_edition: deploymentEdition, webapp_auth: { enabled: true }, diff --git a/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx index c1a38de58cc..d7e6a64cff0 100644 --- a/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx @@ -19,10 +19,6 @@ const mockConsoleState = vi.hoisted(() => ({ })) const mockConsoleStateReader = vi.hoisted(() => vi.fn()) -vi.mock('@/context/account-state', async () => { - const { createAccountStateModuleMock } = await import('@/test/console/state-fixture') - return createAccountStateModuleMock(() => mockConsoleState.current) -}) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState.current) @@ -39,6 +35,7 @@ vi.mock('@/service/use-common') const renderMembersPage = () => renderWithConsoleQuery(, { + accountProfile: mockConsoleState.current.userProfile, systemFeatures: { is_email_setup: true }, }) diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index 06a92891eaa..9f10af7470d 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -12,11 +12,11 @@ import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar' import { NUM_INFINITE } from '@/app/components/billing/config' import { Plan } from '@/app/components/billing/type' import UpgradeBtn from '@/app/components/billing/upgrade-btn' -import { userProfileEmailAtom } from '@/context/account-state' import { useLocale } from '@/context/i18n' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useProviderContext } from '@/context/provider-context' import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state' +import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { getAccessControlTemplateLanguage, LanguagesSupported } from '@/i18n-config/language' import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' @@ -35,7 +35,10 @@ const MembersPage = () => { const locale = useLocale() const language = getAccessControlTemplateLanguage(locale) - const userProfileEmail = useAtomValue(userProfileEmailAtom) + const { data: userProfileEmail } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile.email, + }) const currentWorkspace = useAtomValue(currentWorkspaceAtom) const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx index 1508dc1d7d8..66775ac0a18 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/__tests__/index.spec.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event' import { vi } from 'vitest' import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common' import { useMembers } from '@/service/use-common' +import { createAccountProfileQueryWrapper } from '@/test/console/account-profile' import { render } from '@/test/console/render' import TransferOwnershipModal from '../index' @@ -15,10 +16,6 @@ const mockConsoleState = vi.hoisted(() => ({ })) const mockConsoleStateReader = vi.hoisted(() => vi.fn()) -vi.mock('@/context/account-state', async () => { - const { createAccountStateModuleMock } = await import('@/test/console/state-fixture') - return createAccountStateModuleMock(() => mockConsoleState.current) -}) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState.current) @@ -83,6 +80,9 @@ describe('TransferOwnershipModal', () => { <> , + { + wrapper: createAccountProfileQueryWrapper(mockConsoleState.current.userProfile ?? {}), + }, ) const mockEmailVerification = ({ diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx index c8ee7c96dcb..a1a858d4ce7 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx @@ -2,12 +2,13 @@ import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Input } from '@langgenius/dify-ui/input' import { toast } from '@langgenius/dify-ui/toast' +import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useCallback, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { userProfileAtom } from '@/context/account-state' import { currentWorkspaceAtom } from '@/context/workspace-state' +import { userProfileQueryOptions } from '@/features/account-profile/client' import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common' import MemberSelector from './member-selector' @@ -29,7 +30,10 @@ const getErrorMessage = (error: unknown) => { const TransferOwnershipModal = ({ onClose, show }: Props) => { const { t } = useTranslation() const currentWorkspace = useAtomValue(currentWorkspaceAtom) - const userProfile = useAtomValue(userProfileAtom) + const { data: userProfile } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile, + }) const [step, setStep] = useState(STEP.start) const [code, setCode] = useState('') const [time, setTime] = useState(0) diff --git a/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx index 3006dc12a84..10e966091bf 100644 --- a/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/preference-page/__tests__/index.spec.tsx @@ -1,30 +1,31 @@ import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen' import { ToastHost } from '@langgenius/dify-ui/toast' +import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { languages } from '@/i18n-config/language' import { updateUserProfile } from '@/service/common' +import { createAccountProfileQueryClient } from '@/test/console/account-profile' import { render } from '@/test/console/render' import { timezones } from '@/utils/timezone' import PreferencePage from '../index' +const mockGet = vi.hoisted(() => vi.fn()) +const mockRequest = vi.hoisted(() => vi.fn()) const mockRefresh = vi.fn() -const mockMutateUserProfile = vi.fn() let mockLocale: string | undefined = 'en-US' let mockUserProfile: GetAccountProfileResponse +vi.mock('@/service/base', () => ({ + get: mockGet, + request: mockRequest, + sseGeneratorPost: vi.fn(), +})) + vi.mock('@/next/navigation', () => ({ useRouter: () => ({ refresh: mockRefresh }), })) -vi.mock('@/context/account-state', async () => { - const { createAccountStateModuleMock } = await import('@/test/console/state-fixture') - return createAccountStateModuleMock(() => ({ - userProfile: mockUserProfile, - refreshUserProfile: mockMutateUserProfile, - })) -}) - vi.mock('@/context/i18n', () => ({ useLocale: () => mockLocale, })) @@ -54,11 +55,12 @@ const createUserProfile = ( }) const renderPage = () => { - render( - <> + const queryClient = createAccountProfileQueryClient(mockUserProfile) + return render( + - , + , ) } @@ -93,6 +95,12 @@ beforeEach(() => { vi.clearAllMocks() mockLocale = 'en-US' mockUserProfile = createUserProfile() + const profileResponse = () => + new Response(JSON.stringify(mockUserProfile), { + headers: { 'content-type': 'application/json' }, + }) + mockGet.mockImplementation(async () => profileResponse()) + mockRequest.mockImplementation(async () => profileResponse()) }) // Rendering @@ -169,7 +177,6 @@ describe('PreferencePage - Interactions', () => { it('should show success toast when timezone updates', async () => { const midwayTimezone = getTimezoneOption('Pacific/Midway') - updateUserProfileMock.mockResolvedValueOnce({ result: 'success' }) renderPage() @@ -177,16 +184,17 @@ describe('PreferencePage - Interactions', () => { expect(await screen.findByText('common.actionMsg.modifiedSuccessfully')).toBeInTheDocument() await waitFor(() => { - expect(updateUserProfileMock).toHaveBeenCalledWith({ - url: '/account/timezone', - body: { timezone: midwayTimezone.value }, - }) + expect(mockRequest).toHaveBeenCalled() }) + expect(mockRequest.mock.calls[0]?.[0]).toEqual(expect.stringContaining('/account/timezone')) + const request = mockRequest.mock.calls[0]?.[2]?.request as Request + expect(request.method).toBe('POST') + await expect(request.json()).resolves.toEqual({ timezone: midwayTimezone.value }) }, 15000) it('should show error toast when timezone update fails', async () => { const midwayTimezone = getTimezoneOption('Pacific/Midway') - updateUserProfileMock.mockRejectedValueOnce(new Error('Timezone failed')) + mockRequest.mockRejectedValueOnce(new Error('Timezone failed')) renderPage() diff --git a/web/app/components/header/account-setting/preference-page/index.tsx b/web/app/components/header/account-setting/preference-page/index.tsx index 389509f70c0..3392578f77b 100644 --- a/web/app/components/header/account-setting/preference-page/index.tsx +++ b/web/app/components/header/account-setting/preference-page/index.tsx @@ -9,15 +9,16 @@ import { SelectTrigger, } from '@langgenius/dify-ui/select' import { toast } from '@langgenius/dify-ui/toast' -import { useAtomValue, useSetAtom } from 'jotai' +import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { useTheme } from 'next-themes' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { refreshUserProfileAtom, userProfileAtom } from '@/context/account-state' import { useLocale } from '@/context/i18n' +import { userProfileQueryOptions } from '@/features/account-profile/client' import { setLocaleOnClient } from '@/i18n-config' import { languages } from '@/i18n-config/language' import { useRouter } from '@/next/navigation' +import { consoleQuery } from '@/service/client' import { updateUserProfile } from '@/service/common' import { timezones } from '@/utils/timezone' @@ -43,8 +44,11 @@ const isThemeOption = (value: string): value is ThemeOption => { export default function PreferencePage() { const locale = useLocale() - const userProfile = useAtomValue(userProfileAtom) - const refreshUserProfile = useSetAtom(refreshUserProfileAtom) + const { data: userProfile } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile, + }) + const updateTimezone = useMutation(consoleQuery.account.timezone.post.mutationOptions()) const [editing, setEditing] = useState(false) const { t } = useTranslation() const router = useRouter() @@ -79,13 +83,10 @@ export default function PreferencePage() { } } const handleSelectTimezone = async (item: TimezoneOption) => { - const url = '/account/timezone' - const bodyKey = 'timezone' setEditing(true) try { - await updateUserProfile({ url, body: { [bodyKey]: item.value } }) + await updateTimezone.mutateAsync({ body: { timezone: item.value } }) toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' })) - refreshUserProfile() } catch (e) { toast.error((e as Error).message) } finally { diff --git a/web/app/components/header/account-setting/update-setting-dialog.tsx b/web/app/components/header/account-setting/update-setting-dialog.tsx index 78402ffdc4d..57405157192 100644 --- a/web/app/components/header/account-setting/update-setting-dialog.tsx +++ b/web/app/components/header/account-setting/update-setting-dialog.tsx @@ -16,7 +16,7 @@ import { DialogTrigger, } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' -import { useAtomValue } from 'jotai' +import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs' @@ -30,7 +30,7 @@ import { dayjsToTimeOfDay, timeOfDayToDayjs, } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils' -import { userProfileAtom } from '@/context/account-state' +import { userProfileQueryOptions } from '@/features/account-profile/client' import { useMutationPluginAutoUpgradeSettings, usePluginAutoUpgradeSettings, @@ -44,8 +44,10 @@ type Props = { const UpdateSettingDialog = ({ category, disabled = false }: Props) => { const { t } = useTranslation() - const userProfile = useAtomValue(userProfileAtom) - const timezone = userProfile.timezone || 'UTC' + const { data: timezone } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile.timezone || 'UTC', + }) const { data: autoUpgradeSetting, error, diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index b1cf891b648..1d57032998d 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -492,7 +492,6 @@ const mainNavUserProfile = { const consoleState: ConsoleStateFixture = { userProfile: mainNavUserProfile, - refreshUserProfile: vi.fn(), currentWorkspace: { id: 'workspace-1', name: 'Solar Studio', diff --git a/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx b/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx index e56dfeea16f..547b5e19a5a 100644 --- a/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx +++ b/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx @@ -52,10 +52,6 @@ vi.mock('@/config', async (importOriginal) => { } }) -vi.mock('@/context/account-state', async () => { - const { createAccountStateModuleMock } = await import('@/test/console/state-fixture') - return createAccountStateModuleMock(() => mockConsoleState.current) -}) vi.mock('@/context/version-state', async () => { const { createVersionStateModuleMock } = await import('@/test/console/state-fixture') return createVersionStateModuleMock(() => mockConsoleState.current) @@ -93,6 +89,7 @@ describe('SupportMenu', () => { const renderSupportMenu = () => { const { wrapper } = createConsoleQueryWrapper({ + accountProfile: mockConsoleState.current.userProfile, systemFeatures: { deployment_edition: deploymentEdition }, }) return render( diff --git a/web/app/components/main-nav/components/support-menu.tsx b/web/app/components/main-nav/components/support-menu.tsx index 9b280dd46c0..a07540f1d24 100644 --- a/web/app/components/main-nav/components/support-menu.tsx +++ b/web/app/components/main-nav/components/support-menu.tsx @@ -10,10 +10,10 @@ import { } from '@/app/components/header/account-dropdown/menu-item-content' import { mailToSupport } from '@/app/components/header/utils/util' import { SUPPORT_EMAIL_ADDRESS, ZENDESK_WIDGET_KEY } from '@/config' -import { userProfileAtom } from '@/context/account-state' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { langGeniusVersionInfoAtom } from '@/context/version-state' +import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' export default function SupportMenu() { @@ -23,7 +23,10 @@ export default function SupportMenu() { select: ({ deployment_edition }) => deployment_edition, }) const { enableBilling, plan } = useProviderContext() - const userProfile = useAtomValue(userProfileAtom) + const { data: userProfileEmail } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile.email, + }) const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom) const { setShowPricingModal } = useModalContext() const hasDedicatedChannel = plan.type !== Plan.sandbox || Boolean(SUPPORT_EMAIL_ADDRESS.trim()) @@ -79,7 +82,7 @@ export default function SupportMenu() { [ }, ]) -vi.mock('@/context/account-state', async () => { - const { createAccountStateModuleMock } = await import('@/test/console/state-fixture') - return createAccountStateModuleMock(() => mockConsoleState) -}) - vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState) @@ -70,6 +66,11 @@ vi.mock('@/service/client', () => ({ }, }, account: { + profile: { + get: { + queryKey: () => [['console', 'account', 'profile', 'get'], { type: 'query' }], + }, + }, education: { autocomplete: { get: { @@ -131,6 +132,7 @@ const setupContext = (isCurrentWorkspaceManager: boolean) => { const renderPage = (isEducationAccount = true) => { const { wrapper } = createConsoleQueryWrapper({ + accountProfile: mockConsoleState.userProfile as Partial, educationStatus: { is_student: isEducationAccount }, workspacePermissionKeys: null, }) diff --git a/web/app/education/user-info.tsx b/web/app/education/user-info.tsx index e5bcd966f6b..44dd2a95b16 100644 --- a/web/app/education/user-info.tsx +++ b/web/app/education/user-info.tsx @@ -1,15 +1,18 @@ import { Avatar } from '@langgenius/dify-ui/avatar' import { Button } from '@langgenius/dify-ui/button' -import { useAtomValue } from 'jotai' +import { useSuspenseQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' -import { userProfileAtom } from '@/context/account-state' +import { userProfileQueryOptions } from '@/features/account-profile/client' import { useRouter } from '@/next/navigation' import { useLogout } from '@/service/use-common' const UserInfo = () => { const router = useRouter() const { t } = useTranslation() - const userProfile = useAtomValue(userProfileAtom) + const { data: userProfile } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile, + }) const { mutateAsync: logout } = useLogout() const handleLogout = async () => { diff --git a/web/app/education/verify/__tests__/verify-flow.spec.tsx b/web/app/education/verify/__tests__/verify-flow.spec.tsx index e169d963974..a1d6ee00501 100644 --- a/web/app/education/verify/__tests__/verify-flow.spec.tsx +++ b/web/app/education/verify/__tests__/verify-flow.spec.tsx @@ -8,13 +8,6 @@ const mockReplace = vi.hoisted(() => vi.fn()) const mockRedirect = vi.hoisted(() => vi.fn(() => null as never)) const mockRequestVerification = vi.hoisted(() => vi.fn()) -vi.mock('@/context/account-state', async () => { - const { createAccountStateModuleMock } = await import('@/test/console/state-fixture') - return createAccountStateModuleMock(() => ({ - userProfile: { email: 'student@university.edu' }, - })) -}) - vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => path, })) @@ -38,6 +31,7 @@ function renderFlow({ isEducationAccount?: boolean }) { const { queryClient, wrapper } = createConsoleQueryWrapper({ + accountProfile: { email: 'student@university.edu' }, educationStatus: { allow_refresh: allowRefresh, is_student: isEducationAccount, @@ -84,6 +78,7 @@ describe('EducationVerifyFlow', () => { it('requests one token on entry and replaces the route with the application form', async () => { mockRequestVerification.mockResolvedValue({ token: 'education token' }) const { queryClient, wrapper } = createConsoleQueryWrapper({ + accountProfile: { email: 'student@university.edu' }, educationStatus: { allow_refresh: false, is_student: false }, }) seedFeatures(queryClient, { education: { enabled: true } }) diff --git a/web/app/education/verify/verify-flow.tsx b/web/app/education/verify/verify-flow.tsx index 2514716d773..5eb074cac91 100644 --- a/web/app/education/verify/verify-flow.tsx +++ b/web/app/education/verify/verify-flow.tsx @@ -4,13 +4,12 @@ import type { EducationStatusResponse } from '@dify/contracts/api/console/accoun import type { GetFeaturesResponse } from '@dify/contracts/api/console/features/types.gen' import type { ReactNode } from 'react' import { Button, buttonVariants } from '@langgenius/dify-ui/button' -import { useMutation, useQuery } from '@tanstack/react-query' -import { useAtomValue } from 'jotai' +import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' -import { userProfileEmailAtom } from '@/context/account-state' import { useDocLink } from '@/context/i18n' +import { userProfileQueryOptions } from '@/features/account-profile/client' import Link from '@/next/link' import { redirect, useRouter } from '@/next/navigation' import { consoleClient, consoleQuery } from '@/service/client' @@ -69,7 +68,10 @@ export function EducationVerifyFlow({ }) { const { t } = useTranslation() const router = useRouter() - const userEmail = useAtomValue(userProfileEmailAtom) + const { data: userEmail } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile.email, + }) const docLink = useDocLink() const verificationStartedRef = useRef(false) const featuresQuery = useQuery( diff --git a/web/context/__tests__/console-bootstrap.spec.tsx b/web/context/__tests__/console-bootstrap.spec.tsx index d6a2449e369..796db50e601 100644 --- a/web/context/__tests__/console-bootstrap.spec.tsx +++ b/web/context/__tests__/console-bootstrap.spec.tsx @@ -13,7 +13,7 @@ import { setAnalyticsConsent } from '@/app/components/base/analytics-consent/con import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils' import { ZENDESK_FIELD_IDS } from '@/config' import { createSystemFeaturesFixture } from '@/test/console/system-features' -import { refreshUserProfileAtom, userProfileAtom } from '../account-state' +import { userProfileAtom } from '../account-state' import { initialWorkspaceSummary } from '../app-context-defaults' import { datasetDefaultPermissionKeysAtom, @@ -210,7 +210,6 @@ function ConsoleBootstrapProbe() { const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom) const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom) const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom) - const refreshUserProfile = useSetAtom(refreshUserProfileAtom) const refreshPermissionsAfterMutationDenial = useSetAtom( refreshWorkspacePermissionKeysAfterMutationDenialAtom, ) @@ -266,9 +265,6 @@ function ConsoleBootstrapProbe() { {langGeniusVersionInfo.current_version}/{langGeniusVersionInfo.latest_version}/ {langGeniusVersionInfo.current_env} - @@ -443,15 +439,6 @@ describe('Console bootstrap', () => { }) describe('Refresh actions', () => { - it('should invalidate the user profile query when refresh is called', async () => { - const { queryClient } = renderConsoleBootstrap() - const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries') - - fireEvent.click(await screen.findByRole('button', { name: /refresh user/i })) - - expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['user-profile'] }) - }) - it('starts a fresh permission request without waiting for an older request', async () => { const { queryClient } = renderConsoleBootstrap() await screen.findByText('dataset keys:dataset.acl.edit') diff --git a/web/context/account-state.ts b/web/context/account-state.ts index a00ffc38e67..06879199ace 100644 --- a/web/context/account-state.ts +++ b/web/context/account-state.ts @@ -1,7 +1,6 @@ 'use client' import { atom } from 'jotai' -import { queryClientAtom } from 'jotai-tanstack-query' import { userProfileQueryOptions } from '@/features/account-profile/client' import { atomWithResolvedSuspenseQuery } from '@/utils/query-atoms' @@ -15,15 +14,6 @@ export const userProfileIdAtom = atom((get) => { return get(userProfileAtom).id }) -export const userProfileEmailAtom = atom((get) => { - return get(userProfileAtom).email -}) - export const accountProfileMetaAtom = atom((get) => { return get(accountProfileQueryAtom).data.meta }) - -export const refreshUserProfileAtom = atom(null, (get) => { - const queryClient = get(queryClientAtom) - queryClient.invalidateQueries({ queryKey: userProfileQueryOptions().queryKey }) -}) diff --git a/web/features/deployments/detail/access/permissions/access-subject-selector/__tests__/add-button.spec.tsx b/web/features/deployments/detail/access/permissions/access-subject-selector/__tests__/add-button.spec.tsx index e1a76d0b74c..f4fa7cee0e4 100644 --- a/web/features/deployments/detail/access/permissions/access-subject-selector/__tests__/add-button.spec.tsx +++ b/web/features/deployments/detail/access/permissions/access-subject-selector/__tests__/add-button.spec.tsx @@ -1,20 +1,13 @@ import type { AccessControlAccount, Subject } from '@/models/access-control' -import { render, screen, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' import { SubjectType } from '@/models/access-control' +import { renderWithAccountProfile as render } from '@/test/console/account-profile' import { AccessSubjectAddButton } from '../add-button' const mockUseSearchAccessSubjects = vi.hoisted(() => vi.fn()) -vi.mock('@/context/account-state', async () => { - const { atom } = await import('jotai') - - return { - userProfileAtom: atom({ email: 'current@example.com' }), - } -}) - vi.mock('@/service/access-control/use-access-subjects', () => ({ useSearchAccessSubjects: (...args: unknown[]) => mockUseSearchAccessSubjects(...args), })) diff --git a/web/features/deployments/detail/access/permissions/access-subject-selector/subject-options.tsx b/web/features/deployments/detail/access/permissions/access-subject-selector/subject-options.tsx index 290ee1c71b1..7db803f9f15 100644 --- a/web/features/deployments/detail/access/permissions/access-subject-selector/subject-options.tsx +++ b/web/features/deployments/detail/access/permissions/access-subject-selector/subject-options.tsx @@ -12,9 +12,9 @@ import { Avatar } from '@langgenius/dify-ui/avatar' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { ComboboxItem, ComboboxItemText } from '@langgenius/dify-ui/combobox' -import { useAtomValue } from 'jotai' +import { useSuspenseQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' -import { userProfileAtom } from '@/context/account-state' +import { userProfileQueryOptions } from '@/features/account-profile/client' import { SubjectType } from '@/models/access-control' export function SubjectItem({ @@ -155,7 +155,10 @@ type MemberItemProps = { } function MemberItem({ member, subject }: MemberItemProps) { - const currentUser = useAtomValue(userProfileAtom) + const { data: currentUserEmail } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile.email, + }) const { t } = useTranslation() return ( @@ -169,7 +172,7 @@ function MemberItem({ member, subject }: MemberItemProps) { {member.name} - {currentUser.email === member.email && ( + {currentUserEmail === member.email && ( ({t(($) => $.you, { ns: 'common' })}) diff --git a/web/service/client.spec.ts b/web/service/client.spec.ts index 65360c80d18..ae63878f264 100644 --- a/web/service/client.spec.ts +++ b/web/service/client.spec.ts @@ -634,6 +634,32 @@ describe('consoleQuery education defaults', () => { }) }) +describe('consoleQuery account profile mutation defaults', () => { + it('should invalidate the account profile after a timezone update', async () => { + const consoleQuery = await loadConsoleQuery() + const queryClient = new QueryClient() + const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries') + + await consoleQuery.account.timezone.post.mutationOptions().onSuccess?.( + { + id: 'user-1', + name: 'Test User', + email: 'test@example.com', + avatar_url: null, + is_password_set: true, + timezone: 'Pacific/Midway', + }, + { body: { timezone: 'Pacific/Midway' } }, + undefined, + createMutationContext(queryClient), + ) + + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.account.profile.get.key(), + }) + }) +}) + describe('consoleQuery app mutation defaults', () => { it('should write an updated app into its exact detail cache', async () => { const consoleQuery = await loadConsoleQuery() diff --git a/web/service/client.ts b/web/service/client.ts index 4c13689daa5..8338063862a 100644 --- a/web/service/client.ts +++ b/web/service/client.ts @@ -401,6 +401,17 @@ export const consoleQuery: RouterUtils = createTanstackQue }, }, }, + timezone: { + post: { + mutationOptions: { + onSuccess: async (_data, _variables, _onMutateResult, context) => { + await context.client.invalidateQueries({ + queryKey: consoleQuery.account.profile.get.key(), + }) + }, + }, + }, + }, }, apps: { post: { diff --git a/web/test/console/state-fixture.ts b/web/test/console/state-fixture.ts index de0d19f9b18..767280f801a 100644 --- a/web/test/console/state-fixture.ts +++ b/web/test/console/state-fixture.ts @@ -26,7 +26,6 @@ export type ConsoleStateFixture = { deploymentEdition?: 'COMMUNITY' | 'ENTERPRISE' | 'CLOUD' brandingEnabled?: boolean langGeniusVersionInfo?: Partial - refreshUserProfile?: () => void refreshCurrentWorkspace?: () => void } @@ -63,9 +62,6 @@ const userProfileAtom = atom(defaultUserProfile) const userProfileIdAtom = atom((get) => get(userProfileAtom).id) const userProfileEmailAtom = atom((get) => get(userProfileAtom).email) const accountProfileMetaAtom = atom({ currentVersion: null, currentEnv: null }) -const refreshUserProfileCallbackAtom = atom({ callback: () => {} }) -const refreshUserProfileAtom = atom(null, (get) => get(refreshUserProfileCallbackAtom).callback()) - const currentWorkspaceAtom = atom(defaultCurrentWorkspace) const currentWorkspaceIdAtom = atom((get) => get(currentWorkspaceAtom).id) const isCurrentWorkspaceManagerAtom = atom(false) @@ -134,7 +130,6 @@ export const seedRegisteredConsoleStateFixture = (store: JotaiStore) => { ...defaultLangGeniusVersionInfo, ...state.langGeniusVersionInfo, }) - store.set(refreshUserProfileCallbackAtom, { callback: state.refreshUserProfile ?? (() => {}) }) store.set(refreshCurrentWorkspaceCallbackAtom, { callback: state.refreshCurrentWorkspace ?? (() => {}), }) @@ -147,7 +142,6 @@ export const createAccountStateModuleMock = (getState: ConsoleStateFixtureResolv const state = getState() return { userProfile: state.userProfile, - refreshUserProfile: state.refreshUserProfile, } }) return { @@ -155,7 +149,6 @@ export const createAccountStateModuleMock = (getState: ConsoleStateFixtureResolv userProfileIdAtom, userProfileEmailAtom, accountProfileMetaAtom, - refreshUserProfileAtom, } }