refactor(web): read account profile from query cache (#40470)

This commit is contained in:
yyh
2026-08-11 11:17:29 +00:00
committed by GitHub
parent 78c7fb9c5c
commit 7352944dec
30 changed files with 157 additions and 141 deletions
@@ -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)
@@ -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)
@@ -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)
@@ -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 } =
@@ -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()
@@ -326,7 +326,6 @@ const mockConsoleState = {
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
isCurrentWorkspaceDatasetOperator: false,
refreshUserProfile: vi.fn(),
}
const { mockConsoleStateReader } = vi.hoisted(() => ({
@@ -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
@@ -169,7 +169,6 @@ const createProviderContext = ({
const createConsoleState = (overrides: Partial<ConsoleStateFixture> = {}): ConsoleStateFixture => {
return {
userProfile: testUserProfile,
refreshUserProfile: vi.fn(),
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
isCurrentWorkspaceEditor: false,
@@ -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(<StatefulAccountSetting />, {
accountProfile: (mockConsoleState.current as ConsoleStateFixture).userProfile,
systemFeatures: {
deployment_edition: deploymentEdition,
webapp_auth: { enabled: true },
@@ -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(<MembersPage />, {
accountProfile: mockConsoleState.current.userProfile,
systemFeatures: { is_email_setup: true },
})
@@ -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)
@@ -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', () => {
<>
<TransferOwnershipModal show onClose={mockOnClose} />
</>,
{
wrapper: createAccountProfileQueryWrapper(mockConsoleState.current.userProfile ?? {}),
},
)
const mockEmailVerification = ({
@@ -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>(STEP.start)
const [code, setCode] = useState<string>('')
const [time, setTime] = useState<number>(0)
@@ -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(
<QueryClientProvider client={queryClient}>
<PreferencePage />
<ToastHost />
</>,
</QueryClientProvider>,
)
}
@@ -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()
@@ -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 {
@@ -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,
@@ -492,7 +492,6 @@ const mainNavUserProfile = {
const consoleState: ConsoleStateFixture = {
userProfile: mainNavUserProfile,
refreshUserProfile: vi.fn(),
currentWorkspace: {
id: 'workspace-1',
name: 'Solar Studio',
@@ -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(
@@ -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() {
<DropdownMenuLinkItem
className="mx-0 h-8 gap-1 px-3 py-1"
href={mailToSupport(
userProfile.email,
userProfileEmail,
plan.type,
langGeniusVersionInfo?.current_version,
SUPPORT_EMAIL_ADDRESS,
@@ -1,3 +1,4 @@
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import { toast } from '@langgenius/dify-ui/toast'
import { cleanup, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
@@ -25,11 +26,6 @@ const mockWorkspaces = vi.hoisted(() => [
},
])
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<GetAccountProfileResponse>,
educationStatus: { is_student: isEducationAccount },
workspacePermissionKeys: null,
})
+6 -3
View File
@@ -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 () => {
@@ -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 } })
+6 -4
View File
@@ -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(
@@ -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}
</span>
<button type="button" onClick={refreshUserProfile}>
refresh user
</button>
<button type="button" onClick={() => void refreshPermissionsAfterMutationDenial()}>
refresh permissions after denial
</button>
@@ -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')
-10
View File
@@ -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 })
})
@@ -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),
}))
@@ -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 (
<ComboboxBaseItem subject={subject} className="pr-3">
@@ -169,7 +172,7 @@ function MemberItem({ member, subject }: MemberItemProps) {
</div>
</div>
<span className="mr-1 system-sm-medium text-text-secondary">{member.name}</span>
{currentUser.email === member.email && (
{currentUserEmail === member.email && (
<span className="system-xs-regular text-text-tertiary">
({t(($) => $.you, { ns: 'common' })})
</span>
+26
View File
@@ -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()
+11
View File
@@ -401,6 +401,17 @@ export const consoleQuery: RouterUtils<typeof consoleClient> = createTanstackQue
},
},
},
timezone: {
post: {
mutationOptions: {
onSuccess: async (_data, _variables, _onMutateResult, context) => {
await context.client.invalidateQueries({
queryKey: consoleQuery.account.profile.get.key(),
})
},
},
},
},
},
apps: {
post: {
-7
View File
@@ -26,7 +26,6 @@ export type ConsoleStateFixture = {
deploymentEdition?: 'COMMUNITY' | 'ENTERPRISE' | 'CLOUD'
brandingEnabled?: boolean
langGeniusVersionInfo?: Partial<LangGeniusVersionInfo>
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<GetWorkspacesCurrentSummaryResponse>(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,
}
}