refactor(web): remove profile bootstrap atom layer (#40474)

This commit is contained in:
yyh
2026-08-12 04:05:57 +00:00
committed by GitHub
parent fbaf23dc8d
commit 28ba7033fa
38 changed files with 273 additions and 491 deletions
@@ -45,6 +45,7 @@ const render = (ui: ReactElement, options: RenderOptions = {}, vectorSpaceUsageU
seedEducationStatus(queryClient, mockEducationStatus)
const { wrapper } = createConsoleQueryWrapper({
accountProfile: mockConsoleState.userProfile as { email?: string },
accountProfileMeta: { currentVersion: '1.0.0' },
systemFeatures: { deployment_edition: 'CLOUD' },
queryClient,
})
@@ -61,11 +62,6 @@ vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => mockConsoleState)
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => mockConsoleState)
})
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowPricingModal: mockSetShowPricingModal,
@@ -32,6 +32,7 @@ const render = (ui: ReactElement, options: RenderOptions = {}) => {
seedEducationStatus(queryClient, mockEducationStatus)
const { wrapper } = createConsoleQueryWrapper({
accountProfile: mockConsoleState.userProfile as { email?: string },
accountProfileMeta: { currentVersion: '1.0.0' },
systemFeatures: { deployment_edition: 'CLOUD' },
queryClient,
})
@@ -50,11 +51,6 @@ vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => mockConsoleState)
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => mockConsoleState)
})
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowPricingModal: mockSetShowPricingModal,
@@ -26,6 +26,7 @@ const mockFetchSubscriptionUrls = vi.hoisted(() => vi.fn())
const render = (ui: React.ReactElement) => {
const { wrapper } = createConsoleQueryWrapper({
accountProfile: mockConsoleState.userProfile as { email?: string },
accountProfileMeta: { currentVersion: '1.0.0' },
educationStatus: mockEducationStatus,
})
return renderWithConsoleState(ui, { wrapper })
@@ -40,11 +41,6 @@ vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => mockConsoleState)
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => mockConsoleState)
})
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
useGetPricingPageLanguage: () => 'en',
@@ -1,92 +0,0 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ProfileBootstrapGate } from '../profile-bootstrap-gate'
const profileQueryKey = ['console', 'account', 'profile', 'get']
type Deferred<T> = {
promise: Promise<T>
resolve: (value: T) => void
reject: (reason?: unknown) => void
}
function createDeferred<T>(): Deferred<T> {
let resolve!: (value: T) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((nextResolve, nextReject) => {
resolve = nextResolve
reject = nextReject
})
return { promise, resolve, reject }
}
const mocks = vi.hoisted(() => ({
profileQuery: undefined as Deferred<{ id: string }> | undefined,
}))
vi.mock('@/features/account-profile/client', () => ({
userProfileQueryOptions: () => ({
queryKey: profileQueryKey,
queryFn: () => mocks.profileQuery!.promise,
}),
}))
function createQueryClient() {
return new QueryClient({
defaultOptions: {
queries: { retry: false },
},
})
}
function renderGate(children: ReactNode, queryClient = createQueryClient()) {
render(
<QueryClientProvider client={queryClient}>
<ProfileBootstrapGate>{children}</ProfileBootstrapGate>
</QueryClientProvider>,
)
return queryClient
}
describe('ProfileBootstrapGate', () => {
beforeEach(() => {
mocks.profileQuery = createDeferred()
})
it('waits for the profile before mounting atom consumers', async () => {
renderGate(<div>Console shell</div>)
expect(screen.queryByText('Console shell')).not.toBeInTheDocument()
await act(async () => {
mocks.profileQuery!.resolve({ id: 'user-1' })
})
expect(await screen.findByText('Console shell')).toBeInTheDocument()
})
it('keeps atom consumers mounted when a cached profile background refetch fails', async () => {
const queryClient = createQueryClient()
queryClient.setQueryData(profileQueryKey, { id: 'user-1' }, { updatedAt: 1 })
renderGate(<div>Console shell</div>, queryClient)
expect(screen.getByText('Console shell')).toBeInTheDocument()
await waitFor(() => {
expect(queryClient.getQueryState(profileQueryKey)?.fetchStatus).toBe('fetching')
})
await act(async () => {
mocks.profileQuery!.reject(new Error('profile refetch failed'))
})
await waitFor(() => {
expect(queryClient.getQueryState(profileQueryKey)?.status).toBe('error')
})
expect(screen.getByText('Console shell')).toBeInTheDocument()
})
})
@@ -3,7 +3,7 @@
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen'
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import { useSuspenseQuery } from '@tanstack/react-query'
import { skipToken, useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { Fragment, useEffect, useRef } from 'react'
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
@@ -12,10 +12,11 @@ import { useAmplitudeInitialized } from '@/app/components/base/amplitude/use-amp
import { useAnalyticsConsent } from '@/app/components/base/analytics-consent/consent-store'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
import { ZENDESK_FIELD_IDS } from '@/config'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import { getLangGeniusVersionInfo } from '@/context/app-context-normalizers'
import { currentWorkspaceAtom } from '@/context/workspace-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { deploymentEditionAtom } from '@/features/system-features/state'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { consoleQuery } from '@/service/client'
type AmplitudeProperties = Record<string, string | number | boolean>
@@ -108,13 +109,37 @@ function syncZendeskField({
}
function ZendeskConversationSync() {
const { data: userProfile } = useSuspenseQuery({
const { data: accountProfile } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.profile,
select: (data) => ({
email: data.profile.email,
meta: data.meta,
}),
})
const { data: systemFeatures } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
select: (data) => ({
brandingEnabled: data.branding.enabled,
deploymentEdition: data.deployment_edition,
}),
})
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const deploymentEdition = useAtomValue(deploymentEditionAtom)
const { data: versionData } = useQuery(
consoleQuery.version.get.queryOptions({
input: accountProfile.meta.currentVersion
? {
query: {
current_version: accountProfile.meta.currentVersion,
},
}
: skipToken,
enabled: !systemFeatures.brandingEnabled,
}),
)
const langGeniusVersionInfo = getLangGeniusVersionInfo({
meta: accountProfile.meta,
versionData,
})
const syncStateRef = useRef<ZendeskSyncState>({})
useEffect(() => {
@@ -122,7 +147,7 @@ function ZendeskConversationSync() {
syncZendeskField({
fieldId: ZENDESK_FIELD_IDS.ENVIRONMENT,
deploymentEdition,
deploymentEdition: systemFeatures.deploymentEdition,
value: langGeniusVersionInfo.current_env.toLowerCase(),
previousValue: syncStateRef.current.environment,
setNextValue: (value) => {
@@ -131,7 +156,7 @@ function ZendeskConversationSync() {
})
syncZendeskField({
fieldId: ZENDESK_FIELD_IDS.VERSION,
deploymentEdition,
deploymentEdition: systemFeatures.deploymentEdition,
value: langGeniusVersionInfo.version,
previousValue: syncStateRef.current.version,
setNextValue: (value) => {
@@ -140,8 +165,8 @@ function ZendeskConversationSync() {
})
syncZendeskField({
fieldId: ZENDESK_FIELD_IDS.EMAIL,
deploymentEdition,
value: userProfile.email,
deploymentEdition: systemFeatures.deploymentEdition,
value: accountProfile.email,
previousValue: syncStateRef.current.email,
setNextValue: (value) => {
nextState.email = value
@@ -149,7 +174,7 @@ function ZendeskConversationSync() {
})
syncZendeskField({
fieldId: ZENDESK_FIELD_IDS.WORKSPACE_ID,
deploymentEdition,
deploymentEdition: systemFeatures.deploymentEdition,
value: currentWorkspace.id,
previousValue: syncStateRef.current.workspaceId,
setNextValue: (value) => {
@@ -158,7 +183,7 @@ function ZendeskConversationSync() {
})
syncStateRef.current = nextState
}, [currentWorkspace.id, deploymentEdition, langGeniusVersionInfo, userProfile.email])
}, [accountProfile.email, currentWorkspace.id, langGeniusVersionInfo, systemFeatures])
return null
}
@@ -1,11 +0,0 @@
'use client'
import type { ReactNode } from 'react'
import { useSuspenseQuery } from '@tanstack/react-query'
import { userProfileQueryOptions } from '@/features/account-profile/client'
export function ProfileBootstrapGate({ children }: { children: ReactNode }) {
useSuspenseQuery(userProfileQueryOptions())
return children
}
+2 -5
View File
@@ -5,17 +5,14 @@ import { ModalContextProvider } from '@/context/modal-context-provider'
import { ProviderContextProvider } from '@/context/provider-context-provider'
import { ExternalServiceSync } from './external-service-sync'
import { CommonLayoutHydrationBoundary } from './hydration-boundary'
import { ProfileBootstrapGate } from './profile-bootstrap-gate'
export async function ConsoleRuntimeProviders({ children }: { children: ReactNode }) {
return (
<>
<OAuthRegistrationAnalytics />
<CommonLayoutHydrationBoundary>
<ProfileBootstrapGate>
<ExternalServiceSync />
{children}
</ProfileBootstrapGate>
<ExternalServiceSync />
{children}
</CommonLayoutHydrationBoundary>
</>
)
@@ -8,14 +8,14 @@ import type { SiteInfo } from '@/models/share'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useSuspenseQuery } from '@tanstack/react-query'
import copy from 'copy-to-clipboard'
import { useAtomValue } from 'jotai'
import { Suspense, use, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { createTheme } from '@/app/components/base/chat/embedded-chatbot/theme/theme'
import { InputVarType } from '@/app/components/workflow/types'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { basePath } from '@/utils/var'
import {
compressAndEncodeBase64,
@@ -141,14 +141,15 @@ const EmbeddedContent = ({
)
const latestResolvedIframeUrlRef = useRef('')
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { data: currentEnv } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.meta.currentEnv,
})
const theme = createTheme(
siteInfo?.chat_color_theme ?? null,
siteInfo?.chat_color_theme_inverted ?? false,
)
const isTestEnv =
langGeniusVersionInfo.current_env === 'TESTING' ||
langGeniusVersionInfo.current_env === 'DEVELOPMENT'
const isTestEnv = currentEnv === 'TESTING' || currentEnv === 'DEVELOPMENT'
const handleHiddenInputValueChange = (variable: string, value: WorkflowLaunchInputValue) => {
const nextHiddenInputValues = {
@@ -5,13 +5,11 @@ 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 { 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'
@@ -19,11 +17,13 @@ import s from './style.module.css'
const AppsFull: FC<{ loc: string; className?: string }> = ({ loc, className }) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const { data: userProfileEmail } = useSuspenseQuery({
const { data: accountProfile } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.profile.email,
select: (data) => ({
email: data.profile.email,
currentVersion: data.meta.currentVersion,
}),
})
const currentVersion = useAtomValue(langGeniusCurrentVersionAtom)
const isTeam = plan.type === Plan.team
const usage = plan.usage.buildApps
const total = plan.total.buildApps
@@ -66,7 +66,11 @@ const AppsFull: FC<{ loc: string; className?: string }> = ({ loc, className }) =
<a
target="_blank"
rel="noopener noreferrer"
href={mailToSupport(userProfileEmail, plan.type, currentVersion)}
href={mailToSupport(
accountProfile.email,
plan.type,
accountProfile.currentVersion ?? '',
)}
>
{t(($) => $['apps.contactUs'], { ns: 'billing' })}
</a>
@@ -7,7 +7,6 @@ import { createMockProviderContextValue } from '@/__mocks__/provider-context'
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
import { defaultPlan } from '@/app/components/billing/config'
import { Plan } from '@/app/components/billing/type'
import { initialLangGeniusVersionInfo } from '@/context/app-context-defaults'
import { useProviderContext } from '@/context/provider-context'
import { createConsoleQueryClient, renderHookWithConsoleQuery } from '@/test/console/query-data'
import useWebAppBrand from '../use-web-app-brand'
@@ -114,14 +113,6 @@ vi.mock('@/context/permission-state', async () => {
refreshCurrentWorkspace: consoleStateRef.value?.refreshCurrentWorkspace,
}))
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => ({
...consoleStateRef.value,
refreshCurrentWorkspace: consoleStateRef.value?.refreshCurrentWorkspace,
}))
})
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
@@ -168,7 +159,6 @@ const createConsoleState = (overrides: Partial<ConsoleStateFixture> = {}): Conso
isCurrentWorkspaceDatasetOperator: false,
workspacePermissionKeys: ['customization.manage'],
refreshCurrentWorkspace: vi.fn(),
langGeniusVersionInfo: initialLangGeniusVersionInfo,
isLoadingCurrentWorkspace: false,
...overrides,
}
@@ -1,5 +1,5 @@
import { act, fireEvent, screen } from '@testing-library/react'
import { render } from '@/test/console/render'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import { DetailSidebarFrame } from '..'
import { DETAIL_SIDEBAR_STORAGE_KEY } from '../storage'
@@ -12,14 +12,6 @@ const { hotkeyRegistrations } = vi.hoisted(() => ({
}
>(),
}))
const mockConsoleState = vi.hoisted(() => ({
current: {
langGeniusVersionInfo: {
current_env: '',
},
},
}))
vi.mock('@tanstack/react-hotkeys', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-hotkeys')>()
return {
@@ -34,11 +26,6 @@ vi.mock('@tanstack/react-hotkeys', async (importOriginal) => {
}
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => mockConsoleState.current)
})
vi.mock('@/app/components/main-nav/components/account-section', () => ({
default: ({ compact }: { compact?: boolean }) => (
<button type="button" aria-label="account">
@@ -59,8 +46,8 @@ vi.mock('@/app/components/header/env-nav', () => ({
default: () => <div>Environment tag</div>,
}))
function renderDetailSidebarFrame() {
return render(
function renderDetailSidebarFrame(currentEnv: string | null = null) {
return renderWithConsoleQuery(
<DetailSidebarFrame
renderTop={({ expand, onToggle }) => (
<div data-testid="detail-top" data-expand={expand}>
@@ -75,6 +62,7 @@ function renderDetailSidebarFrame() {
</div>
)}
/>,
{ accountProfileMeta: { currentEnv } },
)
}
@@ -82,11 +70,6 @@ describe('DetailSidebarFrame', () => {
beforeEach(() => {
localStorage.clear()
hotkeyRegistrations.clear()
mockConsoleState.current = {
langGeniusVersionInfo: {
current_env: '',
},
}
})
it('renders expanded detail content by default and registers the shortcut for focused inputs', () => {
@@ -109,13 +92,7 @@ describe('DetailSidebarFrame', () => {
})
it('collapses detail content from the top toggle and hides environment metadata', () => {
mockConsoleState.current = {
langGeniusVersionInfo: {
current_env: 'TESTING',
},
}
renderDetailSidebarFrame()
renderDetailSidebarFrame('TESTING')
fireEvent.click(screen.getByTestId('detail-toggle'))
expect(screen.getByTestId('detail-top')).toHaveAttribute('data-expand', 'false')
+6 -4
View File
@@ -3,12 +3,12 @@
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { useHotkey } from '@tanstack/react-hotkeys'
import { useAtomValue } from 'jotai'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
import EnvNav from '@/app/components/header/env-nav'
import AccountSection from '@/app/components/main-nav/components/account-section'
import HelpMenu from '@/app/components/main-nav/components/help-menu'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { DETAIL_SIDEBAR_TOGGLE_HOTKEY } from './hotkeys'
import { useDetailSidebarMode } from './storage'
@@ -44,7 +44,10 @@ export function DetailSidebarFrame({
renderTop,
renderSection,
}: DetailSidebarFrameProps) {
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { data: currentEnv } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.meta.currentEnv,
})
const [storedDetailSidebarExpand, setStoredDetailSidebarExpand] = useDetailSidebarMode()
const detailNavigationMode = storedDetailSidebarExpand === 'collapse' ? 'collapse' : 'expand'
const detailNavigationExpanded = detailNavigationMode === 'expand'
@@ -60,7 +63,6 @@ export function DetailSidebarFrame({
const detailNavigationVisibleExpanded =
detailNavigationExpanded || isDetailNavigationHoverPreviewOpen
const bottomNavigationExpanded = detailNavigationVisibleExpanded
const currentEnv = langGeniusVersionInfo?.current_env
const showEnvTag = currentEnv === 'TESTING' || currentEnv === 'DEVELOPMENT'
function handleToggleDetailNavigation() {
@@ -28,11 +28,6 @@ vi.mock('@/context/permission-state', async () => {
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
return createPermissionStateModuleMock(() => mockConsoleState.current ?? {})
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => mockConsoleState.current ?? {})
})
vi.mock('@/next/navigation', () => ({
useRouter: vi.fn(() => ({
push: vi.fn(),
@@ -158,13 +153,6 @@ const baseConsoleState: ConsoleStateFixture = {
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceDatasetOperator: false,
refreshCurrentWorkspace: vi.fn(),
langGeniusVersionInfo: {
current_env: 'testing',
current_version: '0.1.0',
latest_version: '0.1.0',
release_notes: '',
version: '0.1.0',
},
isLoadingCurrentWorkspace: false,
workspacePermissionKeys: [
'workspace.member.manage',
@@ -1,18 +1,12 @@
import type { ConsoleStateFixture } from '@/test/console/state-fixture'
import { screen } from '@testing-library/react'
import { vi } from 'vitest'
import { render } from '@/test/console/render'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import EnvNav from '../index'
const mockConsoleState = vi.hoisted(() => ({
current: {} as Partial<ConsoleStateFixture>,
}))
const mockConsoleStateReader = vi.hoisted(() => vi.fn())
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => mockConsoleState.current)
})
const renderEnvNav = (currentEnv: string) =>
renderWithConsoleQuery(<EnvNav />, {
accountProfileMeta: { currentEnv },
})
describe('EnvNav', () => {
beforeEach(() => {
@@ -20,41 +14,17 @@ describe('EnvNav', () => {
})
it('should render null when environment is PRODUCTION', () => {
const consoleState = {
langGeniusVersionInfo: {
current_env: 'PRODUCTION',
},
} as unknown as ConsoleStateFixture
mockConsoleState.current = consoleState
mockConsoleStateReader.mockReturnValue(consoleState)
const { container } = render(<EnvNav />)
const { container } = renderEnvNav('PRODUCTION')
expect(container.firstChild).toBeNull()
})
it('should render TESTING tag and icon when environment is TESTING', () => {
const consoleState = {
langGeniusVersionInfo: {
current_env: 'TESTING',
},
} as unknown as ConsoleStateFixture
mockConsoleState.current = consoleState
mockConsoleStateReader.mockReturnValue(consoleState)
render(<EnvNav />)
renderEnvNav('TESTING')
expect(screen.getByText('common.environment.testing')).toBeInTheDocument()
})
it('should render DEVELOPMENT tag and icon when environment is DEVELOPMENT', () => {
const consoleState = {
langGeniusVersionInfo: {
current_env: 'DEVELOPMENT',
},
} as unknown as ConsoleStateFixture
mockConsoleState.current = consoleState
mockConsoleStateReader.mockReturnValue(consoleState)
render(<EnvNav />)
renderEnvNav('DEVELOPMENT')
expect(screen.getByText('common.environment.development')).toBeInTheDocument()
})
})
+10 -9
View File
@@ -1,8 +1,8 @@
'use client'
import { useAtomValue } from 'jotai'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
const headerEnvClassName: { [k: string]: string } = {
DEVELOPMENT: 'bg-[#FEC84B] border-[#FDB022] text-[#93370D]',
@@ -11,24 +11,25 @@ const headerEnvClassName: { [k: string]: string } = {
const EnvNav = () => {
const { t } = useTranslation()
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const showEnvTag =
langGeniusVersionInfo.current_env === 'TESTING' ||
langGeniusVersionInfo.current_env === 'DEVELOPMENT'
const { data: currentEnv } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.meta.currentEnv,
})
const showEnvTag = currentEnv === 'TESTING' || currentEnv === 'DEVELOPMENT'
if (!showEnvTag) return null
return (
<div
className={`mr-1 flex h-5.5 items-center rounded-md border px-2 text-xs font-medium ${headerEnvClassName[langGeniusVersionInfo.current_env]} `}
className={`mr-1 flex h-5.5 items-center rounded-md border px-2 text-xs font-medium ${headerEnvClassName[currentEnv]} `}
>
{langGeniusVersionInfo.current_env === 'TESTING' && (
{currentEnv === 'TESTING' && (
<>
<span aria-hidden className="i-custom-vender-solid-education-beaker-02 size-3" />
<div className="ml-1">{t(($) => $['environment.testing'], { ns: 'common' })}</div>
</>
)}
{langGeniusVersionInfo.current_env === 'DEVELOPMENT' && (
{currentEnv === 'DEVELOPMENT' && (
<>
<span aria-hidden className="i-custom-vender-solid-development-terminal-square size-3" />
<div className="ml-1">{t(($) => $['environment.development'], { ns: 'common' })}</div>
@@ -3,6 +3,7 @@ import type {
StepByStepTourStatePatchPayload,
StepByStepTourStateResponse,
} from '@dify/contracts/api/console/onboarding/types.gen'
import type { GetVersionResponse } from '@dify/contracts/api/console/version/types.gen'
import type {
GetWorkspacesCurrentSummaryResponse,
TenantListItemResponse,
@@ -12,6 +13,7 @@ import type { Mock } from 'vitest'
import type { StepByStepTourSessionState } from '@/app/components/step-by-step-tour/types'
import type { ModalContextState } from '@/context/modal-context'
import type { ProviderContextState } from '@/context/provider-context'
import type { UserProfileWithMeta } from '@/features/account-profile/client'
import type { ConsoleStateFixture } from '@/test/console/state-fixture'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
@@ -156,8 +158,13 @@ const mockStepByStepTour = vi.hoisted(() => {
stateQueryKey,
}
})
type MainNavConsoleState = ConsoleStateFixture & {
profileMeta: UserProfileWithMeta['meta']
versionData: GetVersionResponse
}
const mockConsoleState = vi.hoisted(() => ({
current: undefined as ConsoleStateFixture | undefined,
current: undefined as MainNavConsoleState | undefined,
}))
vi.mock('@/features/agent-v2/feature-flag', () => ({
@@ -176,11 +183,6 @@ vi.mock('@/context/permission-state', async () => {
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
return createPermissionStateModuleMock(() => mockConsoleState.current ?? {})
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => mockConsoleState.current ?? {})
})
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
@@ -486,7 +488,7 @@ const mainNavUserProfile = {
is_password_set: true,
}
const consoleState: ConsoleStateFixture = {
const consoleState: MainNavConsoleState = {
userProfile: mainNavUserProfile,
currentWorkspace: {
id: 'workspace-1',
@@ -500,12 +502,13 @@ const consoleState: ConsoleStateFixture = {
isCurrentWorkspaceEditor: true,
isCurrentWorkspaceDatasetOperator: false,
refreshCurrentWorkspace: vi.fn(),
langGeniusVersionInfo: {
current_env: 'testing',
current_version: '1.0.0',
latest_version: '1.0.0',
release_notes: '',
profileMeta: {
currentEnv: 'testing',
currentVersion: '1.0.0',
},
versionData: {
version: '1.0.0',
release_notes: '',
},
isLoadingCurrentWorkspace: false,
isLoadingWorkspacePermissionKeys: false,
@@ -545,10 +548,19 @@ const renderMainNav = (
...(currentConsoleState.userProfile ?? {}),
},
meta: {
currentVersion: null,
currentEnv: null,
currentVersion: currentConsoleState.profileMeta.currentVersion,
currentEnv: currentConsoleState.profileMeta.currentEnv,
},
})
const currentVersion = currentConsoleState.profileMeta.currentVersion
if (currentVersion) {
queryClient.setQueryData(
consoleQuery.version.get.queryOptions({
input: { query: { current_version: currentVersion } },
}).queryKey,
currentConsoleState.versionData,
)
}
queryClient.setQueryData(consoleQuery.workspaces.get.queryKey(), { workspaces: mockWorkspaces })
queryClient.setQueryData(mockStepByStepTour.stateQueryKey, mockStepByStepTour.state)
const store = options.store ?? createStore()
@@ -1124,9 +1136,8 @@ describe('MainNav', () => {
const user = userEvent.setup()
mockConsoleState.current = {
...consoleState,
langGeniusVersionInfo: {
...consoleState.langGeniusVersionInfo,
latest_version: '1.1.0',
versionData: {
version: '1.1.0',
release_notes: 'https://github.com/langgenius/dify/releases/tag/1.1.0',
},
}
@@ -52,11 +52,6 @@ vi.mock('@/config', async (importOriginal) => {
}
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => mockConsoleState.current)
})
vi.mock('@/context/modal-context', () => ({
useModalContext: vi.fn(),
}))
@@ -90,6 +85,9 @@ describe('SupportMenu', () => {
const renderSupportMenu = () => {
const { wrapper } = createConsoleQueryWrapper({
accountProfile: mockConsoleState.current.userProfile,
accountProfileMeta: {
currentVersion: mockConsoleState.current.langGeniusVersionInfo.current_version,
},
systemFeatures: { deployment_edition: deploymentEdition },
})
return render(
@@ -13,7 +13,7 @@ import {
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { Switch } from '@langgenius/dify-ui/switch'
import { useSuspenseQuery } from '@tanstack/react-query'
import { skipToken, useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue, useSetAtom } from 'jotai'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -36,15 +36,17 @@ import {
stepByStepTourStateUpdatingAtom,
} from '@/app/components/step-by-step-tour/state'
import { useSetStepByStepTourShellMode } from '@/app/components/step-by-step-tour/storage'
import { getLangGeniusVersionInfo } from '@/context/app-context-normalizers'
import { useDocLink } from '@/context/i18n'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import {
currentWorkspaceIdAtom,
currentWorkspaceLoadingAtom,
isCurrentWorkspaceOwnerAtom,
} from '@/context/workspace-state'
import { env } from '@/env'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { consoleQuery } from '@/service/client'
import styles from './help-menu.module.css'
import AccountAboutDialog from './help-menu/account-about-dialog'
import SupportMenu from './support-menu'
@@ -86,8 +88,20 @@ const HelpMenu = ({ triggerIcon = defaultTriggerIcon, triggerClassName }: HelpMe
const { t } = useTranslation()
const docLink = useDocLink()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const { data: profileMeta } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.meta,
})
const { data: versionData } = useQuery(
consoleQuery.version.get.queryOptions({
input: profileMeta.currentVersion
? { query: { current_version: profileMeta.currentVersion } }
: skipToken,
enabled: !systemFeatures.branding.enabled,
}),
)
const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const langGeniusVersionInfo = getLangGeniusVersionInfo({ meta: profileMeta, versionData })
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
const learnDifyHidden = useLearnDifyHiddenValue()
@@ -1,6 +1,5 @@
import { DropdownMenuItem, DropdownMenuLinkItem } from '@langgenius/dify-ui/dropdown-menu'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useTranslation } from 'react-i18next'
import { openZendeskWindow } from '@/app/components/base/zendesk/utils'
import { Plan } from '@/app/components/billing/type'
@@ -12,7 +11,6 @@ import { mailToSupport } from '@/app/components/header/utils/util'
import { SUPPORT_EMAIL_ADDRESS, ZENDESK_WIDGET_KEY } from '@/config'
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'
@@ -23,11 +21,13 @@ export default function SupportMenu() {
select: ({ deployment_edition }) => deployment_edition,
})
const { enableBilling, plan } = useProviderContext()
const { data: userProfileEmail } = useSuspenseQuery({
const { data: accountProfile } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.profile.email,
select: (data) => ({
email: data.profile.email,
currentVersion: data.meta.currentVersion,
}),
})
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { setShowPricingModal } = useModalContext()
const hasDedicatedChannel = plan.type !== Plan.sandbox || Boolean(SUPPORT_EMAIL_ADDRESS.trim())
const shouldShowUpgradeContact =
@@ -82,9 +82,9 @@ export default function SupportMenu() {
<DropdownMenuLinkItem
className="mx-0 h-8 gap-1 px-3 py-1"
href={mailToSupport(
userProfileEmail,
accountProfile.email,
plan.type,
langGeniusVersionInfo?.current_version,
accountProfile.currentVersion ?? '',
SUPPORT_EMAIL_ADDRESS,
)}
rel="noopener noreferrer"
+6 -5
View File
@@ -10,11 +10,11 @@ import Badge from '@/app/components/base/badge'
import { DifyLogo } from '@/app/components/base/logo/dify-logo'
import EnvNav from '@/app/components/header/env-nav'
import StepByStepTourMount from '@/app/components/step-by-step-tour/mount'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import {
isCurrentWorkspaceDatasetOperatorAtom,
isCurrentWorkspaceEditorAtom,
} from '@/context/workspace-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag'
import { useCanManageAgents } from '@/features/agent-v2/permissions'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
@@ -33,15 +33,16 @@ const WebAppsSection = dynamic(() => import('./components/web-apps-section'), {
export function MainNav({ className }: MainNavProps) {
const { t } = useTranslation()
const pathname = usePathname()
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const isCurrentWorkspaceDatasetOperator = useAtomValue(isCurrentWorkspaceDatasetOperatorAtom)
const isCurrentWorkspaceEditor = useAtomValue(isCurrentWorkspaceEditorAtom)
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const { data: currentEnv } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.meta.currentEnv,
})
const agentV2Enabled = isAgentV2Enabled()
const canManageAgents = useCanManageAgents()
const showEnvTag =
langGeniusVersionInfo.current_env === 'TESTING' ||
langGeniusVersionInfo.current_env === 'DEVELOPMENT'
const showEnvTag = currentEnv === 'TESTING' || currentEnv === 'DEVELOPMENT'
const canUseAppDeploy = isCurrentWorkspaceEditor && systemFeatures.enable_app_deploy
const navItems = useMemo<MainNavItem[]>(
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
import { renderHook } from '@/test/console/render'
import useWorkspacePluginInstallPermission from '../use-workspace-plugin-install-permission'
@@ -7,30 +8,17 @@ let mockWorkspacePermissionKeys: string[] = []
vi.mock('@/context/permission-state', async () => {
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
return createPermissionStateModuleMock(() => ({
langGeniusVersionInfo: {
current_env: '',
current_version: '1.0.0',
latest_version: '',
release_notes: '',
version: '',
},
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => ({
langGeniusVersionInfo: {
current_env: '',
current_version: '1.0.0',
latest_version: '',
release_notes: '',
version: '',
},
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
})
const renderPermissionHook = () => {
const { wrapper } = createConsoleQueryWrapper({
accountProfileMeta: { currentVersion: '1.0.0' },
})
return renderHook(() => useWorkspacePluginInstallPermission(), { wrapper })
}
describe('useWorkspacePluginInstallPermission', () => {
beforeEach(() => {
mockWorkspacePermissionKeys = []
@@ -39,7 +27,7 @@ describe('useWorkspacePluginInstallPermission', () => {
it('should grant install and update capabilities with plugin.install', () => {
mockWorkspacePermissionKeys = ['plugin.install']
const { result } = renderHook(() => useWorkspacePluginInstallPermission())
const { result } = renderPermissionHook()
expect(result.current.canInstallPlugin).toBe(true)
expect(result.current.canUpdatePlugin).toBe(true)
@@ -49,7 +37,7 @@ describe('useWorkspacePluginInstallPermission', () => {
})
it('should not expose installed plugin list viewing as a permission capability', () => {
const { result } = renderHook(() => useWorkspacePluginInstallPermission())
const { result } = renderPermissionHook()
expect('canViewInstalledPlugins' in result.current).toBe(false)
})
@@ -57,7 +45,7 @@ describe('useWorkspacePluginInstallPermission', () => {
it('should grant delete capability but not install or update with plugin.delete', () => {
mockWorkspacePermissionKeys = ['plugin.delete']
const { result } = renderHook(() => useWorkspacePluginInstallPermission())
const { result } = renderPermissionHook()
expect(result.current.canInstallPlugin).toBe(false)
expect(result.current.canUpdatePlugin).toBe(false)
@@ -69,7 +57,7 @@ describe('useWorkspacePluginInstallPermission', () => {
it('should grant plugin debug capability with plugin.debug', () => {
mockWorkspacePermissionKeys = ['plugin.debug']
const { result } = renderHook(() => useWorkspacePluginInstallPermission())
const { result } = renderPermissionHook()
expect(result.current.canInstallPlugin).toBe(false)
expect(result.current.canUpdatePlugin).toBe(false)
@@ -81,7 +69,7 @@ describe('useWorkspacePluginInstallPermission', () => {
it('should grant plugin preference setting capability with plugin.plugin_preferences', () => {
mockWorkspacePermissionKeys = ['plugin.plugin_preferences']
const { result } = renderHook(() => useWorkspacePluginInstallPermission())
const { result } = renderPermissionHook()
expect(result.current.canInstallPlugin).toBe(false)
expect(result.current.canUpdatePlugin).toBe(false)
@@ -91,7 +79,7 @@ describe('useWorkspacePluginInstallPermission', () => {
})
it('should deny plugin capabilities without plugin install or manage permissions', () => {
const { result } = renderHook(() => useWorkspacePluginInstallPermission())
const { result } = renderPermissionHook()
expect(result.current.canInstallPlugin).toBe(false)
expect(result.current.canUpdatePlugin).toBe(false)
@@ -1,11 +1,15 @@
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useMemo } from 'react'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { hasPermission } from '@/utils/permission'
const useWorkspacePluginInstallPermission = () => {
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { data: currentVersion } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.meta.currentVersion ?? '',
})
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canInstallPlugin = useMemo(() => {
@@ -34,7 +38,7 @@ const useWorkspacePluginInstallPermission = () => {
canDeletePlugin,
canDebugPlugin,
canSetPluginPreferences,
currentDifyVersion: langGeniusVersionInfo?.current_version,
currentDifyVersion: currentVersion,
}
}
@@ -1,7 +1,9 @@
import type { ReactElement } from 'react'
import type { PluginDeclaration } from '../../../../types'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { render } from '@/test/console/render'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
import { render as renderWithConsoleState } from '@/test/console/render'
import { PluginCategoryEnum, TaskStatus } from '../../../../types'
import Install from '../install'
@@ -62,13 +64,14 @@ const mockConsoleState = vi.hoisted(() => ({
langGeniusVersionInfo: { current_version: '1.0.0' as string | undefined },
}))
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => ({
langGeniusVersionInfo: mockConsoleState.langGeniusVersionInfo,
}))
})
const render = (ui: ReactElement) => {
const { wrapper } = createConsoleQueryWrapper({
accountProfileMeta: {
currentVersion: mockConsoleState.langGeniusVersionInfo.current_version ?? null,
},
})
return renderWithConsoleState(ui, { wrapper })
}
vi.mock('../../../../card', () => ({
default: ({
@@ -3,12 +3,12 @@ import type { FC } from 'react'
import type { PluginDeclaration } from '../../../types'
import { Button } from '@langgenius/dify-ui/button'
import { RiLoader2Line } from '@remixicon/react'
import { useAtomValue } from 'jotai'
import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useEffect, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { uninstallPlugin } from '@/service/plugins'
import { useInstallPackageFromLocal, usePluginTaskList } from '@/service/use-plugins'
import { isEqualOrLaterThanVersion } from '@/utils/semver'
@@ -101,14 +101,14 @@ const Installed: FC<Props> = ({
}
}
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { data: currentVersion } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.meta.currentVersion ?? '',
})
const isDifyVersionCompatible = useMemo(() => {
if (!langGeniusVersionInfo.current_version) return true
return isEqualOrLaterThanVersion(
langGeniusVersionInfo.current_version,
payload.meta.minimum_dify_version ?? '0.0.0',
)
}, [langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version])
if (!currentVersion) return true
return isEqualOrLaterThanVersion(currentVersion, payload.meta.minimum_dify_version ?? '0.0.0')
}, [currentVersion, payload.meta.minimum_dify_version])
return (
<>
@@ -1,8 +1,10 @@
import type { ReactElement } from 'react'
import type { Plugin, PluginManifestInMarket } from '../../../../types'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { act } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { render } from '@/test/console/render'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
import { render as renderWithConsoleState } from '@/test/console/render'
import { PluginCategoryEnum, TaskStatus } from '../../../../types'
import Install from '../install'
@@ -78,13 +80,14 @@ vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', ()
}),
}))
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => ({
langGeniusVersionInfo: mockConsoleState.langGeniusVersionInfo,
}))
})
const render = (ui: ReactElement) => {
const { wrapper } = createConsoleQueryWrapper({
accountProfileMeta: {
currentVersion: mockConsoleState.langGeniusVersionInfo.current_version ?? null,
},
})
return renderWithConsoleState(ui, { wrapper })
}
// Mock service hooks
vi.mock('@/service/use-plugins', () => ({
@@ -3,12 +3,12 @@ import type { FC } from 'react'
import type { InstallPackageResponse, Plugin, PluginManifestInMarket } from '../../../types'
import { Button } from '@langgenius/dify-ui/button'
import { RiLoader2Line } from '@remixicon/react'
import { useAtomValue } from 'jotai'
import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import {
useInstallPackageFromMarketPlace,
usePluginDeclarationFromMarketPlace,
@@ -126,15 +126,18 @@ const Installed: FC<Props> = ({
}
}
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { data: currentVersion } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.meta.currentVersion ?? '',
})
const { data: pluginDeclaration } = usePluginDeclarationFromMarketPlace(uniqueIdentifier)
const isDifyVersionCompatible = useMemo(() => {
if (!pluginDeclaration || !langGeniusVersionInfo.current_version) return true
if (!pluginDeclaration || !currentVersion) return true
return isEqualOrLaterThanVersion(
langGeniusVersionInfo.current_version,
currentVersion,
pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0',
)
}, [langGeniusVersionInfo.current_version, pluginDeclaration])
}, [currentVersion, pluginDeclaration])
const { canInstall } = useInstallPluginLimit({
...payload,
@@ -8,11 +8,6 @@ import PluginItem from '../index'
const mockEnableMarketplace = vi.fn(() => true)
const render = (ui: ReactElement) =>
renderWithConsoleQuery(ui, {
systemFeatures: { enable_marketplace: mockEnableMarketplace() },
})
const mockTheme = vi.fn(() => 'light')
vi.mock('@/hooks/use-theme', () => ({
default: () => ({ theme: mockTheme() }),
@@ -70,12 +65,13 @@ const createLangGeniusVersionInfo = (currentVersion: string) => ({
version: '',
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => ({
langGeniusVersionInfo: mockLangGeniusVersionInfo(),
}))
})
const render = (ui: ReactElement) =>
renderWithConsoleQuery(ui, {
accountProfileMeta: {
currentVersion: mockLangGeniusVersionInfo().current_version,
},
systemFeatures: { enable_marketplace: mockEnableMarketplace() },
})
vi.mock('../action', () => ({
default: ({ onDelete, pluginName }: { onDelete: () => void; pluginName: string }) => (
@@ -11,13 +11,12 @@ import {
RiLoginCircleLine,
} from '@remixicon/react'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { API_PREFIX } from '@/config'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import useTheme from '@/hooks/use-theme'
@@ -81,15 +80,18 @@ const PluginItem: FC<Props> = ({
return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : ''
}, [source, author])
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { data: currentVersion } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.meta.currentVersion ?? '',
})
const isDifyVersionCompatible = useMemo(() => {
if (!langGeniusVersionInfo.current_version) return true
if (!currentVersion) return true
return isEqualOrLaterThanVersion(
langGeniusVersionInfo.current_version,
currentVersion,
declarationMeta.minimum_dify_version ?? '0.0.0',
)
}, [declarationMeta.minimum_dify_version, langGeniusVersionInfo.current_version])
}, [currentVersion, declarationMeta.minimum_dify_version])
const isDeprecated = useMemo(() => {
return status === 'deleted' && !!deprecated_reason
@@ -11,7 +11,7 @@ import {
usePluginAutoUpgradeSettings,
usePluginPermissionSettings,
} from '@/service/use-plugins'
import { renderHookWithConsoleQuery as renderHook } from '@/test/console/query-data'
import { renderHookWithConsoleQuery } from '@/test/console/query-data'
import { PermissionType, PluginCategoryEnum } from '../../types'
import useReferenceSetting, { useCanInstallPluginFromMarketplace } from '../use-reference-setting'
@@ -39,6 +39,18 @@ const setConsoleState = (state: ConsoleStateFixture) => {
}
}
function renderHook<Result, Props = void>(
callback: (props: Props) => Result,
options: Parameters<typeof renderHookWithConsoleQuery<Result, Props>>[1] = {},
) {
return renderHookWithConsoleQuery(callback, {
...options,
accountProfileMeta: {
currentVersion: mockConsoleState.langGeniusVersionInfo?.current_version ?? null,
},
})
}
vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => mockConsoleState)
@@ -47,11 +59,6 @@ vi.mock('@/context/permission-state', async () => {
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
return createPermissionStateModuleMock(() => mockConsoleState)
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => mockConsoleState)
})
vi.mock('@/service/use-plugins', () => ({
usePluginAutoUpgradeSettings: vi.fn(),
usePluginPermissionSettings: vi.fn(),
@@ -8,12 +8,12 @@ import {
workspacePermissionKeysAtom,
workspacePermissionKeysLoadingAtom,
} from '@/context/permission-state'
import { langGeniusVersionInfoAtom } from '@/context/version-state'
import {
currentWorkspaceLoadingAtom,
isCurrentWorkspaceManagerAtom,
isCurrentWorkspaceOwnerAtom,
} from '@/context/workspace-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import {
useInvalidateReferenceSettings,
@@ -49,10 +49,13 @@ export const usePluginSettingsAccess = () => {
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { data: rbacEnabled } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
select: (s) => s.rbac_enabled,
select: (data) => data.rbac_enabled,
})
const { data: currentVersion } = useSuspenseQuery({
...userProfileQueryOptions(),
select: (data) => data.meta.currentVersion ?? '',
})
const { canSetPermissions, canSetPluginPreferences } = useCanSetPluginSettings()
const permissionQuery = usePluginPermissionSettings()
@@ -95,7 +98,7 @@ export const usePluginSettingsAccess = () => {
canManagement: canInstallPlugin,
canDebugger: canDebugPlugin,
canSetPermissions,
currentDifyVersion: langGeniusVersionInfo?.current_version,
currentDifyVersion: currentVersion,
isPermissionLoading:
permissionQuery.isLoading ||
permissionQuery.isFetching ||
@@ -20,7 +20,6 @@ import {
workspacePermissionKeysAtom,
workspacePermissionKeysLoadingAtom,
} from '../permission-state'
import { langGeniusVersionInfoAtom } from '../version-state'
import {
currentWorkspaceAtom,
currentWorkspaceLoadingAtom,
@@ -207,7 +206,6 @@ function ConsoleBootstrapProbe() {
const datasetDefaultPermissionKeys = useAtomValue(datasetDefaultPermissionKeysAtom)
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const refreshPermissionsAfterMutationDenial = useSetAtom(
refreshWorkspacePermissionKeysAfterMutationDenialAtom,
)
@@ -254,11 +252,6 @@ function ConsoleBootstrapProbe() {
dataset operator:
{String(isCurrentWorkspaceDatasetOperator)}
</span>
<span>
version:
{langGeniusVersionInfo.current_version}/{langGeniusVersionInfo.latest_version}/
{langGeniusVersionInfo.current_env}
</span>
<button type="button" onClick={() => void refreshPermissionsAfterMutationDenial()}>
refresh permissions after denial
</button>
@@ -368,7 +361,7 @@ describe('Console bootstrap', () => {
})
describe('Bootstrap atoms', () => {
it('should provide profile, workspace, permissions, loading state, and version metadata', async () => {
it('should provide workspace, permissions, and loading state', async () => {
renderConsoleBootstrap()
expect(await screen.findByText('workspace:Workspace')).toBeInTheDocument()
@@ -376,10 +369,9 @@ describe('Console bootstrap', () => {
expect(screen.getByText('dataset keys:dataset.acl.edit')).toBeInTheDocument()
expect(screen.getByText('permission loading:false')).toBeInTheDocument()
expect(screen.getByText('workspace loading:false')).toBeInTheDocument()
expect(await screen.findByText('version:1.0.0/1.0.1/cloud')).toBeInTheDocument()
})
it('should fall back to placeholder values when workspace, permission, or version data is missing', async () => {
it('should fall back to placeholder values when workspace or permission data is missing', async () => {
mockCurrentWorkspaceQueryState.data = undefined
mockPermissionKeysState.datasetPermissionKeys = []
mockPermissionKeysState.permissionKeys = []
@@ -393,7 +385,6 @@ describe('Console bootstrap', () => {
expect(screen.getByText(`role:${initialWorkspaceSummary.role}`)).toBeInTheDocument()
expect(screen.getByText('keys:')).toBeInTheDocument()
expect(screen.getByText('dataset keys:')).toBeInTheDocument()
expect(screen.getByText('version://')).toBeInTheDocument()
})
it('should normalize invalid workspace roles to the initial workspace role', async () => {
-11
View File
@@ -1,11 +0,0 @@
'use client'
import { atom } from 'jotai'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { atomWithResolvedSuspenseQuery } from '@/utils/query-atoms'
const accountProfileQueryAtom = atomWithResolvedSuspenseQuery(() => userProfileQueryOptions())
export const accountProfileMetaAtom = atom((get) => {
return get(accountProfileQueryAtom).data.meta
})
-40
View File
@@ -1,40 +0,0 @@
'use client'
import { atom } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
import { brandingEnabledAtom } from '@/features/system-features/state'
import { consoleQuery } from '@/service/client'
import { accountProfileMetaAtom } from './account-state'
import { initialLangGeniusVersionInfo } from './app-context-defaults'
import { getLangGeniusVersionInfo } from './app-context-normalizers'
const versionQueryAtom = atomWithQuery((get) => {
const meta = get(accountProfileMetaAtom)
const brandingEnabled = get(brandingEnabledAtom)
const enabled = Boolean(meta.currentVersion && !brandingEnabled)
return consoleQuery.version.get.queryOptions({
input: {
query: {
current_version: meta.currentVersion ?? '',
},
},
enabled,
})
})
export const langGeniusVersionInfoAtom = atom((get) => {
const meta = get(accountProfileMetaAtom)
const versionData = get(versionQueryAtom).data
if (!versionData) return initialLangGeniusVersionInfo
return getLangGeniusVersionInfo({
meta,
versionData,
})
})
export const langGeniusCurrentVersionAtom = atom((get) => {
return get(langGeniusVersionInfoAtom).current_version
})
@@ -75,22 +75,6 @@ vi.mock('@/context/permission-state', async () => {
},
}))
})
vi.mock('@/context/version-state', async () => {
const { createVersionStateModuleMock } = await import('@/test/console/state-fixture')
return createVersionStateModuleMock(() => ({
userProfile: { id: 'user-1' },
currentWorkspace: { id: 'workspace-1' },
workspacePermissionKeys: ['app.acl.edit'],
langGeniusVersionInfo: {
current_env: 'PRODUCTION',
current_version: '',
latest_version: '',
version: '',
release_notes: '',
},
}))
})
vi.mock('@/service/client', () => ({
consoleQuery: {
account: {
-4
View File
@@ -13,7 +13,3 @@ const systemFeaturesAtom = atom((get) => {
export const deploymentEditionAtom = atom((get) => {
return get(systemFeaturesAtom).deployment_edition
})
export const brandingEnabledAtom = atom((get) => {
return get(systemFeaturesAtom).branding.enabled
})
+15 -6
View File
@@ -7,6 +7,8 @@ import { render } from '@testing-library/react'
import { createQueryClientWrapper } from '@/test/console/query-client'
import { createTestQueryClient } from '@/test/query-client'
type AccountProfileMeta = UserProfileWithMeta['meta']
const accountProfileQueryKey = [
['console', 'account', 'profile', 'get'],
{ type: 'query' },
@@ -27,19 +29,22 @@ const createAccountProfileFixture = (
const createMockUserProfileResponse = (
profile: Partial<GetAccountProfileResponse> = {},
meta: Partial<AccountProfileMeta> = {},
): UserProfileWithMeta => ({
profile: createAccountProfileFixture(profile),
meta: {
currentVersion: null,
currentEnv: null,
...meta,
},
})
export const seedAccountProfileQuery = (
queryClient: QueryClient,
profile: Partial<GetAccountProfileResponse> = {},
meta: Partial<AccountProfileMeta> = {},
) => {
const data = createMockUserProfileResponse(profile)
const data = createMockUserProfileResponse(profile, meta)
queryClient.setQueryData(accountProfileQueryKey, data)
return data
}
@@ -47,27 +52,30 @@ export const seedAccountProfileQuery = (
export const ensureAccountProfileQuery = (
queryClient: QueryClient,
profile: Partial<GetAccountProfileResponse> = {},
meta: Partial<AccountProfileMeta> = {},
) => {
const existingProfile = queryClient.getQueryData<UserProfileWithMeta>(accountProfileQueryKey)
if (existingProfile === undefined) return seedAccountProfileQuery(queryClient, profile)
if (existingProfile === undefined) return seedAccountProfileQuery(queryClient, profile, meta)
return existingProfile
}
export const createAccountProfileQueryClient = (
profile: Partial<GetAccountProfileResponse> = {},
meta: Partial<AccountProfileMeta> = {},
) => {
const queryClient = createTestQueryClient()
seedAccountProfileQuery(queryClient, profile)
seedAccountProfileQuery(queryClient, profile, meta)
return queryClient
}
export const createAccountProfileQueryWrapper = (
profile: Partial<GetAccountProfileResponse> = {},
meta: Partial<AccountProfileMeta> = {},
) => {
const queryClient = createAccountProfileQueryClient(profile)
const queryClient = createAccountProfileQueryClient(profile, meta)
return createQueryClientWrapper(queryClient)
}
@@ -75,11 +83,12 @@ export const renderWithAccountProfile = (
ui: ReactElement,
options: Omit<RenderOptions, 'wrapper'> & {
accountProfile?: Partial<GetAccountProfileResponse>
accountProfileMeta?: Partial<AccountProfileMeta>
} = {},
) => {
const { accountProfile, ...renderOptions } = options
const { accountProfile, accountProfileMeta, ...renderOptions } = options
return render(ui, {
...renderOptions,
wrapper: createAccountProfileQueryWrapper(accountProfile),
wrapper: createAccountProfileQueryWrapper(accountProfile, accountProfileMeta),
})
}
+9 -2
View File
@@ -16,6 +16,7 @@ import type {
RenderResult,
} from '@testing-library/react'
import type { ReactElement, ReactNode } from 'react'
import type { UserProfileWithMeta } from '@/features/account-profile/client'
import type { DeepPartial } from '@/test/console/system-features'
import { zGetFeaturesResponse } from '@dify/contracts/api/console/features/zod.gen'
import { render, renderHook } from '@testing-library/react'
@@ -170,6 +171,7 @@ export type ConsoleQueryTestOptions = {
*/
systemFeatures?: DeepPartial<GetSystemFeaturesResponse> | null
accountProfile?: Partial<GetAccountProfileResponse> | null
accountProfileMeta?: Partial<UserProfileWithMeta['meta']>
educationStatus?: Partial<EducationStatusResponse>
currentWorkspace?: Partial<GetWorkspacesCurrentSummaryResponse> | null
trialModels?: readonly string[] | null
@@ -193,8 +195,9 @@ export const createConsoleQueryWrapper = (
): ConsoleQueryWrapper => {
const queryClient = options.queryClient ?? createConsoleQueryClient()
if (options.accountProfile !== null) {
if (options.accountProfile) seedAccountProfileQuery(queryClient, options.accountProfile)
else ensureAccountProfileQuery(queryClient, { timezone: 'UTC' })
if (options.accountProfile)
seedAccountProfileQuery(queryClient, options.accountProfile, options.accountProfileMeta)
else ensureAccountProfileQuery(queryClient, { timezone: 'UTC' }, options.accountProfileMeta)
}
if (options.educationStatus) seedEducationStatus(queryClient, options.educationStatus)
if (options.currentWorkspace !== null) {
@@ -235,6 +238,7 @@ export const renderWithConsoleQuery = (
const {
systemFeatures: sf,
accountProfile,
accountProfileMeta,
educationStatus,
currentWorkspace,
trialModels,
@@ -246,6 +250,7 @@ export const renderWithConsoleQuery = (
const { wrapper, queryClient, systemFeatures } = createConsoleQueryWrapper({
systemFeatures: sf,
accountProfile,
accountProfileMeta,
educationStatus,
currentWorkspace,
trialModels,
@@ -267,6 +272,7 @@ export const renderHookWithConsoleQuery = <Result, Props = void>(
const {
systemFeatures: sf,
accountProfile,
accountProfileMeta,
educationStatus,
currentWorkspace,
trialModels,
@@ -278,6 +284,7 @@ export const renderHookWithConsoleQuery = <Result, Props = void>(
const { wrapper, queryClient, systemFeatures } = createConsoleQueryWrapper({
systemFeatures: sf,
accountProfile,
accountProfileMeta,
educationStatus,
currentWorkspace,
trialModels,
+1 -28
View File
@@ -1,6 +1,5 @@
import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { createStore } from 'jotai'
import type { LangGeniusVersionInfo } from '@/context/app-context-types'
import { atom } from 'jotai'
import { createSystemFeaturesFixture } from '@/test/console/system-features'
@@ -25,13 +24,12 @@ export type ConsoleStateFixture = {
knowledgeFsEnabled?: boolean
deploymentEdition?: 'COMMUNITY' | 'ENTERPRISE' | 'CLOUD'
brandingEnabled?: boolean
langGeniusVersionInfo?: Partial<LangGeniusVersionInfo>
refreshCurrentWorkspace?: () => void
}
type ConsoleStateFixtureResolver = () => ConsoleStateFixture
type JotaiStore = ReturnType<typeof createStore>
type ConsoleStateOwner = 'workspace' | 'permission' | 'systemFeatures' | 'version'
type ConsoleStateOwner = 'workspace' | 'permission' | 'systemFeatures'
const defaultCurrentWorkspace = {
id: 'workspace-1',
@@ -41,14 +39,6 @@ const defaultCurrentWorkspace = {
role: 'owner',
} satisfies GetWorkspacesCurrentSummaryResponse
const defaultLangGeniusVersionInfo = {
current_env: 'CLOUD',
current_version: '',
latest_version: '',
version: '',
release_notes: '',
} satisfies LangGeniusVersionInfo
const currentWorkspaceAtom = atom<GetWorkspacesCurrentSummaryResponse>(defaultCurrentWorkspace)
const currentWorkspaceIdAtom = atom((get) => get(currentWorkspaceAtom).id)
const isCurrentWorkspaceManagerAtom = atom(false)
@@ -68,9 +58,6 @@ const systemFeaturesAtom = atom(createSystemFeaturesFixture())
const deploymentEditionAtom = atom((get) => get(systemFeaturesAtom).deployment_edition)
const brandingEnabledAtom = atom((get) => get(systemFeaturesAtom).branding.enabled)
const langGeniusVersionInfoAtom = atom<LangGeniusVersionInfo>(defaultLangGeniusVersionInfo)
const langGeniusCurrentVersionAtom = atom((get) => get(langGeniusVersionInfoAtom).current_version)
const consoleStateFixtureResolvers: Partial<
Record<ConsoleStateOwner, ConsoleStateFixtureResolver>
> = {}
@@ -109,10 +96,6 @@ export const seedRegisteredConsoleStateFixture = (store: JotaiStore) => {
},
}),
)
store.set(langGeniusVersionInfoAtom, {
...defaultLangGeniusVersionInfo,
...state.langGeniusVersionInfo,
})
store.set(refreshCurrentWorkspaceCallbackAtom, {
callback: state.refreshCurrentWorkspace ?? (() => {}),
})
@@ -169,13 +152,3 @@ export const createSystemFeaturesStateModuleMock = (getState: ConsoleStateFixtur
brandingEnabledAtom,
}
}
export const createVersionStateModuleMock = (getState: ConsoleStateFixtureResolver) => {
registerConsoleStateFixture('version', () => ({
langGeniusVersionInfo: getState().langGeniusVersionInfo,
}))
return {
langGeniusVersionInfoAtom,
langGeniusCurrentVersionAtom,
}
}