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 (
-
+
-
+
@@ -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 }) => ( )} /> 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 && } -
+
{children}
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",