From 81fd1da3ff9a0e8b959040ef23b7eab37d38892c Mon Sep 17 00:00:00 2001
From: yyh <92089059+lyzno1@users.noreply.github.com>
Date: Wed, 29 Jul 2026 20:33:26 +0800
Subject: [PATCH] fix: refactor account about dialog ownership (#39765)
---
.../account-about/__tests__/index.spec.tsx | 114 ----
.../components/header/account-about/index.tsx | 124 -----
.../account-dropdown/__tests__/index.spec.tsx | 507 +++---------------
.../account-dropdown/default-menu-content.tsx | 240 ---------
.../header/account-dropdown/index.tsx | 68 +--
.../main-nav/__tests__/index.spec.tsx | 59 ++
.../main-nav/components/account-section.tsx | 1 -
.../main-nav/components/help-menu.tsx | 18 +-
.../help-menu/account-about-dialog.tsx | 115 ++++
9 files changed, 280 insertions(+), 966 deletions(-)
delete mode 100644 web/app/components/header/account-about/__tests__/index.spec.tsx
delete mode 100644 web/app/components/header/account-about/index.tsx
delete mode 100644 web/app/components/header/account-dropdown/default-menu-content.tsx
create mode 100644 web/app/components/main-nav/components/help-menu/account-about-dialog.tsx
diff --git a/web/app/components/header/account-about/__tests__/index.spec.tsx b/web/app/components/header/account-about/__tests__/index.spec.tsx
deleted file mode 100644
index ff965d9a08c..00000000000
--- a/web/app/components/header/account-about/__tests__/index.spec.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import type { LangGeniusVersionInfo } from '@/context/app-context-types'
-import { fireEvent, screen } from '@testing-library/react'
-import { renderWithConsoleQuery } from '@/test/console/query-data'
-import AccountAbout from '../index'
-
-describe('AccountAbout', () => {
- const mockVersionInfo: LangGeniusVersionInfo = {
- current_version: '0.6.0',
- latest_version: '0.6.0',
- release_notes: 'https://github.com/langgenius/dify/releases/tag/0.6.0',
- version: '0.6.0',
- release_date: '2024-01-01',
- features: {
- can_replace_logo: false,
- model_load_balancing_enabled: false,
- },
- can_auto_update: false,
- current_env: 'production',
- }
-
- const mockOnCancel = vi.fn()
-
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- describe('Rendering', () => {
- it('should render correctly with version information', () => {
- renderWithConsoleQuery(
- ,
- {
- systemFeatures: { deployment_edition: 'CLOUD', branding: { enabled: false } },
- },
- )
-
- expect(screen.getByText(/^Version/)).toBeInTheDocument()
- expect(screen.getAllByText(/0.6.0/).length).toBeGreaterThan(0)
- })
-
- it('should render branding logo if enabled', () => {
- renderWithConsoleQuery(
- ,
- {
- systemFeatures: {
- deployment_edition: 'CLOUD',
- branding: { enabled: true, workspace_logo: 'custom-logo.png' },
- },
- },
- )
-
- const img = screen.getByAltText('logo')
- expect(img).toBeInTheDocument()
- expect(img).toHaveAttribute('src', 'custom-logo.png')
- })
- })
-
- describe('Version Logic', () => {
- it('should show "Latest Available" when current version equals latest', () => {
- renderWithConsoleQuery(
- ,
- { systemFeatures: { deployment_edition: 'CLOUD' } },
- )
-
- expect(screen.getByText(/about.latestAvailable/)).toBeInTheDocument()
- })
-
- it('should show "Now Available" when current version is behind', () => {
- const behindVersionInfo = { ...mockVersionInfo, latest_version: '0.7.0' }
-
- renderWithConsoleQuery(
- ,
- { systemFeatures: { deployment_edition: 'CLOUD' } },
- )
-
- expect(screen.getByText(/about.nowAvailable/)).toBeInTheDocument()
- expect(screen.getByText(/about.updateNow/)).toBeInTheDocument()
- })
- })
-
- describe('Community Edition', () => {
- it('should render correctly in Community Edition', () => {
- renderWithConsoleQuery(
- ,
- { systemFeatures: { deployment_edition: 'COMMUNITY' } },
- )
-
- expect(screen.getByText(/Open Source License/)).toBeInTheDocument()
- })
-
- it('should hide update button in Community Edition when behind version', () => {
- const behindVersionInfo = { ...mockVersionInfo, latest_version: '0.7.0' }
-
- renderWithConsoleQuery(
- ,
- { systemFeatures: { deployment_edition: 'COMMUNITY' } },
- )
-
- expect(screen.queryByText(/about.updateNow/)).not.toBeInTheDocument()
- })
- })
-
- describe('User Interactions', () => {
- it('should call onCancel when close button is clicked', () => {
- renderWithConsoleQuery(
- ,
- { systemFeatures: { deployment_edition: 'CLOUD' } },
- )
-
- fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
-
- expect(mockOnCancel).toHaveBeenCalled()
- })
- })
-})
diff --git a/web/app/components/header/account-about/index.tsx b/web/app/components/header/account-about/index.tsx
deleted file mode 100644
index 51a922c15d5..00000000000
--- a/web/app/components/header/account-about/index.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-'use client'
-import type { LangGeniusVersionInfo } from '@/context/app-context-types'
-import { Button } from '@langgenius/dify-ui/button'
-import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
-import { RiCloseLine } from '@remixicon/react'
-import { useSuspenseQuery } from '@tanstack/react-query'
-import dayjs from 'dayjs'
-import { useTranslation } from 'react-i18next'
-import { DifyLogo } from '@/app/components/base/logo/dify-logo'
-import { systemFeaturesQueryOptions } from '@/features/system-features/client'
-import Link from '@/next/link'
-
-type IAccountSettingProps = {
- langGeniusVersionInfo: LangGeniusVersionInfo
- onCancel: () => void
-}
-
-export default function AccountAbout({ langGeniusVersionInfo, onCancel }: IAccountSettingProps) {
- const { t } = useTranslation()
- const isLatest = langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version
- const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
- const isNonCloudEdition =
- systemFeatures.deployment_edition === 'COMMUNITY' ||
- systemFeatures.deployment_edition === 'ENTERPRISE'
-
- return (
-
- )
-}
diff --git a/web/app/components/header/account-dropdown/__tests__/index.spec.tsx b/web/app/components/header/account-dropdown/__tests__/index.spec.tsx
index ae45a322562..2ee71387918 100644
--- a/web/app/components/header/account-dropdown/__tests__/index.spec.tsx
+++ b/web/app/components/header/account-dropdown/__tests__/index.spec.tsx
@@ -1,92 +1,30 @@
-import type { GetSystemFeaturesResponse } from '@dify/contracts/api/console/system-features/types.gen'
-import type { ModalContextState } from '@/context/modal-context'
import type { ProviderContextState } from '@/context/provider-context'
-import type { ConsoleStateFixture } from '@/test/console/state-fixture'
-import type { DeepPartial } from '@/test/console/system-features'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { renderToString } from 'react-dom/server'
-import { Plan } from '@/app/components/billing/type'
+import { resetUser } from '@/app/components/base/amplitude/utils'
import AccountSection from '@/app/components/main-nav/components/account-section'
-import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { useLogout } from '@/service/use-common'
import { createAccountProfileQueryClient } from '@/test/console/account-profile'
import { renderWithConsoleQuery } from '@/test/console/query-data'
-import AppSelector from '../index'
+import AccountDropdown from '../index'
-vi.mock('../../account-setting', () => ({
- default: () =>
AccountSetting
,
+const { mockPush, mockResetUser, mockSetSettingsDestination, mockUseRouter } = vi.hoisted(() => ({
+ mockPush: vi.fn(),
+ mockResetUser: vi.fn(),
+ mockSetSettingsDestination: vi.fn(),
+ mockUseRouter: vi.fn(),
}))
-vi.mock('../../account-about', () => ({
- default: ({ onCancel }: { onCancel: () => void }) => (
-
- Version
-
-
- ),
+vi.mock('@/app/components/base/amplitude/utils', () => ({
+ resetUser: mockResetUser,
}))
-vi.mock('@/app/components/header/github-star', () => ({
- default: () => GithubStar
,
-}))
-
-vi.mock('@/app/components/base/theme-switcher', () => ({
- default: () => (
-
- ),
-}))
-
-const { mockSetTheme } = vi.hoisted(() => ({
- mockSetTheme: vi.fn(),
-}))
-const mockConsoleState = vi.hoisted(() => ({
- current: undefined as ConsoleStateFixture | undefined,
-}))
-const mockConsoleStateReader = vi.hoisted(() => vi.fn())
-const mockUseRouter = vi.hoisted(() => vi.fn())
-
-vi.mock('next-themes', () => ({
- useTheme: () => ({
- theme: 'system',
- setTheme: mockSetTheme,
- }),
-}))
-
-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 ?? {})
-})
-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(),
}))
-vi.mock('@/context/modal-context', () => ({
- useModalContext: vi.fn(),
-}))
-
-const mockSetSettingsDestination = vi.fn()
-vi.mock('nuqs', async (importOriginal) => {
- const actual = await importOriginal()
- return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
-})
-
vi.mock('@/service/use-common', async (importOriginal) => ({
...(await importOriginal()),
useLogout: vi.fn(),
@@ -100,396 +38,119 @@ vi.mock('@/next/navigation', async (importOriginal) => {
}
})
-// Mock config and env
-const { mockConfig, mockEnv } = vi.hoisted(() => ({
- mockConfig: {
- AMPLITUDE_API_KEY: '',
- ZENDESK_WIDGET_KEY: '',
- SUPPORT_EMAIL_ADDRESS: '',
- },
- mockEnv: {
- env: {
- NEXT_PUBLIC_SITE_ABOUT: 'show',
- },
- },
-}))
-vi.mock('@/config', async (importOriginal) => {
- const actual = await importOriginal()
+vi.mock('nuqs', async (importOriginal) => {
+ const actual = await importOriginal()
return {
...actual,
- get AMPLITUDE_API_KEY() {
- return mockConfig.AMPLITUDE_API_KEY
- },
- get ZENDESK_WIDGET_KEY() {
- return mockConfig.ZENDESK_WIDGET_KEY
- },
- get SUPPORT_EMAIL_ADDRESS() {
- return mockConfig.SUPPORT_EMAIL_ADDRESS
- },
- IS_DEV: false,
+ useQueryState: () => [null, mockSetSettingsDestination],
}
})
-vi.mock('@/env', () => mockEnv)
-const baseUserProfile = {
- id: '1',
- name: 'Test User',
- email: 'test@example.com',
- avatar: '',
- avatar_url: 'avatar.png',
- is_password_set: false,
+vi.mock('next-themes', () => ({
+ useTheme: () => ({ theme: 'system', setTheme: vi.fn() }),
+}))
+
+const userProfile = {
+ id: 'current-user',
+ name: 'Current User',
+ email: 'current@example.com',
+ avatar_url: 'current-avatar.png',
}
-const baseConsoleState: ConsoleStateFixture = {
- userProfile: baseUserProfile,
- refreshUserProfile: vi.fn(),
- currentWorkspace: {
- id: '1',
- name: 'Workspace',
- plan: '',
- status: '',
- created_at: 0,
- role: 'owner',
- providers: [],
- trial_credits: 0,
- trial_credits_used: 0,
- next_credit_reset_date: 0,
- },
- isCurrentWorkspaceManager: true,
- isCurrentWorkspaceOwner: true,
- isCurrentWorkspaceEditor: true,
- isCurrentWorkspaceDatasetOperator: false,
- refreshCurrentWorkspace: vi.fn(),
- langGeniusVersionInfo: {
- current_env: 'testing',
- current_version: '0.6.0',
- latest_version: '0.6.0',
- release_date: '',
- release_notes: '',
- version: '0.6.0',
- can_auto_update: false,
- },
- isLoadingCurrentWorkspace: false,
- workspacePermissionKeys: [],
-}
+const renderAccountDropdown = () => {
+ const queryClient = createAccountProfileQueryClient(userProfile)
-const setConsoleState = (value: ConsoleStateFixture) => {
- mockConsoleState.current = value
- mockConsoleStateReader.mockReturnValue(value)
+ return renderWithConsoleQuery(
+ (
+
+ )}
+ />,
+ { queryClient },
+ )
}
describe('AccountDropdown', () => {
- const mockPush = vi.fn()
const mockLogout = vi.fn()
- let deploymentEdition: GetSystemFeaturesResponse['deployment_edition'] = 'COMMUNITY'
-
- const renderWithRouter = (
- ui: React.ReactElement,
- options: { systemFeatures?: DeepPartial } = {},
- ) => {
- const queryClient = createAccountProfileQueryClient({
- ...baseUserProfile,
- ...(mockConsoleState.current?.userProfile ?? {}),
- })
- return renderWithConsoleQuery(ui, {
- queryClient,
- systemFeatures: options.systemFeatures ?? {
- deployment_edition: deploymentEdition,
- branding: { enabled: false },
- },
- })
- }
beforeEach(() => {
vi.clearAllMocks()
- vi.stubGlobal('localStorage', { removeItem: vi.fn() })
- deploymentEdition = 'COMMUNITY'
- mockEnv.env.NEXT_PUBLIC_SITE_ABOUT = 'show'
-
- setConsoleState(baseConsoleState)
+ mockUseRouter.mockReturnValue({ push: mockPush })
vi.mocked(useProviderContext).mockReturnValue({
isEducationAccount: false,
- plan: { type: Plan.sandbox },
- } as unknown as ProviderContextState)
- vi.mocked(useModalContext).mockReturnValue({} as unknown as ModalContextState)
+ } as ProviderContextState)
vi.mocked(useLogout).mockReturnValue({
mutateAsync: mockLogout,
} as unknown as ReturnType)
- mockUseRouter.mockReturnValue({
- push: mockPush,
- })
})
- afterEach(() => {
- vi.unstubAllGlobals()
+ it('reads the signed-in account from the account profile query', async () => {
+ const user = userEvent.setup()
+ const queryClient = createAccountProfileQueryClient(userProfile)
+
+ renderWithConsoleQuery(, { queryClient })
+
+ expect(screen.getByText('Current User')).toBeInTheDocument()
+
+ await user.click(screen.getByRole('button', { name: 'common.account.account' }))
+
+ expect(await screen.findByText('current@example.com')).toBeInTheDocument()
})
- describe('Rendering', () => {
- it('should show the signed-in account in the main navigation menu', async () => {
- const user = userEvent.setup()
- const queryClient = createAccountProfileQueryClient({
- id: 'current-user',
- name: 'Current User',
- email: 'current@example.com',
- avatar_url: 'current-avatar.png',
- })
+ it('keeps the composed trigger disabled in server-rendered markup', () => {
+ const html = renderToString(
+ (
+
+ )}
+ />,
+ )
+ const container = document.createElement('div')
+ container.innerHTML = html
- renderWithConsoleQuery(, {
- queryClient,
- systemFeatures: { branding: { enabled: false } },
- })
-
- expect(screen.getByText('Current User')).toBeInTheDocument()
- expect(screen.queryByText('Test User')).not.toBeInTheDocument()
-
- await user.click(screen.getByRole('button', { name: 'common.account.account' }))
-
- expect(await screen.findByText('current@example.com')).toBeInTheDocument()
- expect(screen.queryByText('test@example.com')).not.toBeInTheDocument()
- })
-
- it('should render user profile correctly', () => {
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
-
- // Assert
- expect(screen.getByText('Test User')).toBeInTheDocument()
- expect(screen.getByText('test@example.com')).toBeInTheDocument()
- })
-
- it('should set an accessible label on avatar trigger when menu trigger is rendered', () => {
- // Act
- renderWithRouter()
-
- // Assert
- expect(screen.getByRole('button', { name: 'common.account.account' })).toBeInTheDocument()
- })
-
- it('should keep the account trigger disabled in server-rendered markup', () => {
- const container = document.createElement('div')
- container.innerHTML = renderToString()
-
- expect(container.querySelector('button[aria-label="common.account.account"]')).toBeDisabled()
- })
-
- it('should show EDU badge for education accounts', () => {
- // Arrange
- vi.mocked(useProviderContext).mockReturnValue({
- isEducationAccount: true,
- plan: { type: Plan.sandbox },
- } as unknown as ProviderContextState)
-
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
-
- // Assert
- expect(screen.getByText('EDU')).toBeInTheDocument()
- })
+ expect(container.querySelector('button[aria-label="common.account.account"]')).toBeDisabled()
})
- describe('Settings and Support', () => {
- it('should open member settings when settings is clicked', () => {
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
- fireEvent.click(screen.getByText('common.userProfile.settings'))
+ it('opens the main navigation account menu through the composed trigger', async () => {
+ const user = userEvent.setup()
+ renderAccountDropdown()
- // Assert
- expect(mockSetSettingsDestination).toHaveBeenCalledWith('members')
- })
+ const trigger = screen.getByRole('button', { name: 'common.account.account' })
+ expect(trigger).toHaveAttribute('data-open', 'false')
- it('should open preferences from the account dropdown', () => {
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
- fireEvent.click(screen.getByText('common.settings.preferences'))
+ await user.click(trigger)
- // Assert
- expect(mockSetSettingsDestination).toHaveBeenCalledWith('preferences')
- })
-
- it('should show Appearance after Preferences in the main nav account dropdown', () => {
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
-
- const preferences = screen.getByText('common.settings.preferences')
- const appearance = screen.getByText('common.account.appearanceLabel')
-
- // Assert
- expect(preferences.compareDocumentPosition(appearance)).toBe(Node.DOCUMENT_POSITION_FOLLOWING)
- expect(
- screen.getByRole('menuitem', { name: 'common.account.appearanceLabel' }),
- ).toBeInTheDocument()
- })
-
- it('should show Compliance in Cloud Edition for workspace owner', () => {
- // Arrange
- deploymentEdition = 'CLOUD'
- setConsoleState({
- ...baseConsoleState,
- userProfile: { ...baseConsoleState.userProfile, name: 'User' },
- isCurrentWorkspaceOwner: true,
- langGeniusVersionInfo: {
- ...baseConsoleState.langGeniusVersionInfo,
- current_version: '0.6.0',
- latest_version: '0.6.0',
- },
- })
-
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
-
- // Assert
- expect(screen.getByText('common.userProfile.compliance')).toBeInTheDocument()
- })
-
- it('should hide Compliance in Cloud Edition when user is not workspace owner', () => {
- // Arrange
- deploymentEdition = 'CLOUD'
- setConsoleState({
- ...baseConsoleState,
- isCurrentWorkspaceOwner: false,
- })
-
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
-
- // Assert
- expect(screen.queryByText('common.userProfile.compliance')).not.toBeInTheDocument()
- })
+ expect(await screen.findByText('current@example.com')).toBeInTheDocument()
+ expect(trigger).toHaveAttribute('data-open', 'true')
+ expect(screen.getByText('common.settings.preferences')).toBeInTheDocument()
+ expect(screen.getByText('common.account.appearanceLabel')).toBeInTheDocument()
})
- describe('Actions', () => {
- it('should handle logout correctly', async () => {
- // Arrange
- mockLogout.mockResolvedValue({})
+ it('opens preferences from the account menu', async () => {
+ const user = userEvent.setup()
+ renderAccountDropdown()
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
- fireEvent.click(screen.getByText('common.userProfile.logout'))
+ await user.click(screen.getByRole('button', { name: 'common.account.account' }))
+ await user.click(await screen.findByText('common.settings.preferences'))
- // Assert
- await waitFor(() => {
- expect(mockLogout).toHaveBeenCalled()
- expect(mockPush).toHaveBeenCalledWith('/signin')
- })
- })
-
- it('should use the shutdown icon for main nav logout', () => {
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
-
- // Assert
- expect(
- screen
- .getByRole('menuitem', { name: 'common.userProfile.logout' })
- .querySelector('.i-ri-shut-down-line'),
- ).toBeInTheDocument()
- })
-
- it('should show About section when about button is clicked and can close it', () => {
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
- fireEvent.click(screen.getByText('common.userProfile.about'))
-
- // Assert
- expect(screen.getByTestId('account-about')).toBeInTheDocument()
-
- // Act
- fireEvent.click(screen.getByText('Close'))
-
- // Assert
- expect(screen.queryByTestId('account-about')).not.toBeInTheDocument()
- })
-
- it('should keep account dropdown open when clicking the theme switcher', () => {
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button', { name: 'common.account.account' }))
- fireEvent.click(screen.getByTestId('theme-switcher-button'))
-
- // Assert
- expect(screen.getByText('common.userProfile.logout')).toBeInTheDocument()
- })
+ expect(mockSetSettingsDestination).toHaveBeenCalledWith('preferences')
})
- describe('Branding and Environment', () => {
- it('should hide sections when branding is enabled', () => {
- // Act
- renderWithRouter(, {
- systemFeatures: { branding: { enabled: true } },
- })
- fireEvent.click(screen.getByRole('button'))
+ it('logs out and redirects to sign in', async () => {
+ mockLogout.mockResolvedValue({})
+ renderAccountDropdown()
- // Assert
- expect(screen.queryByText('common.userProfile.helpCenter')).not.toBeInTheDocument()
- expect(screen.queryByText('common.userProfile.roadmap')).not.toBeInTheDocument()
- })
+ fireEvent.click(screen.getByRole('button', { name: 'common.account.account' }))
+ fireEvent.click(await screen.findByText('common.userProfile.logout'))
- it('should hide About section when NEXT_PUBLIC_SITE_ABOUT is hide', () => {
- // Arrange
- mockEnv.env.NEXT_PUBLIC_SITE_ABOUT = 'hide'
-
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
-
- // Assert
- expect(screen.queryByText('common.userProfile.about')).not.toBeInTheDocument()
- })
- })
-
- describe('Version Indicators', () => {
- it('should show orange indicator when version is not latest', () => {
- // Arrange
- setConsoleState({
- ...baseConsoleState,
- userProfile: { ...baseConsoleState.userProfile, name: 'User' },
- langGeniusVersionInfo: {
- ...baseConsoleState.langGeniusVersionInfo,
- current_version: '0.6.0',
- latest_version: '0.7.0',
- },
- })
-
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
-
- // Assert
- expect(
- document.querySelector('.bg-components-badge-status-light-warning-bg'),
- ).toBeInTheDocument()
- })
-
- it('should show green indicator when version is latest', () => {
- // Arrange
- setConsoleState({
- ...baseConsoleState,
- userProfile: { ...baseConsoleState.userProfile, name: 'User' },
- langGeniusVersionInfo: {
- ...baseConsoleState.langGeniusVersionInfo,
- current_version: '0.7.0',
- latest_version: '0.7.0',
- },
- })
-
- // Act
- renderWithRouter()
- fireEvent.click(screen.getByRole('button'))
-
- // Assert
- expect(
- document.querySelector('.bg-components-badge-status-light-success-bg'),
- ).toBeInTheDocument()
+ await waitFor(() => {
+ expect(mockLogout).toHaveBeenCalledOnce()
+ expect(resetUser).toHaveBeenCalledOnce()
+ expect(mockPush).toHaveBeenCalledWith('/signin')
})
})
})
diff --git a/web/app/components/header/account-dropdown/default-menu-content.tsx b/web/app/components/header/account-dropdown/default-menu-content.tsx
deleted file mode 100644
index 10bd82acfa1..00000000000
--- a/web/app/components/header/account-dropdown/default-menu-content.tsx
+++ /dev/null
@@ -1,240 +0,0 @@
-'use client'
-
-import type { MouseEventHandler, ReactNode } from 'react'
-import { Avatar } from '@langgenius/dify-ui/avatar'
-import {
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLinkItem,
- DropdownMenuSeparator,
-} from '@langgenius/dify-ui/dropdown-menu'
-import { StatusDot } from '@langgenius/dify-ui/status-dot'
-import { useSuspenseQuery } from '@tanstack/react-query'
-import { useAtomValue } from 'jotai'
-import { useQueryState } from 'nuqs'
-import { useTranslation } from 'react-i18next'
-import PremiumBadge from '@/app/components/base/premium-badge'
-import ThemeSwitcher from '@/app/components/base/theme-switcher'
-import {
- settingsQueryParamName,
- settingsQueryParser,
-} from '@/app/components/header/account-setting/query-params'
-import { userProfileAtom } from '@/context/account-state'
-import { useDocLink } from '@/context/i18n'
-import { useProviderContext } from '@/context/provider-context'
-import { langGeniusVersionInfoAtom } from '@/context/version-state'
-import { isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state'
-import { env } from '@/env'
-import { systemFeaturesQueryOptions } from '@/features/system-features/client'
-import Link from '@/next/link'
-import GithubStar from '../github-star'
-import Compliance from './compliance'
-import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content'
-
-type AccountMenuRouteItemProps = {
- href: string
- iconClassName: string
- label: ReactNode
- trailing?: ReactNode
-}
-
-function AccountMenuRouteItem({ href, iconClassName, label, trailing }: AccountMenuRouteItemProps) {
- return (
- }>
-
-
- )
-}
-
-type AccountMenuExternalItemProps = {
- href: string
- iconClassName: string
- label: ReactNode
- trailing?: ReactNode
-}
-
-function AccountMenuExternalItem({
- href,
- iconClassName,
- label,
- trailing,
-}: AccountMenuExternalItemProps) {
- return (
-
-
-
- )
-}
-
-type AccountMenuActionItemProps = {
- iconClassName: string
- label: ReactNode
- onClick?: MouseEventHandler
- trailing?: ReactNode
-}
-
-function AccountMenuActionItem({
- iconClassName,
- label,
- onClick,
- trailing,
-}: AccountMenuActionItemProps) {
- return (
-
-
-
- )
-}
-
-type AccountMenuSectionProps = {
- children: ReactNode
-}
-
-function AccountMenuSection({ children }: AccountMenuSectionProps) {
- return {children}
-}
-
-type DefaultMenuContentProps = {
- closeAccountDropdown: () => void
- onShowAbout: () => void
- onLogout: () => Promise
-}
-
-export function DefaultMenuContent({
- closeAccountDropdown,
- onShowAbout,
- onLogout,
-}: DefaultMenuContentProps) {
- const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
- const { t } = useTranslation()
- const docLink = useDocLink()
- const userProfile = useAtomValue(userProfileAtom)
- const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
- const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
- const { isEducationAccount } = useProviderContext()
- const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
-
- return (
- <>
-
-
-
-
- {userProfile.name}
- {isEducationAccount && (
-
-
- EDU
-
- )}
-
-
- {userProfile.email}
-
-
-
-
- $['account.account'], { ns: 'common' })}
- trailing={}
- />
- $['userProfile.settings'], { ns: 'common' })}
- onClick={() => setSettingsDestination('members')}
- />
-
-
- {!systemFeatures.branding.enabled && (
- <>
-
- $['userProfile.helpCenter'], { ns: 'common' })}
- trailing={}
- />
- {systemFeatures.deployment_edition === 'CLOUD' && isCurrentWorkspaceOwner && (
-
- )}
-
-
-
- $['userProfile.roadmap'], { ns: 'common' })}
- trailing={}
- />
- $['userProfile.github'], { ns: 'common' })}
- trailing={
-
-
-
-
- }
- />
- {env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
- $['userProfile.about'], { ns: 'common' })}
- onClick={() => {
- onShowAbout()
- closeAccountDropdown()
- }}
- trailing={
-
-
- {langGeniusVersionInfo.current_version}
-
-
-
- }
- />
- )}
-
-
- >
- )}
-
-
- $['theme.theme'], { ns: 'common' })}
- trailing={}
- />
-
-
-
-
- $['userProfile.logout'], { ns: 'common' })}
- onClick={() => {
- void onLogout()
- }}
- />
-
- >
- )
-}
diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx
index c98e696ef06..cc887ab8025 100644
--- a/web/app/components/header/account-dropdown/index.tsx
+++ b/web/app/components/header/account-dropdown/index.tsx
@@ -1,28 +1,20 @@
'use client'
import type { ReactElement } from 'react'
-import { Avatar } from '@langgenius/dify-ui/avatar'
-import { cn } from '@langgenius/dify-ui/cn'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
-import { useAtomValue } from 'jotai'
import { useState, useSyncExternalStore } from 'react'
import { useTranslation } from 'react-i18next'
import { resetUser } from '@/app/components/base/amplitude/utils'
-import { userProfileAtom } from '@/context/account-state'
-import { langGeniusVersionInfoAtom } from '@/context/version-state'
import { useRouter } from '@/next/navigation'
import { useLogout } from '@/service/use-common'
-import AccountAbout from '../account-about'
-import { DefaultMenuContent } from './default-menu-content'
import { MainNavMenuContent } from './main-nav-menu-content'
type AccountDropdownProps = {
- trigger?: (props: { isOpen: boolean; ariaLabel: string }) => ReactElement
- variant?: 'default' | 'mainNav'
+ trigger: (props: { isOpen: boolean; ariaLabel: string }) => ReactElement
}
const mainNavMenuPopupClassName =
@@ -32,9 +24,8 @@ const subscribeHydrationState = () => () => {}
const getHydrationSnapshot = () => false
const getServerHydrationSnapshot = () => true
-export default function AppSelector({ trigger, variant = 'default' }: AccountDropdownProps = {}) {
+export default function AccountDropdown({ trigger }: AccountDropdownProps) {
const router = useRouter()
- const [aboutVisible, setAboutVisible] = useState(false)
const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false)
const isHydrating = useSyncExternalStore(
subscribeHydrationState,
@@ -42,8 +33,6 @@ export default function AppSelector({ trigger, variant = 'default' }: AccountDro
getServerHydrationSnapshot,
)
const { t } = useTranslation()
- const userProfile = useAtomValue(userProfileAtom)
- const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { mutateAsync: logout } = useLogout()
@@ -58,53 +47,22 @@ export default function AppSelector({ trigger, variant = 'default' }: AccountDro
return (
- {trigger ? (
- $['account.account'], { ns: 'common' }),
- })}
- />
- ) : (
- $['account.account'], { ns: 'common' })}
- className={cn(
- 'inline-flex items-center rounded-[20px] p-0.5 hover:bg-background-default-dodge disabled:cursor-default disabled:hover:bg-transparent',
- isAccountMenuOpen && 'bg-background-default-dodge',
- )}
- >
-
-
- )}
+ $['account.account'], { ns: 'common' }),
+ })}
+ />
- {variant === 'mainNav' ? (
-
- ) : (
- setIsAccountMenuOpen(false)}
- onShowAbout={() => setAboutVisible(true)}
- onLogout={handleLogout}
- />
- )}
+
- {aboutVisible && (
-
setAboutVisible(false)}
- langGeniusVersionInfo={langGeniusVersionInfo}
- />
- )}
)
}
diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx
index d3cac90f78c..c0acc04429f 100644
--- a/web/app/components/main-nav/__tests__/index.spec.tsx
+++ b/web/app/components/main-nav/__tests__/index.spec.tsx
@@ -1139,6 +1139,65 @@ describe('MainNav', () => {
})
})
+ it('opens About from its real Help menu owner and restores focus when closed', async () => {
+ const user = userEvent.setup()
+ mockConsoleState.current = {
+ ...consoleState,
+ langGeniusVersionInfo: {
+ ...consoleState.langGeniusVersionInfo,
+ latest_version: '1.1.0',
+ release_notes: 'https://github.com/langgenius/dify/releases/tag/1.1.0',
+ },
+ }
+ renderMainNav()
+
+ const helpButton = screen.getByRole('button', { name: 'common.mainNav.help.openMenu' })
+ await user.click(helpButton)
+ await user.click(await screen.findByRole('menuitem', { name: /common\.userProfile\.about/ }))
+
+ expect(
+ await screen.findByRole('dialog', { name: 'common.userProfile.about' }),
+ ).toBeInTheDocument()
+ expect(screen.queryByRole('menu')).not.toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'common.operation.close' })).toHaveFocus()
+ expect(screen.getByRole('link', { name: 'Privacy Policy' })).toHaveAttribute(
+ 'href',
+ 'https://dify.ai/privacy',
+ )
+ expect(screen.getByRole('link', { name: 'common.about.changeLog' })).toHaveAttribute(
+ 'href',
+ 'https://github.com/langgenius/dify/releases',
+ )
+ expect(screen.getByRole('link', { name: 'common.about.updateNow' })).toHaveAttribute(
+ 'href',
+ 'https://github.com/langgenius/dify/releases/tag/1.1.0',
+ )
+ expect(screen.queryByRole('button', { name: 'common.about.changeLog' })).not.toBeInTheDocument()
+
+ await user.keyboard('{Escape}')
+
+ await waitFor(() => {
+ expect(
+ screen.queryByRole('dialog', { name: 'common.userProfile.about' }),
+ ).not.toBeInTheDocument()
+ expect(helpButton).toHaveFocus()
+ })
+ })
+
+ it('shows the open-source license in About for non-Cloud editions', async () => {
+ const user = userEvent.setup()
+ renderMainNav({ deployment_edition: 'COMMUNITY' })
+
+ await user.click(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' }))
+ await user.click(await screen.findByRole('menuitem', { name: /common\.userProfile\.about/ }))
+
+ expect(await screen.findByRole('link', { name: 'Open Source License' })).toHaveAttribute(
+ 'href',
+ 'https://github.com/langgenius/dify/blob/main/LICENSE',
+ )
+ expect(screen.queryByRole('link', { name: 'Privacy Policy' })).not.toBeInTheDocument()
+ })
+
it('closes the help menu from the support upgrade action', async () => {
renderMainNav()
diff --git a/web/app/components/main-nav/components/account-section.tsx b/web/app/components/main-nav/components/account-section.tsx
index b6932d81269..d818f2ca903 100644
--- a/web/app/components/main-nav/components/account-section.tsx
+++ b/web/app/components/main-nav/components/account-section.tsx
@@ -18,7 +18,6 @@ const AccountSection = ({ compact = false }: AccountSectionProps) => {
return (
(