mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 04:36:31 +08:00
feat(web): refine main nav onboarding UI
- Add a reusable dimm Badge variant for workspace plan labels - Update MainNav workspace, web apps, account, and help menu styling to match Figma - Add MainNav-specific account dropdown with appearance, language, timezone, and logout entries - Keep account trigger compact without plan badge while preserving the badge in the popup header - Prevent the common layout shell from creating a page-level scrollbar
This commit is contained in:
parent
7046e48590
commit
0aededada4
@ -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(<Badge text="beta" size="xs" variant="dimm" />)
|
||||
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(<Badge text="test" />)
|
||||
const badge = container.firstChild as HTMLElement
|
||||
|
||||
@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
'relative inline-flex h-5 items-center rounded-[5px] border border-divider-deep px-[5px] leading-3 whitespace-nowrap text-text-tertiary',
|
||||
'relative inline-flex items-center rounded-[5px] border border-divider-deep leading-3 whitespace-nowrap text-text-tertiary',
|
||||
size === 'xs' ? 'min-w-4 justify-center px-1 py-0.5' : 'h-5 px-[5px]',
|
||||
variant === 'dimm' && 'bg-components-badge-bg-dimm',
|
||||
uppercase ? 'system-2xs-medium-uppercase' : 'system-xs-medium',
|
||||
className,
|
||||
)}
|
||||
|
||||
@ -1,10 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import type { MouseEventHandler, ReactElement, ReactNode } from 'react'
|
||||
import type { Theme } from '@/app/components/base/theme-selector'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLinkItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuRadioItemIndicator,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
@ -13,14 +31,18 @@ import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { env } from '@/env'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import Link from '@/next/link'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { timezones } from '@/utils/timezone'
|
||||
import AccountAbout from '../account-about'
|
||||
import GithubStar from '../github-star'
|
||||
import Indicator from '../indicator'
|
||||
@ -112,10 +134,169 @@ type AccountDropdownProps = {
|
||||
isOpen: boolean
|
||||
ariaLabel: string
|
||||
}) => 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 && <span aria-hidden className={cn('size-4 shrink-0 text-text-tertiary', iconClassName)} />}
|
||||
<span className="min-w-0 grow truncate px-1 system-md-regular text-text-secondary">{label}</span>
|
||||
<DropdownMenuRadioItemIndicator />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function AppearanceSubmenu() {
|
||||
const { t } = useTranslation()
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className={mainNavMenuItemClassName}>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-sun-line"
|
||||
label={t('account.appearanceLabel', { ns: 'common' })}
|
||||
/>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
placement="right-start"
|
||||
sideOffset={6}
|
||||
popupClassName={mainNavMenuSubPopupClassName}
|
||||
>
|
||||
<DropdownMenuRadioGroup value={theme || 'system'} onValueChange={value => setTheme(value as Theme)}>
|
||||
<DropdownMenuRadioItem value="light" closeOnClick className={mainNavMenuItemClassName}>
|
||||
<MainNavRadioItemContent iconClassName="i-ri-sun-line" label={t('account.appearanceLight', { ns: 'common' })} />
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="dark" closeOnClick className={mainNavMenuItemClassName}>
|
||||
<MainNavRadioItemContent iconClassName="i-ri-moon-line" label={t('account.appearanceDark', { ns: 'common' })} />
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="system" closeOnClick className={mainNavMenuItemClassName}>
|
||||
<MainNavRadioItemContent iconClassName="i-ri-computer-line" label={t('account.appearanceSystem', { ns: 'common' })} />
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className={mainNavMenuItemClassName}>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-translate-2"
|
||||
label={t('language.language', { ns: 'common' })}
|
||||
/>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
placement="right-start"
|
||||
sideOffset={6}
|
||||
popupClassName={mainNavMenuSubPopupClassName}
|
||||
>
|
||||
<DropdownMenuRadioGroup
|
||||
value={selectedLanguage}
|
||||
onValueChange={(value) => {
|
||||
if (!editing)
|
||||
void handleSelectLanguage(value)
|
||||
}}
|
||||
>
|
||||
{languageOptions.map(item => (
|
||||
<DropdownMenuRadioItem key={item.value} value={item.value} closeOnClick className={mainNavMenuItemClassName}>
|
||||
<MainNavRadioItemContent label={item.name} />
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className={mainNavMenuItemClassName}>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-global-line"
|
||||
label={t('language.timezone', { ns: 'common' })}
|
||||
/>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
placement="right-start"
|
||||
sideOffset={6}
|
||||
popupClassName={mainNavMenuSubPopupClassName}
|
||||
>
|
||||
<DropdownMenuRadioGroup
|
||||
value={selectedTimezone}
|
||||
onValueChange={(value) => {
|
||||
if (!editing)
|
||||
void handleSelectTimezone(value)
|
||||
}}
|
||||
>
|
||||
{timezones.map(item => (
|
||||
<DropdownMenuRadioItem key={item.value} value={String(item.value)} closeOnClick className={mainNavMenuItemClassName}>
|
||||
<MainNavRadioItemContent label={item.name} />
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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({
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
<DropdownMenuContent
|
||||
placement={variant === 'mainNav' ? 'top-start' : 'bottom-end'}
|
||||
sideOffset={6}
|
||||
popupClassName="w-60 max-w-80 bg-components-panel-bg-blur! py-0! backdrop-blur-xs"
|
||||
alignOffset={variant === 'mainNav' ? 4 : 0}
|
||||
popupClassName={variant === 'mainNav' ? mainNavMenuPopupClassName : 'w-60 max-w-80 bg-components-panel-bg-blur! py-0! backdrop-blur-xs'}
|
||||
>
|
||||
<DropdownMenuGroup className="py-1">
|
||||
<div className="mx-1 flex flex-nowrap items-center py-2 pr-2 pl-3">
|
||||
<div className="grow">
|
||||
<div className="system-md-medium break-all text-text-primary">
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<PremiumBadge size="s" color="blue" className="ml-1 px-2!">
|
||||
<span aria-hidden className="mr-1 i-ri-graduation-cap-fill h-3 w-3" />
|
||||
<span className="system-2xs-medium">EDU</span>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</div>
|
||||
<div className="system-xs-regular break-all text-text-tertiary">{userProfile.email}</div>
|
||||
</div>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
|
||||
</div>
|
||||
<AccountMenuRouteItem
|
||||
href="/account"
|
||||
iconClassName="i-ri-account-circle-line"
|
||||
label={t('account.account', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
<AccountMenuActionItem
|
||||
iconClassName="i-ri-settings-3-line"
|
||||
label={t('userProfile.settings', { ns: 'common' })}
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
{!systemFeatures.branding.enabled && (
|
||||
<>
|
||||
<AccountMenuSection>
|
||||
<AccountMenuExternalItem
|
||||
href={docLink('/use-dify/getting-started/introduction')}
|
||||
iconClassName="i-ri-book-open-line"
|
||||
label={t('userProfile.helpCenter', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
<Support closeAccountDropdown={() => setIsAccountMenuOpen(false)} />
|
||||
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
|
||||
</AccountMenuSection>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
<AccountMenuSection>
|
||||
<AccountMenuExternalItem
|
||||
href="https://roadmap.dify.ai"
|
||||
iconClassName="i-ri-map-2-line"
|
||||
label={t('userProfile.roadmap', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
<AccountMenuExternalItem
|
||||
href="https://github.com/langgenius/dify"
|
||||
iconClassName="i-ri-github-line"
|
||||
label={t('userProfile.github', { ns: 'common' })}
|
||||
trailing={(
|
||||
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
|
||||
<span aria-hidden className="i-ri-star-line size-3 shrink-0 text-text-tertiary" />
|
||||
<GithubStar className="system-2xs-medium-uppercase text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{
|
||||
env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
|
||||
<AccountMenuActionItem
|
||||
iconClassName="i-ri-information-2-line"
|
||||
label={t('userProfile.about', { ns: 'common' })}
|
||||
onClick={() => {
|
||||
setAboutVisible(true)
|
||||
setIsAccountMenuOpen(false)
|
||||
}}
|
||||
trailing={(
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="mr-2 system-xs-regular text-text-tertiary">{langGeniusVersionInfo.current_version}</div>
|
||||
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
|
||||
{variant === 'mainNav'
|
||||
? (
|
||||
<>
|
||||
<DropdownMenuGroup className={mainNavMenuGroupClassName}>
|
||||
<div className="flex items-center gap-1 rounded-xl bg-gradient-to-b from-background-section-burn to-background-section p-3">
|
||||
<div className="flex min-w-0 grow flex-col gap-1">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<div className="truncate body-md-medium text-text-primary">{userProfile.name}</div>
|
||||
{mainNavBadge}
|
||||
</div>
|
||||
)}
|
||||
<div className="truncate system-xs-regular text-text-tertiary">{userProfile.email}</div>
|
||||
</div>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuGroup className={mainNavMenuGroupClassName}>
|
||||
<DropdownMenuLinkItem
|
||||
className={cn('justify-between', mainNavMenuItemClassName)}
|
||||
render={<Link href="/account" />}
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-account-circle-line"
|
||||
label={t('account.account', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuLinkItem>
|
||||
<AppearanceSubmenu />
|
||||
<LanguageSubmenu />
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
<DropdownMenuGroup className={mainNavMenuGroupClassName}>
|
||||
<DropdownMenuItem
|
||||
className={mainNavMenuItemClassName}
|
||||
onClick={() => {
|
||||
void handleLogout()
|
||||
}}
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-logout-box-r-line"
|
||||
label={t('userProfile.logout', { ns: 'common' })}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<DropdownMenuGroup className="py-1">
|
||||
<div className="mx-1 flex flex-nowrap items-center py-2 pr-2 pl-3">
|
||||
<div className="grow">
|
||||
<div className="system-md-medium break-all text-text-primary">
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<PremiumBadge size="s" color="blue" className="ml-1 px-2!">
|
||||
<span aria-hidden className="mr-1 i-ri-graduation-cap-fill h-3 w-3" />
|
||||
<span className="system-2xs-medium">EDU</span>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</div>
|
||||
<div className="system-xs-regular break-all text-text-tertiary">{userProfile.email}</div>
|
||||
</div>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
|
||||
</div>
|
||||
<AccountMenuRouteItem
|
||||
href="/account"
|
||||
iconClassName="i-ri-account-circle-line"
|
||||
label={t('account.account', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</AccountMenuSection>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
</>
|
||||
)}
|
||||
<AccountMenuSection>
|
||||
<DropdownMenuItem
|
||||
closeOnClick={false}
|
||||
className="cursor-default data-highlighted:bg-transparent"
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-t-shirt-2-line"
|
||||
label={t('theme.theme', { ns: 'common' })}
|
||||
trailing={<ThemeSwitcher />}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</AccountMenuSection>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
<AccountMenuSection>
|
||||
<AccountMenuActionItem
|
||||
iconClassName="i-ri-logout-box-r-line"
|
||||
label={t('userProfile.logout', { ns: 'common' })}
|
||||
onClick={() => {
|
||||
void handleLogout()
|
||||
}}
|
||||
/>
|
||||
</AccountMenuSection>
|
||||
<AccountMenuActionItem
|
||||
iconClassName="i-ri-settings-3-line"
|
||||
label={t('userProfile.settings', { ns: 'common' })}
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
{!systemFeatures.branding.enabled && (
|
||||
<>
|
||||
<AccountMenuSection>
|
||||
<AccountMenuExternalItem
|
||||
href={docLink('/use-dify/getting-started/introduction')}
|
||||
iconClassName="i-ri-book-open-line"
|
||||
label={t('userProfile.helpCenter', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
<Support closeAccountDropdown={() => setIsAccountMenuOpen(false)} />
|
||||
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
|
||||
</AccountMenuSection>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
<AccountMenuSection>
|
||||
<AccountMenuExternalItem
|
||||
href="https://roadmap.dify.ai"
|
||||
iconClassName="i-ri-map-2-line"
|
||||
label={t('userProfile.roadmap', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
<AccountMenuExternalItem
|
||||
href="https://github.com/langgenius/dify"
|
||||
iconClassName="i-ri-github-line"
|
||||
label={t('userProfile.github', { ns: 'common' })}
|
||||
trailing={(
|
||||
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
|
||||
<span aria-hidden className="i-ri-star-line size-3 shrink-0 text-text-tertiary" />
|
||||
<GithubStar className="system-2xs-medium-uppercase text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{
|
||||
env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
|
||||
<AccountMenuActionItem
|
||||
iconClassName="i-ri-information-2-line"
|
||||
label={t('userProfile.about', { ns: 'common' })}
|
||||
onClick={() => {
|
||||
setAboutVisible(true)
|
||||
setIsAccountMenuOpen(false)
|
||||
}}
|
||||
trailing={(
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="mr-2 system-xs-regular text-text-tertiary">{langGeniusVersionInfo.current_version}</div>
|
||||
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</AccountMenuSection>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
</>
|
||||
)}
|
||||
<AccountMenuSection>
|
||||
<DropdownMenuItem
|
||||
closeOnClick={false}
|
||||
className="cursor-default data-highlighted:bg-transparent"
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-t-shirt-2-line"
|
||||
label={t('theme.theme', { ns: 'common' })}
|
||||
trailing={<ThemeSwitcher />}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</AccountMenuSection>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
<AccountMenuSection>
|
||||
<AccountMenuActionItem
|
||||
iconClassName="i-ri-logout-box-r-line"
|
||||
label={t('userProfile.logout', { ns: 'common' })}
|
||||
onClick={() => {
|
||||
void handleLogout()
|
||||
}}
|
||||
/>
|
||||
</AccountMenuSection>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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 = ({
|
||||
</div>
|
||||
)
|
||||
|
||||
const MenuIcon = ({
|
||||
className,
|
||||
}: {
|
||||
className?: string
|
||||
}) => (
|
||||
<span className={cn('flex h-4 w-4 shrink-0 items-center justify-center text-text-tertiary', className)} />
|
||||
)
|
||||
|
||||
const NavIcon = ({
|
||||
icon,
|
||||
className,
|
||||
@ -118,15 +111,10 @@ const WorkspacePlanBadge = ({
|
||||
}: {
|
||||
plan: Plan
|
||||
}) => {
|
||||
if (plan !== Plan.sandbox)
|
||||
return <PlanBadge plan={plan} />
|
||||
|
||||
return (
|
||||
<div className="flex min-w-4 shrink-0 items-center justify-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5">
|
||||
<span className="min-w-0 text-center system-2xs-medium-uppercase text-text-tertiary">
|
||||
{plan}
|
||||
</span>
|
||||
</div>
|
||||
<Badge size="xs" variant="dimm" className="shrink-0">
|
||||
{plan === Plan.professional ? 'pro' : plan}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
@ -365,7 +353,7 @@ const WebAppItem = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group flex h-6 cursor-pointer items-center justify-between rounded-lg pr-0.5 pl-2 system-sm-regular text-components-main-nav-text transition-colors',
|
||||
'group flex cursor-pointer items-center justify-between gap-2 rounded-lg py-0.5 pr-0.5 pl-2 text-components-main-nav-text transition-colors',
|
||||
isSelected ? 'bg-state-base-hover text-components-main-nav-text' : 'hover:bg-state-base-hover hover:text-components-main-nav-text',
|
||||
)}
|
||||
onClick={() => router.push(url)}
|
||||
@ -373,15 +361,16 @@ const WebAppItem = ({
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
title={app.app.name}
|
||||
>
|
||||
<div className="flex min-w-0 grow items-center gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<AppIcon
|
||||
size="tiny"
|
||||
className="size-5 rounded-md text-sm"
|
||||
iconType={app.app.icon_type}
|
||||
icon={app.app.icon}
|
||||
background={app.app.icon_background}
|
||||
imageUrl={app.app.icon_url}
|
||||
/>
|
||||
<span className="truncate">{app.app.name}</span>
|
||||
<span className="min-w-0 flex-1 truncate py-1 pr-1 system-sm-regular">{app.app.name}</span>
|
||||
</div>
|
||||
<div className="h-6 shrink-0" onClick={e => e.stopPropagation()}>
|
||||
<ItemOperation
|
||||
@ -429,23 +418,25 @@ const WebAppsSection = () => {
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="flex items-center justify-between px-2 pb-1">
|
||||
<div className="flex items-center justify-between py-1 pr-2.5 pl-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 items-center gap-1 text-left system-xs-medium-uppercase text-text-quaternary hover:text-text-tertiary"
|
||||
className="flex min-w-0 items-center rounded-md px-2 py-1 text-left system-xs-medium-uppercase text-text-tertiary hover:text-text-secondary"
|
||||
onClick={() => setSearchVisible(value => !value)}
|
||||
>
|
||||
<span>{t('sidebar.webApps', { ns: 'explore' })}</span>
|
||||
<span aria-hidden className="i-ri-arrow-down-s-line h-3.5 w-3.5 shrink-0" />
|
||||
<span aria-hidden className="i-ri-arrow-down-s-fill h-4 w-4 shrink-0" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.search', { ns: 'common' })}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', searchVisible && 'bg-state-base-hover text-text-secondary')}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-md p-0.5 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', searchVisible && 'bg-state-base-hover text-text-secondary')}
|
||||
onClick={() => setSearchVisible(value => !value)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-search-line h-4 w-4" />
|
||||
<span className="flex size-5 shrink-0 items-center justify-center">
|
||||
<span aria-hidden className="i-ri-search-line size-3.5" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -530,46 +521,60 @@ const HelpMenu = () => {
|
||||
<DropdownMenuContent
|
||||
placement="top-end"
|
||||
sideOffset={8}
|
||||
popupClassName="w-60 p-1"
|
||||
popupClassName="w-60 overflow-hidden bg-components-panel-bg-blur! p-0! backdrop-blur-[5px]"
|
||||
>
|
||||
{!systemFeatures.branding.enabled && (
|
||||
<>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLinkItem href={docLink('/use-dify/getting-started/introduction')} target="_blank" rel="noopener noreferrer" className="gap-2 px-3">
|
||||
<MenuIcon className="i-ri-book-open-line" />
|
||||
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.helpCenter', { ns: 'common' })}</span>
|
||||
<DropdownMenuGroup className="p-1">
|
||||
<DropdownMenuLinkItem href={docLink('/use-dify/getting-started/introduction')} target="_blank" rel="noopener noreferrer" className="mx-0 h-8 gap-1 px-3 py-1">
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-book-open-line"
|
||||
label={t('mainNav.help.docs', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuLinkItem>
|
||||
<Support closeAccountDropdown={() => setOpen(false)} />
|
||||
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLinkItem href="https://roadmap.dify.ai" target="_blank" rel="noopener noreferrer" className="gap-2 px-3">
|
||||
<MenuIcon className="i-ri-map-2-line" />
|
||||
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.roadmap', { ns: 'common' })}</span>
|
||||
<DropdownMenuSeparator className="my-0!" />
|
||||
<DropdownMenuGroup className="p-1">
|
||||
<DropdownMenuLinkItem href="https://roadmap.dify.ai" target="_blank" rel="noopener noreferrer" className="mx-0 h-8 gap-1 px-3 py-1.5">
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-map-2-line"
|
||||
label={t('userProfile.roadmap', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuLinkItem>
|
||||
<DropdownMenuLinkItem href="https://github.com/langgenius/dify" target="_blank" rel="noopener noreferrer" className="gap-2 px-3">
|
||||
<MenuIcon className="i-ri-github-line" />
|
||||
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.github', { ns: 'common' })}</span>
|
||||
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
|
||||
<span aria-hidden className="i-ri-star-line size-3 shrink-0 text-text-tertiary" />
|
||||
<GithubStar className="system-2xs-medium-uppercase text-text-tertiary" />
|
||||
</div>
|
||||
<DropdownMenuLinkItem href="https://github.com/langgenius/dify" target="_blank" rel="noopener noreferrer" className="mx-0 h-8 gap-1 px-3 py-1.5">
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-github-line"
|
||||
label={t('userProfile.github', { ns: 'common' })}
|
||||
trailing={(
|
||||
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
|
||||
<span aria-hidden className="i-ri-star-line size-3 shrink-0 text-text-tertiary" />
|
||||
<GithubStar className="system-2xs-medium-uppercase text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</DropdownMenuLinkItem>
|
||||
{env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
|
||||
<DropdownMenuItem
|
||||
className="gap-2 px-3"
|
||||
className="mx-0 h-8 gap-1 px-3 py-1.5"
|
||||
onClick={() => {
|
||||
setAboutVisible(true)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<span aria-hidden className="i-ri-information-2-line h-4 w-4 shrink-0 text-text-tertiary" />
|
||||
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.about', { ns: 'common' })}</span>
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="mr-2 system-xs-regular text-text-tertiary">{langGeniusVersionInfo.current_version}</div>
|
||||
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
|
||||
</div>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-information-2-line"
|
||||
label={t('userProfile.about', { ns: 'common' })}
|
||||
trailing={(
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="mr-2 system-xs-regular text-text-tertiary">{t('about.version', { ns: 'common', version: langGeniusVersionInfo.current_version })}</div>
|
||||
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
@ -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<MainNavItem[]>(() => [
|
||||
{
|
||||
href: '/explore/apps',
|
||||
@ -663,14 +671,16 @@ const MainNav = ({
|
||||
</div>
|
||||
<div className="flex w-[240px] items-center justify-between bg-gradient-to-b from-background-body-transparent to-background-body to-50% py-3 pr-1 pl-3 backdrop-blur-[2px]">
|
||||
<AccountDropdown
|
||||
mainNavBadge={<WorkspacePlanBadge plan={workspacePlan} />}
|
||||
variant="mainNav"
|
||||
trigger={({ isOpen, ariaLabel }) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={ariaLabel}
|
||||
className={cn('flex shrink-0 items-center gap-3 rounded-full py-1 pr-4 pl-1 text-left text-components-main-nav-text transition-colors hover:bg-state-base-hover', isOpen && 'bg-state-base-hover')}
|
||||
className={cn('flex max-w-[188px] min-w-0 shrink items-center gap-3 rounded-full py-1 pr-4 pl-1 text-left text-components-main-nav-text transition-colors hover:bg-state-base-hover', isOpen && 'bg-state-base-hover')}
|
||||
>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="md" className="size-7" />
|
||||
<span className="system-md-medium whitespace-nowrap">{userProfile.name}</span>
|
||||
<span className="min-w-0 flex-1 truncate system-md-medium">{userProfile.name}</span>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -148,9 +148,9 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
||||
isValidatingCurrentWorkspace,
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full flex-col overflow-y-auto">
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
{env.NEXT_PUBLIC_MAINTENANCE_NOTICE && <MaintenanceNotice />}
|
||||
<div className="relative flex grow flex-col overflow-x-hidden overflow-y-auto bg-background-body">
|
||||
<div className="relative flex h-0 min-h-0 grow flex-col overflow-hidden bg-background-body">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user