mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +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')
|
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', () => {
|
it('should apply uppercase class by default', () => {
|
||||||
const { container } = render(<Badge text="test" />)
|
const { container } = render(<Badge text="test" />)
|
||||||
const badge = container.firstChild as HTMLElement
|
const badge = container.firstChild as HTMLElement
|
||||||
|
|||||||
@ -6,6 +6,8 @@ type BadgeProps = {
|
|||||||
className?: string
|
className?: string
|
||||||
text?: ReactNode
|
text?: ReactNode
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
size?: 'm' | 'xs'
|
||||||
|
variant?: 'default' | 'dimm'
|
||||||
uppercase?: boolean
|
uppercase?: boolean
|
||||||
hasRedCornerMark?: boolean
|
hasRedCornerMark?: boolean
|
||||||
}
|
}
|
||||||
@ -14,13 +16,17 @@ const Badge = ({
|
|||||||
className,
|
className,
|
||||||
text,
|
text,
|
||||||
children,
|
children,
|
||||||
|
size = 'm',
|
||||||
|
variant = 'default',
|
||||||
uppercase = true,
|
uppercase = true,
|
||||||
hasRedCornerMark,
|
hasRedCornerMark,
|
||||||
}: BadgeProps) => {
|
}: BadgeProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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',
|
uppercase ? 'system-2xs-medium-uppercase' : 'system-xs-medium',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,10 +1,28 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { MouseEventHandler, ReactElement, ReactNode } from 'react'
|
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 { Avatar } from '@langgenius/dify-ui/avatar'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
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 { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
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 { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
import { IS_CLOUD_EDITION } from '@/config'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink, useLocale } from '@/context/i18n'
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { env } from '@/env'
|
import { env } from '@/env'
|
||||||
|
import { setLocaleOnClient } from '@/i18n-config'
|
||||||
|
import { languages } from '@/i18n-config/language'
|
||||||
import Link from '@/next/link'
|
import Link from '@/next/link'
|
||||||
import { useRouter } from '@/next/navigation'
|
import { useRouter } from '@/next/navigation'
|
||||||
|
import { updateUserProfile } from '@/service/common'
|
||||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
|
import { timezones } from '@/utils/timezone'
|
||||||
import AccountAbout from '../account-about'
|
import AccountAbout from '../account-about'
|
||||||
import GithubStar from '../github-star'
|
import GithubStar from '../github-star'
|
||||||
import Indicator from '../indicator'
|
import Indicator from '../indicator'
|
||||||
@ -112,10 +134,169 @@ type AccountDropdownProps = {
|
|||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
ariaLabel: string
|
ariaLabel: string
|
||||||
}) => ReactElement
|
}) => 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({
|
export default function AppSelector({
|
||||||
|
mainNavBadge,
|
||||||
trigger,
|
trigger,
|
||||||
|
variant = 'default',
|
||||||
}: AccountDropdownProps = {}) {
|
}: AccountDropdownProps = {}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [aboutVisible, setAboutVisible] = useState(false)
|
const [aboutVisible, setAboutVisible] = useState(false)
|
||||||
@ -164,113 +345,164 @@ export default function AppSelector({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
|
placement={variant === 'mainNav' ? 'top-start' : 'bottom-end'}
|
||||||
sideOffset={6}
|
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">
|
{variant === 'mainNav'
|
||||||
<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">
|
<DropdownMenuGroup className={mainNavMenuGroupClassName}>
|
||||||
{userProfile.name}
|
<div className="flex items-center gap-1 rounded-xl bg-gradient-to-b from-background-section-burn to-background-section p-3">
|
||||||
{isEducationAccount && (
|
<div className="flex min-w-0 grow flex-col gap-1">
|
||||||
<PremiumBadge size="s" color="blue" className="ml-1 px-2!">
|
<div className="flex min-w-0 items-center gap-1">
|
||||||
<span aria-hidden className="mr-1 i-ri-graduation-cap-fill h-3 w-3" />
|
<div className="truncate body-md-medium text-text-primary">{userProfile.name}</div>
|
||||||
<span className="system-2xs-medium">EDU</span>
|
{mainNavBadge}
|
||||||
</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'} />
|
|
||||||
</div>
|
</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 />}
|
||||||
/>
|
/>
|
||||||
)
|
<AccountMenuActionItem
|
||||||
}
|
iconClassName="i-ri-settings-3-line"
|
||||||
</AccountMenuSection>
|
label={t('userProfile.settings', { ns: 'common' })}
|
||||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
|
||||||
</>
|
/>
|
||||||
)}
|
</DropdownMenuGroup>
|
||||||
<AccountMenuSection>
|
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||||
<DropdownMenuItem
|
{!systemFeatures.branding.enabled && (
|
||||||
closeOnClick={false}
|
<>
|
||||||
className="cursor-default data-highlighted:bg-transparent"
|
<AccountMenuSection>
|
||||||
>
|
<AccountMenuExternalItem
|
||||||
<MenuItemContent
|
href={docLink('/use-dify/getting-started/introduction')}
|
||||||
iconClassName="i-ri-t-shirt-2-line"
|
iconClassName="i-ri-book-open-line"
|
||||||
label={t('theme.theme', { ns: 'common' })}
|
label={t('userProfile.helpCenter', { ns: 'common' })}
|
||||||
trailing={<ThemeSwitcher />}
|
trailing={<ExternalLinkIndicator />}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuItem>
|
<Support closeAccountDropdown={() => setIsAccountMenuOpen(false)} />
|
||||||
</AccountMenuSection>
|
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
|
||||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
</AccountMenuSection>
|
||||||
<AccountMenuSection>
|
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||||
<AccountMenuActionItem
|
<AccountMenuSection>
|
||||||
iconClassName="i-ri-logout-box-r-line"
|
<AccountMenuExternalItem
|
||||||
label={t('userProfile.logout', { ns: 'common' })}
|
href="https://roadmap.dify.ai"
|
||||||
onClick={() => {
|
iconClassName="i-ri-map-2-line"
|
||||||
void handleLogout()
|
label={t('userProfile.roadmap', { ns: 'common' })}
|
||||||
}}
|
trailing={<ExternalLinkIndicator />}
|
||||||
/>
|
/>
|
||||||
</AccountMenuSection>
|
<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>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -191,7 +191,8 @@ describe('MainNav', () => {
|
|||||||
it('renders primary navigation with the planned routes', () => {
|
it('renders primary navigation with the planned routes', () => {
|
||||||
renderMainNav()
|
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.mainNav.home/ })).toHaveAttribute('href', '/explore/apps')
|
||||||
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
|
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
|
||||||
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
|
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 { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
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 DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import ItemOperation from '@/app/components/explore/item-operation'
|
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 AccountAbout from '@/app/components/header/account-about'
|
||||||
import AccountDropdown from '@/app/components/header/account-dropdown'
|
import AccountDropdown from '@/app/components/header/account-dropdown'
|
||||||
import Compliance from '@/app/components/header/account-dropdown/compliance'
|
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 Support from '@/app/components/header/account-dropdown/support'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import GithubStar from '@/app/components/header/github-star'
|
import GithubStar from '@/app/components/header/github-star'
|
||||||
import Indicator from '@/app/components/header/indicator'
|
import Indicator from '@/app/components/header/indicator'
|
||||||
import PlanBadge from '@/app/components/header/plan-badge'
|
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
import { IS_CLOUD_EDITION } from '@/config'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
@ -95,14 +96,6 @@ const WorkspaceIcon = ({
|
|||||||
</div>
|
</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 = ({
|
const NavIcon = ({
|
||||||
icon,
|
icon,
|
||||||
className,
|
className,
|
||||||
@ -118,15 +111,10 @@ const WorkspacePlanBadge = ({
|
|||||||
}: {
|
}: {
|
||||||
plan: Plan
|
plan: Plan
|
||||||
}) => {
|
}) => {
|
||||||
if (plan !== Plan.sandbox)
|
|
||||||
return <PlanBadge plan={plan} />
|
|
||||||
|
|
||||||
return (
|
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">
|
<Badge size="xs" variant="dimm" className="shrink-0">
|
||||||
<span className="min-w-0 text-center system-2xs-medium-uppercase text-text-tertiary">
|
{plan === Plan.professional ? 'pro' : plan}
|
||||||
{plan}
|
</Badge>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,7 +353,7 @@ const WebAppItem = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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',
|
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)}
|
onClick={() => router.push(url)}
|
||||||
@ -373,15 +361,16 @@ const WebAppItem = ({
|
|||||||
onMouseLeave={() => setIsHovering(false)}
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
title={app.app.name}
|
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
|
<AppIcon
|
||||||
size="tiny"
|
size="tiny"
|
||||||
|
className="size-5 rounded-md text-sm"
|
||||||
iconType={app.app.icon_type}
|
iconType={app.app.icon_type}
|
||||||
icon={app.app.icon}
|
icon={app.app.icon}
|
||||||
background={app.app.icon_background}
|
background={app.app.icon_background}
|
||||||
imageUrl={app.app.icon_url}
|
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>
|
||||||
<div className="h-6 shrink-0" onClick={e => e.stopPropagation()}>
|
<div className="h-6 shrink-0" onClick={e => e.stopPropagation()}>
|
||||||
<ItemOperation
|
<ItemOperation
|
||||||
@ -429,23 +418,25 @@ const WebAppsSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<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
|
<button
|
||||||
type="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)}
|
onClick={() => setSearchVisible(value => !value)}
|
||||||
>
|
>
|
||||||
<span>{t('sidebar.webApps', { ns: 'explore' })}</span>
|
<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>
|
</button>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-0.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t('operation.search', { ns: 'common' })}
|
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)}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -530,46 +521,60 @@ const HelpMenu = () => {
|
|||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
placement="top-end"
|
placement="top-end"
|
||||||
sideOffset={8}
|
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 && (
|
{!systemFeatures.branding.enabled && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup className="p-1">
|
||||||
<DropdownMenuLinkItem href={docLink('/use-dify/getting-started/introduction')} target="_blank" rel="noopener noreferrer" className="gap-2 px-3">
|
<DropdownMenuLinkItem href={docLink('/use-dify/getting-started/introduction')} target="_blank" rel="noopener noreferrer" className="mx-0 h-8 gap-1 px-3 py-1">
|
||||||
<MenuIcon className="i-ri-book-open-line" />
|
<MenuItemContent
|
||||||
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.helpCenter', { ns: 'common' })}</span>
|
iconClassName="i-ri-book-open-line"
|
||||||
|
label={t('mainNav.help.docs', { ns: 'common' })}
|
||||||
|
trailing={<ExternalLinkIndicator />}
|
||||||
|
/>
|
||||||
</DropdownMenuLinkItem>
|
</DropdownMenuLinkItem>
|
||||||
<Support closeAccountDropdown={() => setOpen(false)} />
|
<Support closeAccountDropdown={() => setOpen(false)} />
|
||||||
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
|
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator className="my-0!" />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup className="p-1">
|
||||||
<DropdownMenuLinkItem href="https://roadmap.dify.ai" target="_blank" rel="noopener noreferrer" className="gap-2 px-3">
|
<DropdownMenuLinkItem href="https://roadmap.dify.ai" target="_blank" rel="noopener noreferrer" className="mx-0 h-8 gap-1 px-3 py-1.5">
|
||||||
<MenuIcon className="i-ri-map-2-line" />
|
<MenuItemContent
|
||||||
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.roadmap', { ns: 'common' })}</span>
|
iconClassName="i-ri-map-2-line"
|
||||||
|
label={t('userProfile.roadmap', { ns: 'common' })}
|
||||||
|
trailing={<ExternalLinkIndicator />}
|
||||||
|
/>
|
||||||
</DropdownMenuLinkItem>
|
</DropdownMenuLinkItem>
|
||||||
<DropdownMenuLinkItem href="https://github.com/langgenius/dify" target="_blank" rel="noopener noreferrer" className="gap-2 px-3">
|
<DropdownMenuLinkItem href="https://github.com/langgenius/dify" target="_blank" rel="noopener noreferrer" className="mx-0 h-8 gap-1 px-3 py-1.5">
|
||||||
<MenuIcon className="i-ri-github-line" />
|
<MenuItemContent
|
||||||
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.github', { ns: 'common' })}</span>
|
iconClassName="i-ri-github-line"
|
||||||
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
|
label={t('userProfile.github', { ns: 'common' })}
|
||||||
<span aria-hidden className="i-ri-star-line size-3 shrink-0 text-text-tertiary" />
|
trailing={(
|
||||||
<GithubStar className="system-2xs-medium-uppercase text-text-tertiary" />
|
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
|
||||||
</div>
|
<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>
|
</DropdownMenuLinkItem>
|
||||||
{env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
|
{env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="gap-2 px-3"
|
className="mx-0 h-8 gap-1 px-3 py-1.5"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAboutVisible(true)
|
setAboutVisible(true)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span aria-hidden className="i-ri-information-2-line h-4 w-4 shrink-0 text-text-tertiary" />
|
<MenuItemContent
|
||||||
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.about', { ns: 'common' })}</span>
|
iconClassName="i-ri-information-2-line"
|
||||||
<div className="flex shrink-0 items-center">
|
label={t('userProfile.about', { ns: 'common' })}
|
||||||
<div className="mr-2 system-xs-regular text-text-tertiary">{langGeniusVersionInfo.current_version}</div>
|
trailing={(
|
||||||
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
|
<div className="flex shrink-0 items-center">
|
||||||
</div>
|
<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>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
@ -587,8 +592,11 @@ const MainNav = ({
|
|||||||
}: MainNavProps) => {
|
}: MainNavProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const { userProfile } = useAppContext()
|
const { currentWorkspace, userProfile } = useAppContext()
|
||||||
|
const { plan } = useProviderContext()
|
||||||
|
const { workspaces } = useWorkspacesContext()
|
||||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
|
const workspacePlan = (workspaces.find(workspace => workspace.current)?.plan || currentWorkspace.plan || plan.type) as Plan
|
||||||
const navItems = useMemo<MainNavItem[]>(() => [
|
const navItems = useMemo<MainNavItem[]>(() => [
|
||||||
{
|
{
|
||||||
href: '/explore/apps',
|
href: '/explore/apps',
|
||||||
@ -663,14 +671,16 @@ const MainNav = ({
|
|||||||
</div>
|
</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]">
|
<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
|
<AccountDropdown
|
||||||
|
mainNavBadge={<WorkspacePlanBadge plan={workspacePlan} />}
|
||||||
|
variant="mainNav"
|
||||||
trigger={({ isOpen, ariaLabel }) => (
|
trigger={({ isOpen, ariaLabel }) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={ariaLabel}
|
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" />
|
<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>
|
</button>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -148,9 +148,9 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
|||||||
isValidatingCurrentWorkspace,
|
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 />}
|
{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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,7 +3,12 @@
|
|||||||
"about.latestAvailable": "Dify {{version}} is the latest version available.",
|
"about.latestAvailable": "Dify {{version}} is the latest version available.",
|
||||||
"about.nowAvailable": "Dify {{version}} is now available.",
|
"about.nowAvailable": "Dify {{version}} is now available.",
|
||||||
"about.updateNow": "Update now",
|
"about.updateNow": "Update now",
|
||||||
|
"about.version": "version {{version}}",
|
||||||
"account.account": "Account",
|
"account.account": "Account",
|
||||||
|
"account.appearanceDark": "Dark",
|
||||||
|
"account.appearanceLabel": "Appearance",
|
||||||
|
"account.appearanceLight": "Light",
|
||||||
|
"account.appearanceSystem": "System",
|
||||||
"account.avatar": "Avatar",
|
"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.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}}",
|
"account.changeEmail.changeTo": "Change to {{email}}",
|
||||||
@ -209,11 +214,13 @@
|
|||||||
"integrations.googleAccount": "Login with Google account",
|
"integrations.googleAccount": "Login with Google account",
|
||||||
"label.optional": "(optional)",
|
"label.optional": "(optional)",
|
||||||
"language.displayLanguage": "Display Language",
|
"language.displayLanguage": "Display Language",
|
||||||
|
"language.language": "Language",
|
||||||
"language.timezone": "Time Zone",
|
"language.timezone": "Time Zone",
|
||||||
"license.expiring": "Expiring in one day",
|
"license.expiring": "Expiring in one day",
|
||||||
"license.expiring_plural": "Expiring in {{count}} days",
|
"license.expiring_plural": "Expiring in {{count}} days",
|
||||||
"license.unlimited": "Unlimited",
|
"license.unlimited": "Unlimited",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
|
"mainNav.help.docs": "Docs",
|
||||||
"mainNav.help.openMenu": "Open help menu",
|
"mainNav.help.openMenu": "Open help menu",
|
||||||
"mainNav.home": "Home",
|
"mainNav.home": "Home",
|
||||||
"mainNav.integrations": "Integrations",
|
"mainNav.integrations": "Integrations",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user