mirror of
https://github.com/langgenius/dify.git
synced 2026-06-24 21:11:16 +08:00
fix(web): polish onboarding main nav and preferences tab (#37844)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
parent
67a5eacf2d
commit
4573aaa717
@ -3668,11 +3668,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/language-page/__tests__/index.spec.tsx": {
|
||||
"jsx-a11y/role-has-required-aria-props": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx": {
|
||||
"jsx-a11y/no-autofocus": {
|
||||
"count": 1
|
||||
|
||||
@ -162,6 +162,7 @@ html[data-theme="dark"] {
|
||||
--color-components-main-nav-glass-surface-middle-2: #0033ff1a;
|
||||
--color-components-main-nav-glass-surface-end: #0033ff14;
|
||||
--color-components-main-nav-glass-edge-highlight-first: #fffffffa;
|
||||
--color-components-main-nav-glass-edge-highlight-middle: #ffffff00;
|
||||
--color-components-main-nav-glass-edge-highlight-end: #ffffff6b;
|
||||
--color-components-main-nav-glass-edge-reflection-first: #0033ff00;
|
||||
--color-components-main-nav-glass-edge-reflection-middle: #0033ff99;
|
||||
|
||||
@ -162,6 +162,7 @@ html[data-theme="light"] {
|
||||
--color-components-main-nav-glass-surface-middle-2: #0033ff1a;
|
||||
--color-components-main-nav-glass-surface-end: #0033ff14;
|
||||
--color-components-main-nav-glass-edge-highlight-first: #fffffffa;
|
||||
--color-components-main-nav-glass-edge-highlight-middle: #ffffff00;
|
||||
--color-components-main-nav-glass-edge-highlight-end: #ffffff6b;
|
||||
--color-components-main-nav-glass-edge-reflection-first: #0033ff00;
|
||||
--color-components-main-nav-glass-edge-reflection-middle: #0033ff99;
|
||||
|
||||
@ -169,6 +169,7 @@
|
||||
--color-components-main-nav-glass-surface-middle-2: var(--color-components-main-nav-glass-surface-middle-2);
|
||||
--color-components-main-nav-glass-surface-end: var(--color-components-main-nav-glass-surface-end);
|
||||
--color-components-main-nav-glass-edge-highlight-first: var(--color-components-main-nav-glass-edge-highlight-first);
|
||||
--color-components-main-nav-glass-edge-highlight-middle: var(--color-components-main-nav-glass-edge-highlight-middle);
|
||||
--color-components-main-nav-glass-edge-highlight-end: var(--color-components-main-nav-glass-edge-highlight-end);
|
||||
--color-components-main-nav-glass-edge-reflection-first: var(--color-components-main-nav-glass-edge-reflection-first);
|
||||
--color-components-main-nav-glass-edge-reflection-middle: var(--color-components-main-nav-glass-edge-reflection-middle);
|
||||
|
||||
@ -248,7 +248,7 @@ describe('AccountDropdown', () => {
|
||||
fireEvent.click(screen.getByText('common.settings.preferences'))
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.LANGUAGE })
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })
|
||||
})
|
||||
|
||||
it('should show Appearance after Preferences in the main nav account dropdown', () => {
|
||||
|
||||
@ -127,7 +127,7 @@ export function MainNavMenuContent({
|
||||
</DropdownMenuLinkItem>
|
||||
<DropdownMenuItem
|
||||
className={mainNavMenuItemClassName}
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.LANGUAGE })}
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })}
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-equalizer-2-line"
|
||||
|
||||
@ -26,6 +26,7 @@ describe('AccountSetting Constants', () => {
|
||||
expect(ACCOUNT_SETTING_TAB.DATA_SOURCE).toBe('data-source')
|
||||
expect(ACCOUNT_SETTING_TAB.API_BASED_EXTENSION).toBe('custom-endpoint')
|
||||
expect(ACCOUNT_SETTING_TAB.CUSTOM).toBe('custom')
|
||||
expect(ACCOUNT_SETTING_TAB.PREFERENCES).toBe('preferences')
|
||||
expect(ACCOUNT_SETTING_TAB.LANGUAGE).toBe('language')
|
||||
})
|
||||
|
||||
@ -42,6 +43,7 @@ describe('AccountSetting Constants', () => {
|
||||
expect(isValidAccountSettingTab('data-source')).toBe(true)
|
||||
expect(isValidAccountSettingTab('custom-endpoint')).toBe(true)
|
||||
expect(isValidAccountSettingTab('custom')).toBe(true)
|
||||
expect(isValidAccountSettingTab('preferences')).toBe(true)
|
||||
expect(isValidAccountSettingTab('language')).toBe(true)
|
||||
})
|
||||
|
||||
@ -55,6 +57,7 @@ describe('AccountSetting Constants', () => {
|
||||
expect(isValidSettingsTab('permissions')).toBe(true)
|
||||
expect(isValidSettingsTab('access-rules')).toBe(true)
|
||||
expect(isValidSettingsTab('billing')).toBe(true)
|
||||
expect(isValidSettingsTab('preferences')).toBe(true)
|
||||
expect(isValidSettingsTab('language')).toBe(true)
|
||||
expect(isValidSettingsTab('provider')).toBe(true)
|
||||
expect(isValidSettingsTab('mcp')).toBe(true)
|
||||
|
||||
@ -257,6 +257,17 @@ describe('AccountSetting', () => {
|
||||
expect(screen.getByText('common.settings.dataSource'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should normalize legacy language tab entries to preferences', () => {
|
||||
// Act
|
||||
renderAccountSetting({ initialTab: ACCOUNT_SETTING_TAB.LANGUAGE })
|
||||
|
||||
// Assert
|
||||
const preferencesButton = screen.getByRole('button', { name: 'common.settings.preferences' })
|
||||
expect(preferencesButton.querySelector('.i-ri-equalizer-2-fill')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.account.general')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.account.appearanceLabel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide sidebar labels on mobile', () => {
|
||||
// Arrange
|
||||
vi.mocked(useBreakpoints).mockReturnValue(MediaType.mobile)
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { ACCOUNT_SETTING_TAB } from '../constants'
|
||||
import UpdateSettingDialogForm from '../update-setting-dialog-form'
|
||||
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContextSelector: (selector: (s: { setShowAccountSettingModal: typeof mockSetShowAccountSettingModal }) => typeof mockSetShowAccountSettingModal) => {
|
||||
return selector({ setShowAccountSettingModal: mockSetShowAccountSettingModal })
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: (defaultNs?: string) => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
const ns = (options?.ns as string | undefined) ?? defaultNs
|
||||
return `${ns ? `${ns}.` : ''}${key}`
|
||||
},
|
||||
i18n: {
|
||||
language: 'en',
|
||||
changeLanguage: vi.fn(),
|
||||
},
|
||||
}),
|
||||
Trans: ({ i18nKey, components }: {
|
||||
i18nKey: string
|
||||
components?: Record<string, React.ReactElement>
|
||||
}) => {
|
||||
const setTimezone = components?.setTimezone
|
||||
if (setTimezone)
|
||||
return React.cloneElement(setTimezone, undefined, i18nKey)
|
||||
|
||||
return <span>{i18nKey}</span>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/date-and-time-picker/time-picker', () => ({
|
||||
default: () => <div data-testid="time-picker" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker', () => ({
|
||||
default: () => <div data-testid="plugins-picker" />,
|
||||
}))
|
||||
|
||||
describe('UpdateSettingDialogForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should open preferences after closing the update setting dialog when timezone link is clicked', () => {
|
||||
const onRequestClose = vi.fn()
|
||||
|
||||
render(
|
||||
<UpdateSettingDialogForm
|
||||
autoUpgrade={{
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: AUTO_UPDATE_MODE.update_all,
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
}}
|
||||
category={PluginCategoryEnum.tool}
|
||||
plugins={[]}
|
||||
scopeOptions={[
|
||||
{ value: AUTO_UPDATE_MODE.update_all, label: 'All' },
|
||||
]}
|
||||
strategyOptions={[
|
||||
{ value: AUTO_UPDATE_STRATEGY.fixOnly, label: 'Fix only' },
|
||||
]}
|
||||
timezone="UTC"
|
||||
updateTimeValue="00:00"
|
||||
minuteFilter={minutes => minutes}
|
||||
onAutoUpgradeChange={vi.fn()}
|
||||
onPluginsChange={vi.fn()}
|
||||
onRequestClose={onRequestClose}
|
||||
onUpdateTimeChange={vi.fn()}
|
||||
renderTimePickerTrigger={() => <button type="button">Pick time</button>}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('autoUpdate.changeTimezone'))
|
||||
|
||||
expect(onRequestClose).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })
|
||||
})
|
||||
})
|
||||
@ -12,6 +12,7 @@ export const ACCOUNT_SETTING_TAB = {
|
||||
DATA_SOURCE: 'data-source',
|
||||
API_BASED_EXTENSION: 'custom-endpoint',
|
||||
CUSTOM: 'custom',
|
||||
PREFERENCES: 'preferences',
|
||||
LANGUAGE: 'language',
|
||||
} as const
|
||||
|
||||
@ -30,6 +31,7 @@ const WORKSPACE_SETTING_TAB_VALUES = [
|
||||
export type WorkspaceSettingTab = typeof WORKSPACE_SETTING_TAB_VALUES[number]
|
||||
|
||||
const USER_SETTING_TAB_VALUES = [
|
||||
ACCOUNT_SETTING_TAB.PREFERENCES,
|
||||
ACCOUNT_SETTING_TAB.LANGUAGE,
|
||||
] as const
|
||||
|
||||
|
||||
@ -20,11 +20,11 @@ import { BillingPermission, hasPermission } from '@/utils/permission'
|
||||
import AccessRulesPage from './access-rules-page'
|
||||
import { ApiBasedExtensionPage } from './api-based-extension-page'
|
||||
import DataSourcePage from './data-source-page-new'
|
||||
import LanguagePage from './language-page'
|
||||
import MembersPage from './members-page'
|
||||
import ModelProviderPage from './model-provider-page'
|
||||
import { useResetModelProviderListExpanded } from './model-provider-page/atoms'
|
||||
import PermissionsPage from './permissions-page'
|
||||
import PreferencePage from './preference-page'
|
||||
|
||||
const iconClassName = `
|
||||
w-4 h-4 mr-2
|
||||
@ -58,12 +58,14 @@ export default function AccountSetting({
|
||||
const isRbacEnabled = systemFeatures.rbac_enabled
|
||||
const canManageWorkspaceRoles = isRbacEnabled && hasPermission(workspacePermissionKeys, 'workspace.role.manage')
|
||||
const canViewBilling = enableBilling && hasPermission(workspacePermissionKeys, BillingPermission.View)
|
||||
// Keep legacy `language` deep links opening Preferences during the tab rename migration.
|
||||
const normalizedActiveTab = activeTab === ACCOUNT_SETTING_TAB.LANGUAGE ? ACCOUNT_SETTING_TAB.PREFERENCES : activeTab
|
||||
const activeMenu = (() => {
|
||||
if (activeTab === ACCOUNT_SETTING_TAB.BILLING && !canViewBilling)
|
||||
return ACCOUNT_SETTING_TAB.LANGUAGE
|
||||
if ((activeTab === ACCOUNT_SETTING_TAB.PERMISSIONS || activeTab === ACCOUNT_SETTING_TAB.ACCESS_RULES) && !canManageWorkspaceRoles)
|
||||
if (normalizedActiveTab === ACCOUNT_SETTING_TAB.BILLING && !canViewBilling)
|
||||
return ACCOUNT_SETTING_TAB.PREFERENCES
|
||||
if ((normalizedActiveTab === ACCOUNT_SETTING_TAB.PERMISSIONS || normalizedActiveTab === ACCOUNT_SETTING_TAB.ACCESS_RULES) && !canManageWorkspaceRoles)
|
||||
return ACCOUNT_SETTING_TAB.MEMBERS
|
||||
return activeTab
|
||||
return normalizedActiveTab
|
||||
})()
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@ -119,7 +121,7 @@ export default function AccountSetting({
|
||||
activeIcon: <span className={cn('i-ri-color-filter-fill', iconClassName)} />,
|
||||
},
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.LANGUAGE,
|
||||
key: ACCOUNT_SETTING_TAB.PREFERENCES,
|
||||
name: t('settings.preferences', { ns: 'common' }),
|
||||
title: t('account.general', { ns: 'common' }),
|
||||
icon: <span className={cn('i-ri-equalizer-2-line', iconClassName)} />,
|
||||
@ -151,7 +153,7 @@ export default function AccountSetting({
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const languageItem = settingItems.find(item => item.key === ACCOUNT_SETTING_TAB.LANGUAGE)
|
||||
const preferenceItem = settingItems.find(item => item.key === ACCOUNT_SETTING_TAB.PREFERENCES)
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
@ -161,7 +163,7 @@ export default function AccountSetting({
|
||||
},
|
||||
{
|
||||
key: 'user-group',
|
||||
items: languageItem ? [languageItem] : [],
|
||||
items: preferenceItem ? [preferenceItem] : [],
|
||||
},
|
||||
]
|
||||
|
||||
@ -266,7 +268,7 @@ export default function AccountSetting({
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.DATA_SOURCE && <DataSourcePage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.API_BASED_EXTENSION && <ApiBasedExtensionPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.LANGUAGE && <LanguagePage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.PREFERENCES && <PreferencePage />}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
@ -4,7 +4,7 @@ import { act, fireEvent, render, screen, waitFor, within } from '@testing-librar
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { timezones } from '@/utils/timezone'
|
||||
import LanguagePage from '../index'
|
||||
import PreferencePage from '../index'
|
||||
|
||||
const mockRefresh = vi.fn()
|
||||
const mockMutateUserProfile = vi.fn()
|
||||
@ -54,7 +54,7 @@ vi.mock('@langgenius/dify-ui/select', async () => {
|
||||
SelectItem: ({ children, value }: { children: React.ReactNode, value: string }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
return (
|
||||
<button type="button" role="option" onClick={() => context.onValueChange?.(value)}>
|
||||
<button type="button" role="option" aria-selected={false} onClick={() => context.onValueChange?.(value)}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
@ -104,7 +104,7 @@ const createUserProfile = (overrides: Partial<GetAccountProfileResponse> = {}):
|
||||
const renderPage = () => {
|
||||
render(
|
||||
<>
|
||||
<LanguagePage />
|
||||
<PreferencePage />
|
||||
<ToastHost />
|
||||
</>,
|
||||
)
|
||||
@ -150,7 +150,7 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
// Rendering
|
||||
describe('LanguagePage - Rendering', () => {
|
||||
describe('PreferencePage - Rendering', () => {
|
||||
it('should render default language and timezone labels', () => {
|
||||
const english = getLanguageOption('en-US')
|
||||
const niueTimezone = getTimezoneOption('Pacific/Niue')
|
||||
@ -182,7 +182,7 @@ describe('LanguagePage - Rendering', () => {
|
||||
})
|
||||
|
||||
// Interactions
|
||||
describe('LanguagePage - Interactions', () => {
|
||||
describe('PreferencePage - Interactions', () => {
|
||||
it('should show success toast when language updates', async () => {
|
||||
const chinese = getLanguageOption('zh-Hans')
|
||||
mockUserProfile = createUserProfile({ interface_language: 'en-US' })
|
||||
@ -33,7 +33,7 @@ const isThemeOption = (value: string): value is ThemeOption => {
|
||||
return (themes as readonly string[]).includes(value)
|
||||
}
|
||||
|
||||
export default function LanguagePage() {
|
||||
export default function PreferencePage() {
|
||||
const locale = useLocale()
|
||||
const { userProfile, mutateUserProfile } = useAppContext()
|
||||
const [editing, setEditing] = useState(false)
|
||||
@ -53,7 +53,7 @@ function SettingTimeZone({
|
||||
className="cursor-pointer border-none bg-transparent p-0 text-left body-xs-regular text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => {
|
||||
onRequestClose()
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.LANGUAGE })
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -23,7 +23,8 @@ import { AppModeEnum } from '@/types/app'
|
||||
import MainNav from '../index'
|
||||
import { DETAIL_SIDEBAR_STORAGE_KEY } from '../storage'
|
||||
|
||||
const activeEdgeClassName = 'before:pointer-events-none'
|
||||
const activeGradientMaskClassName = 'aria-[current=page]:main-nav-active-glass'
|
||||
const activeStackingClassName = 'aria-[current=page]:z-1'
|
||||
|
||||
const { mockIsAgentV2Enabled, mockSwitchWorkspace, mockToastSuccess, hotkeyRegistrations } = vi.hoisted(() => ({
|
||||
mockSwitchWorkspace: vi.fn(),
|
||||
@ -428,7 +429,7 @@ describe('MainNav', () => {
|
||||
expect(logoLink.parentElement).toHaveClass('pt-3', 'pr-2', 'pb-2', 'pl-4')
|
||||
|
||||
const homeLink = screen.getByRole('link', { name: /common.mainNav.home/ })
|
||||
expect(homeLink.closest('nav')).toHaveClass('flex', 'flex-col', 'gap-px', 'p-2')
|
||||
expect(homeLink.closest('nav')).toHaveClass('isolate', 'flex', 'flex-col', 'gap-px', 'p-2')
|
||||
expect(homeLink).toHaveClass('h-8', 'w-full', 'rounded-[10px]', 'px-2', 'py-1.5')
|
||||
|
||||
const webAppsButton = screen.getByRole('button', { name: 'explore.sidebar.webApps' })
|
||||
@ -566,8 +567,7 @@ describe('MainNav', () => {
|
||||
renderMainNav()
|
||||
|
||||
const datasetsLink = screen.getByRole('link', { name: /common.menus.datasets/ })
|
||||
expect(datasetsLink.className).toContain('bg-[linear-gradient(98.077deg')
|
||||
expect(datasetsLink).toHaveClass(activeEdgeClassName)
|
||||
expect(datasetsLink).toHaveClass(activeGradientMaskClassName)
|
||||
expect(datasetsLink).toHaveAttribute('aria-current', 'page')
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).not.toHaveAttribute('aria-current')
|
||||
})
|
||||
@ -578,7 +578,7 @@ describe('MainNav', () => {
|
||||
renderMainNav()
|
||||
|
||||
const studioLink = screen.getByRole('link', { name: /common.menus.apps/ })
|
||||
expect(studioLink).toHaveClass(activeEdgeClassName)
|
||||
expect(studioLink).toHaveClass(activeGradientMaskClassName)
|
||||
expect(studioLink).toHaveAttribute('aria-current', 'page')
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).not.toHaveAttribute('aria-current')
|
||||
})
|
||||
@ -854,7 +854,7 @@ describe('MainNav', () => {
|
||||
renderMainNav()
|
||||
|
||||
const marketplaceLink = screen.getByRole('link', { name: /common.mainNav.marketplace/ })
|
||||
expect(marketplaceLink).toHaveClass(activeEdgeClassName)
|
||||
expect(marketplaceLink).toHaveClass(activeGradientMaskClassName)
|
||||
})
|
||||
|
||||
it('marks roster active on roster routes', () => {
|
||||
@ -863,7 +863,7 @@ describe('MainNav', () => {
|
||||
renderMainNav()
|
||||
|
||||
const rosterLink = screen.getByRole('link', { name: /common.menus.roster/ })
|
||||
expect(rosterLink).toHaveClass(activeEdgeClassName)
|
||||
expect(rosterLink).toHaveClass(activeGradientMaskClassName)
|
||||
expect(rosterLink).toHaveAttribute('aria-current', 'page')
|
||||
})
|
||||
|
||||
@ -874,13 +874,8 @@ describe('MainNav', () => {
|
||||
|
||||
const homeLink = screen.getByRole('link', { name: /common.mainNav.home/ })
|
||||
|
||||
expect(homeLink).toHaveClass(
|
||||
'backdrop-blur-[5px]',
|
||||
'text-saas-dify-blue-inverted',
|
||||
activeEdgeClassName,
|
||||
'after:border-components-main-nav-glass-edge-highlight-first',
|
||||
)
|
||||
expect(homeLink.className).toContain('var(--color-components-main-nav-glass-surface-first)')
|
||||
expect(homeLink).toHaveClass(activeGradientMaskClassName)
|
||||
expect(homeLink).toHaveClass(activeStackingClassName)
|
||||
})
|
||||
|
||||
it('keeps Home active on the legacy explore apps route only', () => {
|
||||
|
||||
48
web/app/components/main-nav/components/nav-link.css
Normal file
48
web/app/components/main-nav/components/nav-link.css
Normal file
@ -0,0 +1,48 @@
|
||||
@utility main-nav-active-glass {
|
||||
@apply overflow-hidden system-md-semibold text-saas-dify-blue-inverted backdrop-blur-[5px];
|
||||
|
||||
background-image: linear-gradient(
|
||||
91.46deg,
|
||||
var(--color-components-main-nav-glass-surface-first, rgb(0 51 255 / 0.08)) 0%,
|
||||
var(--color-components-main-nav-glass-surface-middle-1, rgb(0 51 255 / 0.12)) 17.98%,
|
||||
var(--color-components-main-nav-glass-surface-middle-2, rgb(0 51 255 / 0.1)) 58.75%,
|
||||
var(--color-components-main-nav-glass-surface-end, rgb(0 51 255 / 0.08)) 101.09%
|
||||
);
|
||||
box-shadow:
|
||||
0 4px 8px 0 var(--color-components-main-nav-glass-shadow-reflection-glow),
|
||||
0 10px 12px -4px var(--color-shadow-shadow-4),
|
||||
0 3px 5px -2px var(--color-shadow-shadow-1),
|
||||
0 8px 16px -4px var(--color-components-main-nav-glass-shadow-reflection);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
border: 1px solid transparent;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
var(--color-components-main-nav-glass-edge-reflection-first, rgb(0 51 255 / 0)) 0%,
|
||||
var(--color-components-main-nav-glass-edge-reflection-middle, rgb(0 51 255 / 0.6)) 50%,
|
||||
var(--color-components-main-nav-glass-edge-reflection-end, rgb(0 51 255 / 0)) 100%
|
||||
) border-box;
|
||||
-webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: destination-out;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
border: 1px solid transparent;
|
||||
background: linear-gradient(180deg, var(--color-components-main-nav-glass-edge-highlight-first, rgb(255 255 255 / 0.98)) 0%, var(--color-components-main-nav-glass-edge-highlight-middle, rgb(255 255 255 / 0)) 18%, var(--color-components-main-nav-glass-edge-highlight-end, rgb(255 255 255 / 0.42)) 100%) border-box;
|
||||
-webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: destination-out;
|
||||
mask-composite: exclude;
|
||||
box-shadow: inset 0 0 8px 0 var(--color-components-main-nav-glass-inner-glow);
|
||||
}
|
||||
}
|
||||
@ -4,21 +4,6 @@ import type { MainNavItem } from '../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import Link from '@/next/link'
|
||||
|
||||
const navItemClassName = 'group relative flex h-8 w-full items-center gap-2 rounded-[10px] px-2 py-1.5 outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-state-accent-solid'
|
||||
|
||||
const activeNavItemClassName = cn(
|
||||
'overflow-hidden',
|
||||
'bg-[linear-gradient(98.077deg,var(--color-components-main-nav-glass-surface-first)_0%,var(--color-components-main-nav-glass-surface-middle-1)_17.98%,var(--color-components-main-nav-glass-surface-middle-2)_58.75%,var(--color-components-main-nav-glass-surface-end)_101.09%)]',
|
||||
'system-md-semibold text-saas-dify-blue-inverted backdrop-blur-[5px]',
|
||||
'shadow-[0px_4px_8px_0px_var(--color-components-main-nav-glass-shadow-reflection-glow),0px_12px_16px_-4px_var(--color-shadow-shadow-4),0px_4px_6px_-2px_var(--color-shadow-shadow-1),0px_10px_16px_-4px_var(--color-components-main-nav-glass-shadow-reflection)]',
|
||||
'before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:p-px before:content-[\'\']',
|
||||
'before:bg-[linear-gradient(var(--color-components-main-nav-glass-edge-highlight-first),var(--color-components-main-nav-glass-edge-highlight-first))_top/100%_1px_no-repeat,linear-gradient(var(--color-components-main-nav-glass-edge-highlight-end),var(--color-components-main-nav-glass-edge-highlight-end))_bottom/100%_1px_no-repeat,linear-gradient(180deg,var(--color-components-main-nav-glass-edge-reflection-first)_0%,var(--color-components-main-nav-glass-edge-reflection-middle)_50%,var(--color-components-main-nav-glass-edge-reflection-end)_100%)_left/1px_100%_no-repeat,linear-gradient(180deg,var(--color-components-main-nav-glass-edge-reflection-first)_0%,var(--color-components-main-nav-glass-edge-reflection-middle)_50%,var(--color-components-main-nav-glass-edge-reflection-end)_100%)_right/1px_100%_no-repeat]',
|
||||
'before:[mask-composite:exclude] before:[-webkit-mask-composite:xor] before:[-webkit-mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)]',
|
||||
'after:pointer-events-none after:absolute after:inset-[-1px] after:rounded-[inherit] after:border after:border-components-main-nav-glass-edge-highlight-first after:shadow-[inset_0_0_8px_0_var(--color-components-main-nav-glass-inner-glow)] after:content-[\'\']',
|
||||
)
|
||||
|
||||
const inactiveNavItemClassName = 'system-md-medium bg-components-main-nav-nav-button-bg text-components-main-nav-nav-button-text hover:bg-components-main-nav-nav-button-bg-hover hover:text-components-main-nav-nav-button-text'
|
||||
|
||||
const NavIcon = ({
|
||||
icon,
|
||||
className,
|
||||
@ -46,12 +31,14 @@ const MainNavLink = ({
|
||||
aria-current={activated ? 'page' : undefined}
|
||||
title={item.label}
|
||||
className={cn(
|
||||
navItemClassName,
|
||||
activated ? activeNavItemClassName : inactiveNavItemClassName,
|
||||
'group relative flex h-8 w-full items-center gap-2 rounded-[10px] px-2 py-1.5 outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset',
|
||||
'not-aria-[current=page]:bg-components-main-nav-nav-button-bg not-aria-[current=page]:system-md-medium not-aria-[current=page]:text-components-main-nav-nav-button-text not-aria-[current=page]:hover:bg-components-main-nav-nav-button-bg-hover not-aria-[current=page]:hover:text-components-main-nav-nav-button-text',
|
||||
'aria-[current=page]:main-nav-active-glass aria-[current=page]:z-1',
|
||||
)}
|
||||
>
|
||||
<NavIcon icon={activated ? item.activeIcon : item.icon} />
|
||||
<span className={cn('truncate', activated && 'text-shadow-[0px_0px_8px_var(--color-components-main-nav-glass-text-glow)]')}>{item.label}</span>
|
||||
<NavIcon icon={item.icon} className="group-aria-[current=page]:hidden" />
|
||||
<NavIcon icon={item.activeIcon} className="hidden group-aria-[current=page]:block" />
|
||||
<span className="truncate group-aria-[current=page]:text-shadow-[0px_0px_8px_var(--color-components-main-nav-glass-text-glow)]">{item.label}</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@ -297,7 +297,7 @@ const MainNav = ({
|
||||
? null
|
||||
: (
|
||||
<>
|
||||
<nav className="flex flex-col gap-px p-2">
|
||||
<nav className="isolate flex flex-col gap-px p-2">
|
||||
{navItems.map(item => (
|
||||
<MainNavLink key={item.href} item={item} pathname={pathname} />
|
||||
))}
|
||||
|
||||
@ -8,6 +8,7 @@ import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { createAccountProfileQueryClient } from '@/test/account-profile-query'
|
||||
import { PluginCategoryEnum, PluginSource } from '../../../types'
|
||||
import AutoUpdateSetting from '../index'
|
||||
@ -53,6 +54,32 @@ vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en-US',
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: (defaultNs?: string) => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
const ns = (options?.ns as string | undefined) ?? defaultNs
|
||||
const params = { ...options }
|
||||
delete params.ns
|
||||
const suffix = Object.keys(params).length > 0 ? `:${JSON.stringify(params)}` : ''
|
||||
return `${ns ? `${ns}.` : ''}${key}${suffix}`
|
||||
},
|
||||
i18n: {
|
||||
language: 'en',
|
||||
changeLanguage: vi.fn(),
|
||||
},
|
||||
}),
|
||||
Trans: ({ i18nKey, components }: {
|
||||
i18nKey: string
|
||||
components?: Record<string, React.ReactElement>
|
||||
}) => {
|
||||
const setTimezone = components?.setTimezone
|
||||
if (setTimezone)
|
||||
return React.cloneElement(setTimezone, undefined, i18nKey)
|
||||
|
||||
return <span>{i18nKey}</span>
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock plugins service
|
||||
const mockPluginsData: { plugins: PluginDetail[] } = { plugins: [] }
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
@ -1330,8 +1357,10 @@ describe('auto-update-setting', () => {
|
||||
|
||||
// Act
|
||||
render(<AutoUpdateSetting {...defaultProps} payload={payload} />)
|
||||
fireEvent.click(screen.getByText('autoUpdate.changeTimezone'))
|
||||
|
||||
expect(screen.getByText('autoUpdate.changeTimezone')).toBeInTheDocument()
|
||||
// Assert
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ const SettingTimeZone: FC<{
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 text-left body-xs-regular text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.LANGUAGE })}
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
@import '../components/base/action-button/index.css';
|
||||
@import '../components/base/badge/index.css';
|
||||
@import '../components/base/premium-badge/index.css';
|
||||
@import '../components/main-nav/components/nav-link.css';
|
||||
|
||||
/* ---------- JS plugins ------------------------------------------------ */
|
||||
@plugin './plugins/icons.ts';
|
||||
|
||||
@ -106,7 +106,7 @@ const PreferencesOpener = () => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.LANGUAGE })}
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })}
|
||||
>
|
||||
open preferences
|
||||
</button>
|
||||
@ -191,7 +191,7 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'open preferences' }))
|
||||
|
||||
expect(await screen.findByTestId('account-setting-active-tab')).toHaveTextContent(ACCOUNT_SETTING_TAB.LANGUAGE)
|
||||
expect(await screen.findByTestId('account-setting-active-tab')).toHaveTextContent(ACCOUNT_SETTING_TAB.PREFERENCES)
|
||||
})
|
||||
|
||||
it('relies on the in-memory guard when localStorage reads throw', async () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user