diff --git a/web/app/components/base/__tests__/badge.spec.tsx b/web/app/components/base/__tests__/badge.spec.tsx
index 8da348ec90..532e043768 100644
--- a/web/app/components/base/__tests__/badge.spec.tsx
+++ b/web/app/components/base/__tests__/badge.spec.tsx
@@ -33,6 +33,13 @@ describe('Badge', () => {
expect(badge).toHaveClass('relative', 'inline-flex', 'h-5', 'items-center')
})
+ it('should render xs dimm badge variant', () => {
+ const { container } = render( )
+ const badge = container.firstChild as HTMLElement
+ expect(badge).toHaveClass('min-w-4', 'px-1', 'py-0.5', 'bg-components-badge-bg-dimm')
+ expect(badge).not.toHaveClass('h-5')
+ })
+
it('should apply uppercase class by default', () => {
const { container } = render( )
const badge = container.firstChild as HTMLElement
diff --git a/web/app/components/base/badge.tsx b/web/app/components/base/badge.tsx
index b2cbbe0292..7a61c1d9d9 100644
--- a/web/app/components/base/badge.tsx
+++ b/web/app/components/base/badge.tsx
@@ -6,6 +6,8 @@ type BadgeProps = {
className?: string
text?: ReactNode
children?: ReactNode
+ size?: 'm' | 'xs'
+ variant?: 'default' | 'dimm'
uppercase?: boolean
hasRedCornerMark?: boolean
}
@@ -14,13 +16,17 @@ const Badge = ({
className,
text,
children,
+ size = 'm',
+ variant = 'default',
uppercase = true,
hasRedCornerMark,
}: BadgeProps) => {
return (
ReactElement
+ mainNavBadge?: ReactNode
+ variant?: 'default' | 'mainNav'
+}
+
+const mainNavMenuPopupClassName = 'w-60 max-w-80 overflow-hidden bg-components-panel-bg-blur! p-0! backdrop-blur-[5px]'
+const mainNavMenuGroupClassName = 'p-1'
+const mainNavMenuItemClassName = 'mx-0 h-8 gap-1 px-3 py-1'
+const mainNavMenuSubPopupClassName = 'w-60 max-h-[360px] bg-components-panel-bg-blur! p-1! backdrop-blur-[5px]'
+
+function MainNavRadioItemContent({
+ iconClassName,
+ label,
+}: {
+ iconClassName?: string
+ label: ReactNode
+}) {
+ return (
+ <>
+ {iconClassName &&
}
+
{label}
+
+ >
+ )
+}
+
+function AppearanceSubmenu() {
+ const { t } = useTranslation()
+ const { theme, setTheme } = useTheme()
+
+ return (
+
+
+
+
+
+ setTheme(value as Theme)}>
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function LanguageSubmenu() {
+ const locale = useLocale()
+ const router = useRouter()
+ const { t } = useTranslation()
+ const { userProfile, mutateUserProfile } = useAppContext()
+ const [editing, setEditing] = useState(false)
+ const languageOptions = languages.filter(item => item.supported)
+ const selectedLanguage = locale || userProfile.interface_language
+ const selectedTimezone = userProfile.timezone
+
+ const handleSelectLanguage = async (value: string) => {
+ setEditing(true)
+ try {
+ await updateUserProfile({ url: '/account/interface-language', body: { interface_language: value } })
+ toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
+ await setLocaleOnClient(value as Locale, false)
+ router.refresh()
+ }
+ catch (error) {
+ toast.error((error as Error).message)
+ }
+ finally {
+ setEditing(false)
+ }
+ }
+
+ const handleSelectTimezone = async (value: string) => {
+ setEditing(true)
+ try {
+ await updateUserProfile({ url: '/account/timezone', body: { timezone: value } })
+ toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
+ mutateUserProfile()
+ }
+ catch (error) {
+ toast.error((error as Error).message)
+ }
+ finally {
+ setEditing(false)
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+ {
+ if (!editing)
+ void handleSelectLanguage(value)
+ }}
+ >
+ {languageOptions.map(item => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {
+ if (!editing)
+ void handleSelectTimezone(value)
+ }}
+ >
+ {timezones.map(item => (
+
+
+
+ ))}
+
+
+
+ >
+ )
}
export default function AppSelector({
+ mainNavBadge,
trigger,
+ variant = 'default',
}: AccountDropdownProps = {}) {
const router = useRouter()
const [aboutVisible, setAboutVisible] = useState(false)
@@ -164,113 +345,164 @@ export default function AppSelector({
)}
-
-
-
-
- {userProfile.name}
- {isEducationAccount && (
-
-
- EDU
-
- )}
-
-
{userProfile.email}
-
-
-
- }
- />
- setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
- />
-
-
- {!systemFeatures.branding.enabled && (
- <>
-
- }
- />
- setIsAccountMenuOpen(false)} />
- {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && }
-
-
-
- }
- />
-
-
-
-
- )}
- />
- {
- env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
- {
- setAboutVisible(true)
- setIsAccountMenuOpen(false)
- }}
- trailing={(
-
-
{langGeniusVersionInfo.current_version}
-
+ {variant === 'mainNav'
+ ? (
+ <>
+
+
+
+
+
{userProfile.name}
+ {mainNavBadge}
- )}
+
{userProfile.email}
+
+
+
+
+
+ }
+ >
+ }
+ />
+
+
+
+
+
+
+ {
+ void handleLogout()
+ }}
+ >
+
+
+
+ >
+ )
+ : (
+ <>
+
+
+
+
+ {userProfile.name}
+ {isEducationAccount && (
+
+
+ EDU
+
+ )}
+
+
{userProfile.email}
+
+
+
+ }
/>
- )
- }
-
-
- >
- )}
-
-
- }
- />
-
-
-
-
- {
- void handleLogout()
- }}
- />
-
+ setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
+ />
+
+
+ {!systemFeatures.branding.enabled && (
+ <>
+
+ }
+ />
+ setIsAccountMenuOpen(false)} />
+ {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && }
+
+
+
+ }
+ />
+
+
+
+
+ )}
+ />
+ {
+ env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
+ {
+ setAboutVisible(true)
+ setIsAccountMenuOpen(false)
+ }}
+ trailing={(
+
+
{langGeniusVersionInfo.current_version}
+
+
+ )}
+ />
+ )
+ }
+
+
+ >
+ )}
+
+
+ }
+ />
+
+
+
+
+ {
+ void handleLogout()
+ }}
+ />
+
+ >
+ )}
{
diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx
index 8c84a6cf0d..f41972ef07 100644
--- a/web/app/components/main-nav/__tests__/index.spec.tsx
+++ b/web/app/components/main-nav/__tests__/index.spec.tsx
@@ -191,7 +191,8 @@ describe('MainNav', () => {
it('renders primary navigation with the planned routes', () => {
renderMainNav()
- expect(screen.getByText(Plan.team)).toBeInTheDocument()
+ expect(screen.getAllByText(Plan.team)).toHaveLength(1)
+ expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent(Plan.team)
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/explore/apps')
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
diff --git a/web/app/components/main-nav/index.tsx b/web/app/components/main-nav/index.tsx
index 3a8efcb9ea..da9aed1684 100644
--- a/web/app/components/main-nav/index.tsx
+++ b/web/app/components/main-nav/index.tsx
@@ -27,6 +27,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
+import Badge from '@/app/components/base/badge'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { Plan } from '@/app/components/billing/type'
import ItemOperation from '@/app/components/explore/item-operation'
@@ -34,11 +35,11 @@ import { GOTO_ANYTHING_OPEN_EVENT } from '@/app/components/goto-anything/hooks'
import AccountAbout from '@/app/components/header/account-about'
import AccountDropdown from '@/app/components/header/account-dropdown'
import Compliance from '@/app/components/header/account-dropdown/compliance'
+import { ExternalLinkIndicator, MenuItemContent } from '@/app/components/header/account-dropdown/menu-item-content'
import Support from '@/app/components/header/account-dropdown/support'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import GithubStar from '@/app/components/header/github-star'
import Indicator from '@/app/components/header/indicator'
-import PlanBadge from '@/app/components/header/plan-badge'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useDocLink } from '@/context/i18n'
@@ -95,14 +96,6 @@ const WorkspaceIcon = ({
)
-const MenuIcon = ({
- className,
-}: {
- className?: string
-}) => (
-
-)
-
const NavIcon = ({
icon,
className,
@@ -118,15 +111,10 @@ const WorkspacePlanBadge = ({
}: {
plan: Plan
}) => {
- if (plan !== Plan.sandbox)
- return
-
return (
-
-
- {plan}
-
-
+
+ {plan === Plan.professional ? 'pro' : plan}
+
)
}
@@ -365,7 +353,7 @@ const WebAppItem = ({
return (
router.push(url)}
@@ -373,15 +361,16 @@ const WebAppItem = ({
onMouseLeave={() => setIsHovering(false)}
title={app.app.name}
>
-
+
-
{app.app.name}
+
{app.app.name}
e.stopPropagation()}>
{
return (
-
+
setSearchVisible(value => !value)}
>
{t('sidebar.webApps', { ns: 'explore' })}
-
+
-
+
setSearchVisible(value => !value)}
>
-
+
+
+
@@ -530,46 +521,60 @@ const HelpMenu = () => {
{!systemFeatures.branding.enabled && (
<>
-
-
-
- {t('userProfile.helpCenter', { ns: 'common' })}
+
+
+ }
+ />
setOpen(false)} />
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && }
-
-
-
-
- {t('userProfile.roadmap', { ns: 'common' })}
+
+
+
+ }
+ />
-
-
- {t('userProfile.github', { ns: 'common' })}
-
-
-
-
+
+
+
+
+
+ )}
+ />
{env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
{
setAboutVisible(true)
setOpen(false)
}}
>
-
- {t('userProfile.about', { ns: 'common' })}
-
-
{langGeniusVersionInfo.current_version}
-
-
+
+ {t('about.version', { ns: 'common', version: langGeniusVersionInfo.current_version })}
+
+
+ )}
+ />
)}
@@ -587,8 +592,11 @@ const MainNav = ({
}: MainNavProps) => {
const { t } = useTranslation()
const pathname = usePathname()
- const { userProfile } = useAppContext()
+ const { currentWorkspace, userProfile } = useAppContext()
+ const { plan } = useProviderContext()
+ const { workspaces } = useWorkspacesContext()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
+ const workspacePlan = (workspaces.find(workspace => workspace.current)?.plan || currentWorkspace.plan || plan.type) as Plan
const navItems = useMemo
(() => [
{
href: '/explore/apps',
@@ -663,14 +671,16 @@ const MainNav = ({
}
+ variant="mainNav"
trigger={({ isOpen, ariaLabel }) => (
- {userProfile.name}
+ {userProfile.name}
)}
/>
diff --git a/web/context/app-context-provider.tsx b/web/context/app-context-provider.tsx
index fb17664d6d..ab09f4618c 100644
--- a/web/context/app-context-provider.tsx
+++ b/web/context/app-context-provider.tsx
@@ -148,9 +148,9 @@ export const AppContextProvider: FC
= ({ children }) =>
isValidatingCurrentWorkspace,
}}
>
-
+
{env.NEXT_PUBLIC_MAINTENANCE_NOTICE &&
}
-
diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json
index 0767e9297b..603111bffb 100644
--- a/web/i18n/en-US/common.json
+++ b/web/i18n/en-US/common.json
@@ -3,7 +3,12 @@
"about.latestAvailable": "Dify {{version}} is the latest version available.",
"about.nowAvailable": "Dify {{version}} is now available.",
"about.updateNow": "Update now",
+ "about.version": "version {{version}}",
"account.account": "Account",
+ "account.appearanceDark": "Dark",
+ "account.appearanceLabel": "Appearance",
+ "account.appearanceLight": "Light",
+ "account.appearanceSystem": "System",
"account.avatar": "Avatar",
"account.changeEmail.authTip": "Once your email is changed, Google or GitHub accounts linked to your old email will no longer be able to log in to this account.",
"account.changeEmail.changeTo": "Change to {{email}}",
@@ -209,11 +214,13 @@
"integrations.googleAccount": "Login with Google account",
"label.optional": "(optional)",
"language.displayLanguage": "Display Language",
+ "language.language": "Language",
"language.timezone": "Time Zone",
"license.expiring": "Expiring in one day",
"license.expiring_plural": "Expiring in {{count}} days",
"license.unlimited": "Unlimited",
"loading": "Loading",
+ "mainNav.help.docs": "Docs",
"mainNav.help.openMenu": "Open help menu",
"mainNav.home": "Home",
"mainNav.integrations": "Integrations",