mirror of
https://github.com/langgenius/dify.git
synced 2026-10-30 18:34:29 +08:00
refactor(web): read account profile from query cache (#40470)
This commit is contained in:
@@ -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()
|
||||
|
||||
-1
@@ -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
|
||||
|
||||
-1
@@ -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
-4
@@ -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 = ({
|
||||
|
||||
+6
-2
@@ -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)
|
||||
|
||||
+26
-18
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
+2
-9
@@ -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),
|
||||
}))
|
||||
|
||||
+7
-4
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user