mirror of
https://github.com/langgenius/dify.git
synced 2026-06-24 13:01:16 +08:00
fix(web): restore contact us support menu (#37774)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
0cc27dd401
commit
ab11083c2d
@ -2610,11 +2610,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/zendesk/utils.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/billing/header-billing-btn/index.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
|
||||
@ -10,6 +10,7 @@ describe('zendesk/utils', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
// Clean up window.zE after each test
|
||||
window.zE = mockZE
|
||||
})
|
||||
@ -120,4 +121,40 @@ describe('zendesk/utils', () => {
|
||||
expect(window.zE).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('openZendeskWindow', () => {
|
||||
it('should show and open messenger when zE exists', async () => {
|
||||
vi.doMock('@/config', () => ({ IS_CE_EDITION: false }))
|
||||
const { openZendeskWindow } = await import('../utils')
|
||||
|
||||
openZendeskWindow()
|
||||
|
||||
expect(window.zE).toHaveBeenCalledWith('messenger', 'show')
|
||||
expect(window.zE).toHaveBeenCalledWith('messenger', 'open')
|
||||
})
|
||||
|
||||
it('should retry opening until zE is ready', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.doMock('@/config', () => ({ IS_CE_EDITION: false }))
|
||||
const { openZendeskWindow } = await import('../utils')
|
||||
|
||||
window.zE = undefined
|
||||
openZendeskWindow({ interval: 10, retries: 2 })
|
||||
window.zE = mockZE
|
||||
vi.advanceTimersByTime(10)
|
||||
|
||||
expect(window.zE).toHaveBeenCalledWith('messenger', 'show')
|
||||
expect(window.zE).toHaveBeenCalledWith('messenger', 'open')
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should not call window.zE when IS_CE_EDITION is true', async () => {
|
||||
vi.doMock('@/config', () => ({ IS_CE_EDITION: true }))
|
||||
const { openZendeskWindow } = await import('../utils')
|
||||
|
||||
openZendeskWindow()
|
||||
|
||||
expect(window.zE).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,7 +2,7 @@ import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
type ConversationField = {
|
||||
id: string
|
||||
value: any
|
||||
value: unknown
|
||||
}
|
||||
|
||||
declare global {
|
||||
@ -11,13 +11,13 @@ declare global {
|
||||
zE?: (
|
||||
command: string,
|
||||
value: string,
|
||||
payload?: ConversationField[] | string | string[] | (() => any),
|
||||
callback?: () => any,
|
||||
payload?: ConversationField[] | string | string[] | (() => unknown),
|
||||
callback?: () => unknown,
|
||||
) => void
|
||||
}
|
||||
}
|
||||
|
||||
export const setZendeskConversationFields = (fields: ConversationField[], callback?: () => any) => {
|
||||
export const setZendeskConversationFields = (fields: ConversationField[], callback?: () => unknown) => {
|
||||
if (!IS_CE_EDITION && window.zE)
|
||||
window.zE('messenger:set', 'conversationFields', fields, callback)
|
||||
}
|
||||
@ -31,3 +31,35 @@ export const toggleZendeskWindow = (open: boolean) => {
|
||||
if (!IS_CE_EDITION && window.zE)
|
||||
window.zE('messenger', open ? 'open' : 'close')
|
||||
}
|
||||
|
||||
type OpenZendeskWindowOptions = {
|
||||
interval?: number
|
||||
retries?: number
|
||||
}
|
||||
|
||||
const openZendeskWindowOnce = () => {
|
||||
if (IS_CE_EDITION || !window.zE)
|
||||
return false
|
||||
|
||||
window.zE('messenger', 'show')
|
||||
window.zE('messenger', 'open')
|
||||
return true
|
||||
}
|
||||
|
||||
export const openZendeskWindow = ({
|
||||
interval = 100,
|
||||
retries = 20,
|
||||
}: OpenZendeskWindowOptions = {}) => {
|
||||
if (IS_CE_EDITION)
|
||||
return
|
||||
|
||||
if (openZendeskWindowOnce())
|
||||
return
|
||||
|
||||
let attempts = 0
|
||||
const timer = window.setInterval(() => {
|
||||
attempts += 1
|
||||
if (openZendeskWindowOnce() || attempts >= retries)
|
||||
window.clearInterval(timer)
|
||||
}, interval)
|
||||
}
|
||||
|
||||
@ -194,6 +194,8 @@ vi.mock('@/config', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
IS_CLOUD_EDITION: true,
|
||||
SUPPORT_EMAIL_ADDRESS: '',
|
||||
ZENDESK_WIDGET_KEY: '',
|
||||
}
|
||||
})
|
||||
|
||||
@ -944,6 +946,20 @@ describe('MainNav', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('closes the help menu from the support upgrade action', async () => {
|
||||
renderMainNav()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' }))
|
||||
expect(await screen.findByText('common.userProfile.contactUs')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'billing.upgradeBtn.encourageShort' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('common.userProfile.forum')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('hides the help menu when branding is enabled', () => {
|
||||
renderMainNav({ branding: { enabled: true } })
|
||||
|
||||
|
||||
@ -1,30 +1,203 @@
|
||||
import type { Mock } from 'vitest'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { openZendeskWindow } from '@/app/components/base/zendesk/utils'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { mailToSupport } from '@/app/components/header/utils/util'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import SupportMenu from '../support-menu'
|
||||
|
||||
const { mockConfig, mockOpenZendeskWindow, mockMailToSupport, mockSetShowPricingModal } = vi.hoisted(() => ({
|
||||
mockConfig: {
|
||||
isCloudEdition: true,
|
||||
supportEmailAddress: '',
|
||||
zendeskWidgetKey: 'zendesk-key',
|
||||
},
|
||||
mockOpenZendeskWindow: vi.fn(),
|
||||
mockMailToSupport: vi.fn(),
|
||||
mockSetShowPricingModal: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/zendesk/utils', () => ({
|
||||
openZendeskWindow: mockOpenZendeskWindow,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/utils/util', () => ({
|
||||
mailToSupport: mockMailToSupport,
|
||||
}))
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return {
|
||||
...actual,
|
||||
get IS_CLOUD_EDITION() {
|
||||
return mockConfig.isCloudEdition
|
||||
},
|
||||
get SUPPORT_EMAIL_ADDRESS() {
|
||||
return mockConfig.supportEmailAddress
|
||||
},
|
||||
get ZENDESK_WIDGET_KEY() {
|
||||
return mockConfig.zendeskWidgetKey
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('SupportMenu', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockConfig.isCloudEdition = true
|
||||
mockConfig.supportEmailAddress = ''
|
||||
mockConfig.zendeskWidgetKey = 'zendesk-key'
|
||||
;(useAppContext as Mock).mockReturnValue({
|
||||
langGeniusVersionInfo: { current_version: '1.0.0' },
|
||||
userProfile: { email: 'user@example.com' },
|
||||
})
|
||||
;(useProviderContext as Mock).mockReturnValue({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.team },
|
||||
})
|
||||
;(useModalContext as Mock).mockReturnValue({
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
})
|
||||
;(mailToSupport as Mock).mockReturnValue('mailto:support@example.com')
|
||||
})
|
||||
|
||||
const renderSupportMenu = () => {
|
||||
const renderSupportMenu = (onContactUsClick = vi.fn()) => {
|
||||
return render(
|
||||
<DropdownMenu open={true} onOpenChange={() => { }}>
|
||||
<DropdownMenuTrigger>open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<SupportMenu />
|
||||
<SupportMenu onContactUsClick={onContactUsClick} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
}
|
||||
|
||||
it('renders support entries as flat main nav help menu items', () => {
|
||||
it('renders contact us before community support entries when Zendesk is configured', () => {
|
||||
const onContactUsClick = vi.fn()
|
||||
renderSupportMenu(onContactUsClick)
|
||||
|
||||
expect(screen.getByText('common.userProfile.contactUs')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.userProfile.forum')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.userProfile.community')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.userProfile.contactUs').compareDocumentPosition(screen.getByText('common.userProfile.forum'))).toBe(Node.DOCUMENT_POSITION_FOLLOWING)
|
||||
expect(screen.getByRole('menuitem', { name: 'common.userProfile.forum' })).toHaveClass('mx-0', 'px-3')
|
||||
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'common.userProfile.contactUs' }))
|
||||
|
||||
expect(openZendeskWindow).toHaveBeenCalled()
|
||||
expect(onContactUsClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders contact us with upgrade badge for Cloud sandbox plan without dedicated support', () => {
|
||||
;(useProviderContext as Mock).mockReturnValue({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
const onContactUsClick = vi.fn()
|
||||
renderSupportMenu(onContactUsClick)
|
||||
|
||||
expect(screen.getByText('common.userProfile.contactUs')).toHaveClass('text-text-disabled')
|
||||
expect(screen.getByText('billing.upgradeBtn.encourageShort')).toHaveClass('system-xs-semibold-uppercase', 'text-saas-dify-blue-accessible')
|
||||
expect(screen.queryByText('common.userProfile.emailSupport')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'common.userProfile.contactUs billing.upgradeBtn.encourageShort' }))
|
||||
|
||||
expect(mockSetShowPricingModal).not.toHaveBeenCalled()
|
||||
expect(onContactUsClick).not.toHaveBeenCalled()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'billing.upgradeBtn.encourageShort' }))
|
||||
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalled()
|
||||
expect(openZendeskWindow).not.toHaveBeenCalled()
|
||||
expect(onContactUsClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('hides upgrade contact for Cloud sandbox plan when billing is disabled', () => {
|
||||
;(useProviderContext as Mock).mockReturnValue({
|
||||
enableBilling: false,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
renderSupportMenu()
|
||||
|
||||
expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('common.userProfile.emailSupport')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('common.userProfile.forum')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.userProfile.community')).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: 'common.userProfile.forum' })).toHaveClass('mx-0', 'px-3')
|
||||
})
|
||||
|
||||
it('keeps Zendesk contact us for Cloud sandbox plan with support email and Zendesk configured', () => {
|
||||
mockConfig.supportEmailAddress = 'support@example.com'
|
||||
;(useProviderContext as Mock).mockReturnValue({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
renderSupportMenu()
|
||||
|
||||
expect(screen.getByText('common.userProfile.contactUs')).toBeInTheDocument()
|
||||
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'common.userProfile.contactUs' }))
|
||||
|
||||
expect(openZendeskWindow).toHaveBeenCalled()
|
||||
expect(mockSetShowPricingModal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps email support for Cloud sandbox plan with support email and no Zendesk configured', () => {
|
||||
mockConfig.supportEmailAddress = 'support@example.com'
|
||||
mockConfig.zendeskWidgetKey = ''
|
||||
;(useProviderContext as Mock).mockReturnValue({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
renderSupportMenu()
|
||||
|
||||
expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('common.userProfile.emailSupport')).toBeInTheDocument()
|
||||
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
|
||||
expect(mailToSupport).toHaveBeenCalledWith('user@example.com', Plan.sandbox, '1.0.0', 'support@example.com')
|
||||
})
|
||||
|
||||
it('hides dedicated support channels for non-Cloud sandbox plan without support email', () => {
|
||||
mockConfig.isCloudEdition = false
|
||||
;(useProviderContext as Mock).mockReturnValue({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
renderSupportMenu()
|
||||
|
||||
expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('common.userProfile.emailSupport')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('common.userProfile.forum')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders email support when Zendesk is not configured for a dedicated support channel', () => {
|
||||
mockConfig.zendeskWidgetKey = ''
|
||||
|
||||
renderSupportMenu()
|
||||
|
||||
expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('common.userProfile.emailSupport')).toBeInTheDocument()
|
||||
expect(mailToSupport).toHaveBeenCalledWith('user@example.com', Plan.team, '1.0.0', '')
|
||||
expect(screen.getByRole('menuitem', { name: 'common.userProfile.emailSupport' })).toHaveAttribute('href', 'mailto:support@example.com')
|
||||
})
|
||||
|
||||
it('has correct forum and community links', () => {
|
||||
|
||||
@ -124,7 +124,7 @@ const HelpMenu = ({
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="my-0!" />
|
||||
<DropdownMenuGroup className="p-1">
|
||||
<SupportMenu />
|
||||
<SupportMenu onContactUsClick={() => setOpen(false)} />
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="my-0!" />
|
||||
<DropdownMenuGroup className="p-1">
|
||||
|
||||
@ -1,12 +1,87 @@
|
||||
import { DropdownMenuLinkItem } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { DropdownMenuItem, DropdownMenuLinkItem } from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { openZendeskWindow } from '@/app/components/base/zendesk/utils'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ExternalLinkIndicator, MenuItemContent } from '@/app/components/header/account-dropdown/menu-item-content'
|
||||
import { mailToSupport } from '@/app/components/header/utils/util'
|
||||
import { IS_CLOUD_EDITION, SUPPORT_EMAIL_ADDRESS, ZENDESK_WIDGET_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
export default function SupportMenu() {
|
||||
type SupportMenuProps = {
|
||||
onContactUsClick?: () => void
|
||||
}
|
||||
|
||||
export default function SupportMenu({ onContactUsClick }: SupportMenuProps) {
|
||||
const { t } = useTranslation()
|
||||
const { enableBilling, plan } = useProviderContext()
|
||||
const { userProfile, langGeniusVersionInfo } = useAppContext()
|
||||
const { setShowPricingModal } = useModalContext()
|
||||
const hasDedicatedChannel = plan.type !== Plan.sandbox || Boolean(SUPPORT_EMAIL_ADDRESS.trim())
|
||||
const shouldShowUpgradeContact = IS_CLOUD_EDITION && enableBilling && plan.type === Plan.sandbox && !hasDedicatedChannel
|
||||
const hasZendeskWidget = Boolean(ZENDESK_WIDGET_KEY.trim())
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldShowUpgradeContact && (
|
||||
<DropdownMenuItem
|
||||
className="mx-0 h-8 cursor-default gap-1 px-3 py-1"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-chat-smile-2-line text-text-disabled"
|
||||
label={(
|
||||
<span className="text-text-disabled">
|
||||
{t('userProfile.contactUs', { ns: 'common' })}
|
||||
</span>
|
||||
)}
|
||||
trailing={(
|
||||
<button
|
||||
type="button"
|
||||
className="max-w-30 shrink-0 truncate px-1 system-xs-semibold-uppercase text-saas-dify-blue-accessible transition-colors hover:text-saas-dify-blue-static-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden focus-visible:ring-inset"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setShowPricingModal()
|
||||
onContactUsClick?.()
|
||||
}}
|
||||
>
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!shouldShowUpgradeContact && hasDedicatedChannel && hasZendeskWidget && (
|
||||
<DropdownMenuItem
|
||||
className="mx-0 h-8 gap-1 px-3 py-1"
|
||||
onClick={() => {
|
||||
openZendeskWindow()
|
||||
onContactUsClick?.()
|
||||
}}
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-chat-smile-2-line"
|
||||
label={t('userProfile.contactUs', { ns: 'common' })}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!shouldShowUpgradeContact && hasDedicatedChannel && !hasZendeskWidget && (
|
||||
<DropdownMenuLinkItem
|
||||
className="mx-0 h-8 gap-1 px-3 py-1"
|
||||
href={mailToSupport(userProfile.email, plan.type, langGeniusVersionInfo?.current_version, SUPPORT_EMAIL_ADDRESS)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-mail-send-line"
|
||||
label={t('userProfile.emailSupport', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuLinkItem>
|
||||
)}
|
||||
<DropdownMenuLinkItem
|
||||
className="mx-0 h-8 gap-1 px-3 py-1"
|
||||
href="https://forum.dify.ai/"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user