fix(web): preserve settings fallbacks during main nav update

- hide migrated settings tabs from the account settings sidebar

- add disabled integrations destination mapping for future migration

- keep legacy settings modal fallback until integrations sections are ready

- restore main nav active styling and add titles for truncated labels
This commit is contained in:
Jingyi-Dify 2026-05-07 15:06:46 -07:00
parent cacfb7e544
commit eed92f1bd1
13 changed files with 169 additions and 118 deletions

View File

@ -140,6 +140,7 @@ function ComplianceDocRowItem({
[Plan.team]: '',
[Plan.enterprise]: '',
}
const labelTitle = typeof label === 'string' ? label : undefined
return (
<DropdownMenuItem
@ -148,7 +149,7 @@ function ComplianceDocRowItem({
onClick={handleSelect}
>
{icon}
<div className="grow truncate px-1 system-md-regular text-text-secondary">{label}</div>
<div className="grow truncate px-1 system-md-regular text-text-secondary" title={labelTitle}>{label}</div>
<ComplianceDocActionVisual
isCurrentPlanCanDownload={isCurrentPlanCanDownload}
isPending={isPending}

View File

@ -44,10 +44,12 @@ function MainNavRadioItemContent({
iconClassName,
label,
}: MainNavRadioItemContentProps) {
const labelTitle = typeof label === 'string' ? label : undefined
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>
<span className="min-w-0 grow truncate px-1 system-md-regular text-text-secondary" title={labelTitle}>{label}</span>
<DropdownMenuRadioItemIndicator />
</>
)
@ -205,10 +207,10 @@ export function MainNavMenuContent({
<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>
<div className="max-w-[80px] min-w-0 truncate body-md-medium text-text-primary" title={userProfile.name}>{userProfile.name}</div>
{mainNavBadge}
</div>
<div className="truncate system-xs-regular text-text-tertiary">{userProfile.email}</div>
<div className="truncate system-xs-regular text-text-tertiary" title={userProfile.email}>{userProfile.email}</div>
</div>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
</div>

View File

@ -17,10 +17,12 @@ export function MenuItemContent({
label,
trailing,
}: MenuItemContentProps) {
const labelTitle = typeof label === 'string' ? label : undefined
return (
<>
<span aria-hidden className={cn(menuLeadingIconClassName, iconClassName)} />
<div className={menuLabelClassName}>{label}</div>
<div className={menuLabelClassName} title={labelTitle}>{label}</div>
{trailing}
</>
)

View File

@ -97,7 +97,7 @@ const WorkplaceSelector = () => {
{currentWorkspace?.name[0]?.toLocaleUpperCase()}
</span>
</div>
<div className="max-w-[149px] min-w-0 truncate system-sm-medium text-text-secondary max-[800px]:hidden">
<div className="max-w-[149px] min-w-0 truncate system-sm-medium text-text-secondary max-[800px]:hidden" title={currentWorkspace?.name}>
{currentWorkspace?.name}
</div>
</div>

View File

@ -4,6 +4,11 @@ import {
DEFAULT_ACCOUNT_SETTING_TAB,
isValidAccountSettingTab,
} from '../constants'
import {
enableMovedAccountSettingDestinations,
getMovedAccountSettingDestination,
movedAccountSettingDestinations,
} from '../destinations'
describe('AccountSetting Constants', () => {
it('should have correct ACCOUNT_SETTING_MODAL_ACTION', () => {
@ -39,4 +44,15 @@ describe('AccountSetting Constants', () => {
expect(isValidAccountSettingTab('')).toBe(false)
expect(isValidAccountSettingTab('invalid')).toBe(false)
})
it('should keep migrated setting destinations disabled until integrations sections are ready', () => {
expect(enableMovedAccountSettingDestinations).toBe(false)
expect(movedAccountSettingDestinations[ACCOUNT_SETTING_TAB.PROVIDER]).toBe('/tools?section=provider')
expect(movedAccountSettingDestinations[ACCOUNT_SETTING_TAB.DATA_SOURCE]).toBe('/tools?section=data-source')
expect(movedAccountSettingDestinations[ACCOUNT_SETTING_TAB.API_BASED_EXTENSION]).toBe('/tools?section=api-based-extension')
expect(getMovedAccountSettingDestination(ACCOUNT_SETTING_TAB.PROVIDER)).toBeUndefined()
expect(getMovedAccountSettingDestination(ACCOUNT_SETTING_TAB.DATA_SOURCE)).toBeUndefined()
expect(getMovedAccountSettingDestination(ACCOUNT_SETTING_TAB.API_BASED_EXTENSION)).toBeUndefined()
expect(getMovedAccountSettingDestination(ACCOUNT_SETTING_TAB.BILLING)).toBeUndefined()
})
})

View File

@ -175,24 +175,21 @@ describe('AccountSetting', () => {
// Assert
// Assert
expect(screen.getByText('common.userProfile.settings'))!.toBeInTheDocument()
expect(screen.getByText('common.settings.provider'))!.toBeInTheDocument()
expect(screen.queryByText('common.settings.provider'))!.not.toBeInTheDocument()
expect(screen.getAllByText('common.settings.members').length).toBeGreaterThan(0)
expect(screen.getByText('common.settings.billing'))!.toBeInTheDocument()
expect(screen.getByText('common.settings.dataSource'))!.toBeInTheDocument()
expect(screen.getByText('common.settings.apiBasedExtension'))!.toBeInTheDocument()
expect(screen.queryByText('common.settings.dataSource'))!.not.toBeInTheDocument()
expect(screen.queryByText('common.settings.apiBasedExtension'))!.not.toBeInTheDocument()
expect(screen.getByText('custom.custom'))!.toBeInTheDocument()
expect(screen.getAllByText('common.settings.language').length).toBeGreaterThan(0)
expect(screen.queryByText('common.settings.language'))!.not.toBeInTheDocument()
})
it('should respect the initial tab', () => {
it('should keep hidden legacy tab metadata for direct entries', () => {
// Act
renderAccountSetting({ initialTab: ACCOUNT_SETTING_TAB.DATA_SOURCE })
// Assert
// Check that the active item title is Data Source
const titles = screen.getAllByText('common.settings.dataSource')
// One in sidebar, one in header.
expect(titles.length).toBeGreaterThan(1)
expect(screen.getByText('common.settings.dataSource'))!.toBeInTheDocument()
})
it('should hide sidebar labels on mobile', () => {
@ -312,8 +309,8 @@ describe('AccountSetting', () => {
// Assert
// Assert
expect(screen.queryByText('common.settings.provider')).not.toBeInTheDocument()
expect(screen.queryByText('common.settings.members')).not.toBeInTheDocument()
expect(screen.getByText('common.settings.language'))!.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'common.settings.members' })).not.toBeInTheDocument()
expect(screen.queryByText('common.settings.language'))!.not.toBeInTheDocument()
})
it('should hide billing and custom tabs when disabled', () => {
@ -370,13 +367,11 @@ describe('AccountSetting', () => {
renderAccountSetting({ onTabChange: mockOnTabChange })
// Act
fireEvent.click(screen.getByText('common.settings.provider'))
fireEvent.click(screen.getByText('common.settings.billing'))
// Assert
expect(mockOnTabChange).toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.PROVIDER)
// Check for content from ModelProviderPage
// Check for content from ModelProviderPage
expect(screen.getByText('common.modelProvider.models'))!.toBeInTheDocument()
expect(mockOnTabChange).toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.BILLING)
expect(screen.getAllByText('common.settings.billing').length).toBeGreaterThan(1)
})
it('should navigate through various tabs and show correct details', () => {
@ -389,23 +384,11 @@ describe('AccountSetting', () => {
// Checking for title in header which is always there
expect(screen.getAllByText('common.settings.billing').length).toBeGreaterThan(1)
// Data Source
fireEvent.click(screen.getByText('common.settings.dataSource'))
expect(screen.getAllByText('common.settings.dataSource').length).toBeGreaterThan(1)
// API Based Extension
fireEvent.click(screen.getByText('common.settings.apiBasedExtension'))
expect(screen.getAllByText('common.settings.apiBasedExtension').length).toBeGreaterThan(1)
// Custom
fireEvent.click(screen.getByText('custom.custom'))
// Custom Page uses 'custom.custom' key as well.
expect(screen.getAllByText('custom.custom').length).toBeGreaterThan(1)
// Language
fireEvent.click(screen.getAllByText('common.settings.language')[0]!)
expect(screen.getAllByText('common.settings.language').length).toBeGreaterThan(1)
// Members
fireEvent.click(screen.getAllByText('common.settings.members')[0]!)
expect(screen.getAllByText('common.settings.members').length).toBeGreaterThan(1)

View File

@ -0,0 +1,17 @@
import type { AccountSettingTab } from './constants'
import { ACCOUNT_SETTING_TAB } from './constants'
export const movedAccountSettingDestinations: Partial<Record<AccountSettingTab, string>> = {
[ACCOUNT_SETTING_TAB.PROVIDER]: '/tools?section=provider',
[ACCOUNT_SETTING_TAB.DATA_SOURCE]: '/tools?section=data-source',
[ACCOUNT_SETTING_TAB.API_BASED_EXTENSION]: '/tools?section=api-based-extension',
}
export const enableMovedAccountSettingDestinations = false
export const getMovedAccountSettingDestination = (tab: AccountSettingTab) => {
if (!enableMovedAccountSettingDestinations)
return undefined
return movedAccountSettingDestinations[tab]
}

View File

@ -52,60 +52,70 @@ export default function AccountSetting({
const { enableBilling, enableReplaceWebAppLogo } = useProviderContext()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const workplaceGroupItems: GroupItem[] = (() => {
const settingItems: GroupItem[] = [
{
key: ACCOUNT_SETTING_TAB.PROVIDER,
name: t('settings.provider', { ns: 'common' }),
icon: <span className={cn('i-ri-brain-2-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-brain-2-fill', iconClassName)} />,
},
{
key: ACCOUNT_SETTING_TAB.MEMBERS,
name: t('settings.members', { ns: 'common' }),
icon: <span className={cn('i-ri-group-2-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-group-2-fill', iconClassName)} />,
},
{
key: ACCOUNT_SETTING_TAB.BILLING,
name: t('settings.billing', { ns: 'common' }),
description: t('plansCommon.receiptInfo', { ns: 'billing' }),
icon: <span className={cn('i-ri-money-dollar-circle-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-money-dollar-circle-fill', iconClassName)} />,
},
{
key: ACCOUNT_SETTING_TAB.DATA_SOURCE,
name: t('settings.dataSource', { ns: 'common' }),
icon: <span className={cn('i-ri-database-2-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-database-2-fill', iconClassName)} />,
},
{
key: ACCOUNT_SETTING_TAB.API_BASED_EXTENSION,
name: t('settings.apiBasedExtension', { ns: 'common' }),
icon: <span className={cn('i-ri-puzzle-2-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-puzzle-2-fill', iconClassName)} />,
},
{
key: ACCOUNT_SETTING_TAB.CUSTOM,
name: t('custom', { ns: 'custom' }),
icon: <span className={cn('i-ri-color-filter-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-color-filter-fill', iconClassName)} />,
},
{
key: ACCOUNT_SETTING_TAB.LANGUAGE,
name: t('settings.language', { ns: 'common' }),
icon: <span className={cn('i-ri-translate-2', iconClassName)} />,
activeIcon: <span className={cn('i-ri-translate-2', iconClassName)} />,
},
]
const activeItem = settingItems.find(item => item.key === activeMenu)
const visibleSettingItems: GroupItem[] = (() => {
if (isCurrentWorkspaceDatasetOperator)
return []
const items: GroupItem[] = [
{
key: ACCOUNT_SETTING_TAB.PROVIDER,
name: t('settings.provider', { ns: 'common' }),
icon: <span className={cn('i-ri-brain-2-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-brain-2-fill', iconClassName)} />,
},
{
key: ACCOUNT_SETTING_TAB.MEMBERS,
name: t('settings.members', { ns: 'common' }),
icon: <span className={cn('i-ri-group-2-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-group-2-fill', iconClassName)} />,
},
]
const visibleTabs: AccountSettingTab[] = []
if (enableBilling) {
items.push({
key: ACCOUNT_SETTING_TAB.BILLING,
name: t('settings.billing', { ns: 'common' }),
description: t('plansCommon.receiptInfo', { ns: 'billing' }),
icon: <span className={cn('i-ri-money-dollar-circle-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-money-dollar-circle-fill', iconClassName)} />,
})
}
if (enableBilling)
visibleTabs.push(ACCOUNT_SETTING_TAB.BILLING)
items.push(
{
key: ACCOUNT_SETTING_TAB.DATA_SOURCE,
name: t('settings.dataSource', { ns: 'common' }),
icon: <span className={cn('i-ri-database-2-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-database-2-fill', iconClassName)} />,
},
{
key: ACCOUNT_SETTING_TAB.API_BASED_EXTENSION,
name: t('settings.apiBasedExtension', { ns: 'common' }),
icon: <span className={cn('i-ri-puzzle-2-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-puzzle-2-fill', iconClassName)} />,
},
)
visibleTabs.push(ACCOUNT_SETTING_TAB.MEMBERS)
if (enableReplaceWebAppLogo || enableBilling) {
items.push({
key: ACCOUNT_SETTING_TAB.CUSTOM,
name: t('custom', { ns: 'custom' }),
icon: <span className={cn('i-ri-color-filter-line', iconClassName)} />,
activeIcon: <span className={cn('i-ri-color-filter-fill', iconClassName)} />,
})
}
if (enableReplaceWebAppLogo || enableBilling)
visibleTabs.push(ACCOUNT_SETTING_TAB.CUSTOM)
return items
return visibleTabs
.map(tab => settingItems.find(item => item.key === tab))
.filter((item): item is GroupItem => Boolean(item))
})()
const media = useBreakpoints()
@ -115,22 +125,9 @@ export default function AccountSetting({
{
key: 'workspace-group',
name: t('settings.workplaceGroup', { ns: 'common' }),
items: workplaceGroupItems,
},
{
key: 'account-group',
name: t('settings.generalGroup', { ns: 'common' }),
items: [
{
key: ACCOUNT_SETTING_TAB.LANGUAGE,
name: t('settings.language', { ns: 'common' }),
icon: <span className={cn('i-ri-translate-2', iconClassName)} />,
activeIcon: <span className={cn('i-ri-translate-2', iconClassName)} />,
},
],
items: visibleSettingItems,
},
]
const activeItem = [...menuItems[0]!.items, ...menuItems[1]!.items].find(item => item.key === activeMenu)
const [searchValue, setSearchValue] = useState<string>('')
@ -215,7 +212,7 @@ export default function AccountSetting({
<div className="mt-1 system-sm-regular text-text-tertiary">{activeItem?.description}</div>
)}
</div>
{activeItem?.key === ACCOUNT_SETTING_TAB.PROVIDER && (
{activeMenu === ACCOUNT_SETTING_TAB.PROVIDER && (
<div className="flex grow justify-end">
<SearchInput
className="w-[200px]"

View File

@ -222,8 +222,9 @@ describe('MainNav', () => {
expect(homeLink).toHaveClass(
'border-transparent',
'backdrop-blur-[5px]',
'text-saas-dify-blue-inverted',
activeEdgeClassName,
'after:border-components-main-nav-glass-edge-highlight-first',
'after:border-[rgba(255,255,255,0.98)]',
)
expect(homeLink.className).toContain('bg-[linear-gradient(98.077deg')
})

View File

@ -24,7 +24,8 @@ const AccountSection = ({
<button
type="button"
aria-label={ariaLabel}
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')}
title={userProfile.name}
className={cn('text-components-main-nav-text flex max-w-[180px] min-w-0 shrink items-center gap-3 rounded-full py-1 pr-4 pl-1 text-left 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="min-w-0 flex-1 truncate system-md-medium">{userProfile.name}</span>

View File

@ -8,16 +8,16 @@ const navItemClassName = 'group relative flex h-9 items-center gap-2 rounded-xl
const activeNavItemClassName = cn(
'overflow-hidden border border-transparent',
'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-components-main-nav-text-active 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-5),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: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:[content:""] 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:""]',
'bg-[linear-gradient(98.077deg,rgba(0,51,255,0.08)_0%,rgba(0,51,255,0.12)_17.98%,rgba(0,51,255,0.1)_58.75%,rgba(0,51,255,0.08)_101.09%)]',
'system-md-semibold text-saas-dify-blue-inverted backdrop-blur-[5px]',
'shadow-[0px_4px_8px_0px_rgba(255,255,255,0),0px_12px_16px_-4px_rgba(9,9,11,0.08),0px_4px_6px_-2px_rgba(9,9,11,0.03),0px_10px_16px_-4px_rgba(0,51,255,0.06)]',
'before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:p-px before:content-[\'\']',
'before:bg-[linear-gradient(rgba(255,255,255,0.98),rgba(255,255,255,0.98))_top/100%_1px_no-repeat,linear-gradient(rgba(255,255,255,0.42),rgba(255,255,255,0.42))_bottom/100%_1px_no-repeat,linear-gradient(180deg,rgba(0,51,255,0)_0%,rgba(0,51,255,0.6)_50%,rgba(0,51,255,0)_100%)_left/1px_100%_no-repeat,linear-gradient(180deg,rgba(0,51,255,0)_0%,rgba(0,51,255,0.6)_50%,rgba(0,51,255,0)_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-[rgba(255,255,255,0.98)] after:shadow-[inset_0_0_8px_0_rgba(255,255,255,0.3)] after:content-[\'\']',
)
const inactiveNavItemClassName = 'system-md-medium bg-components-main-nav-nav-button-bg text-components-main-nav-text hover:bg-state-base-hover hover:text-components-main-nav-text'
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,
@ -43,13 +43,14 @@ const MainNavLink = ({
return (
<Link
href={item.href}
title={item.label}
className={cn(
navItemClassName,
activated ? activeNavItemClassName : inactiveNavItemClassName,
)}
>
<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>
<span className={cn('truncate', activated && 'text-shadow-[0px_0px_8px_rgba(49,70,255,0.18)]')}>{item.label}</span>
</Link>
)
}

View File

@ -36,13 +36,17 @@ const WorkspaceMenuItemContent = ({
icon: ReactNode
label: ReactNode
trailing?: ReactNode
}) => (
<>
<span className="flex h-4 w-4 shrink-0 items-center justify-center text-text-tertiary">{icon}</span>
<span className="min-w-0 grow truncate text-left system-md-regular text-text-secondary">{label}</span>
{trailing}
</>
)
}) => {
const labelTitle = typeof label === 'string' ? label : undefined
return (
<>
<span className="flex h-4 w-4 shrink-0 items-center justify-center text-text-tertiary">{icon}</span>
<span className="min-w-0 grow truncate text-left system-md-regular text-text-secondary" title={labelTitle}>{label}</span>
{trailing}
</>
)
}
const WorkspaceCard = () => {
const { t } = useTranslation()
@ -103,7 +107,7 @@ const WorkspaceCard = () => {
<WorkspaceIcon name={currentWorkspace.name} className="h-6 w-6 rounded-lg" />
<div className="min-w-0 grow">
<div className="flex min-w-0 items-center gap-1.5">
<span className="max-w-[120px] truncate system-sm-medium text-text-primary">{currentWorkspace.name}</span>
<span className="max-w-[120px] truncate system-sm-medium text-text-primary" title={currentWorkspace.name}>{currentWorkspace.name}</span>
<WorkspacePlanBadge plan={workspacePlan} />
</div>
</div>
@ -120,13 +124,14 @@ const WorkspaceCard = () => {
}}
>
<span className="i-custom-vender-main-nav-credits h-3 w-3 shrink-0" aria-hidden />
<span className="truncate system-xs-medium">{formattedCredits}</span>
<span className="truncate system-xs-medium" title={formattedCredits}>{formattedCredits}</span>
<span className="shrink-0 system-xs-regular">{t('mainNav.workspace.creditsUnit', { ns: 'common' })}</span>
</button>
{enableBilling && (
<button
type="button"
className="max-w-[120px] shrink-0 truncate px-1 system-xs-semibold-uppercase text-saas-dify-blue-accessible transition-colors hover:text-saas-dify-blue-static-hover"
title={t('upgradeBtn.encourageShort', { ns: 'billing' })}
onClick={(e) => {
e.stopPropagation()
handlePlanClick()
@ -148,7 +153,7 @@ const WorkspaceCard = () => {
onClick={() => setOpen(false)}
>
<div className="flex min-w-0 grow flex-col items-start justify-center gap-1">
<div className="max-w-[120px] shrink-0 truncate system-xl-medium leading-5 text-text-primary">{currentWorkspace.name}</div>
<div className="max-w-[120px] shrink-0 truncate system-xl-medium leading-5 text-text-primary" title={currentWorkspace.name}>{currentWorkspace.name}</div>
<WorkspacePlanBadge plan={workspacePlan} />
</div>
<WorkspaceIcon name={currentWorkspace.name} className="h-9 w-9" />
@ -183,6 +188,7 @@ const WorkspaceCard = () => {
<button
type="button"
key={workspace.id}
title={workspace.name}
className={cn(
'flex h-8 w-full items-center gap-2 rounded-lg px-3 py-1 text-left transition-colors hover:bg-state-base-hover',
workspace.current && 'text-text-secondary',

View File

@ -17,6 +17,7 @@ import {
DEFAULT_ACCOUNT_SETTING_TAB,
isValidAccountSettingTab,
} from '@/app/components/header/account-setting/constants'
import { getMovedAccountSettingDestination } from '@/app/components/header/account-setting/destinations'
import {
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
} from '@/app/education-apply/constants'
@ -27,6 +28,7 @@ import {
usePricingModal,
} from '@/hooks/use-query-params'
import dynamic from '@/next/dynamic'
import { useRouter } from '@/next/navigation'
import { useTriggerEventsLimitModal } from './hooks/use-trigger-events-limit-modal'
import {
ModalContext,
@ -82,6 +84,7 @@ export const ModalContextProvider = ({
// Use nuqs hooks for URL-based modal state management
const [showPricingModal, setPricingModalOpen] = usePricingModal()
const [urlAccountModalState, setUrlAccountModalState] = useAccountSettingModal()
const router = useRouter()
const accountSettingCallbacksRef = useRef<Omit<ModalState<AccountSettingTab>, 'payload'> | null>(null)
const accountSettingTab = urlAccountModalState.isOpen
@ -131,15 +134,36 @@ export const ModalContextProvider = ({
return
}
const { payload, ...callbacks } = resolvedState
const movedDestination = getMovedAccountSettingDestination(payload)
if (movedDestination) {
accountSettingCallbacksRef.current = null
setUrlAccountModalState(null)
router.push(movedDestination)
return
}
accountSettingCallbacksRef.current = callbacks
setUrlAccountModalState({ payload })
}, [accountSettingTab, setUrlAccountModalState])
}, [accountSettingTab, router, setUrlAccountModalState])
useEffect(() => {
if (!urlAccountModalState.isOpen)
accountSettingCallbacksRef.current = null
}, [urlAccountModalState.isOpen])
useEffect(() => {
if (!accountSettingTab)
return
const movedDestination = getMovedAccountSettingDestination(accountSettingTab)
if (!movedDestination)
return
accountSettingCallbacksRef.current = null
setUrlAccountModalState(null)
router.push(movedDestination)
}, [accountSettingTab, router, setUrlAccountModalState])
const { plan, isFetchedPlan } = useProviderContext()
const {
showTriggerEventsLimitModal,