chore: add support email env (#33075)

This commit is contained in:
Nite Knite
2026-03-06 14:29:29 +08:00
committed by GitHub
parent dc31b07533
commit 0490756ab2
6 changed files with 45 additions and 8 deletions

View File

@@ -70,6 +70,7 @@ const { mockConfig, mockEnv } = vi.hoisted(() => ({
mockConfig: { mockConfig: {
IS_CLOUD_EDITION: false, IS_CLOUD_EDITION: false,
ZENDESK_WIDGET_KEY: '', ZENDESK_WIDGET_KEY: '',
SUPPORT_EMAIL_ADDRESS: '',
}, },
mockEnv: { mockEnv: {
env: { env: {
@@ -80,6 +81,7 @@ const { mockConfig, mockEnv } = vi.hoisted(() => ({
vi.mock('@/config', () => ({ vi.mock('@/config', () => ({
get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION }, get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION },
get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY }, get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY },
get SUPPORT_EMAIL_ADDRESS() { return mockConfig.SUPPORT_EMAIL_ADDRESS },
IS_DEV: false, IS_DEV: false,
IS_CE_EDITION: false, IS_CE_EDITION: false,
})) }))

View File

@@ -11,6 +11,10 @@ const { mockZendeskKey } = vi.hoisted(() => ({
mockZendeskKey: { value: 'test-key' }, mockZendeskKey: { value: 'test-key' },
})) }))
const { mockSupportEmailKey } = vi.hoisted(() => ({
mockSupportEmailKey: { value: '' },
}))
vi.mock('@/context/app-context', async (importOriginal) => { vi.mock('@/context/app-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/app-context')>() const actual = await importOriginal<typeof import('@/context/app-context')>()
return { return {
@@ -33,6 +37,7 @@ vi.mock('@/config', async (importOriginal) => {
...actual, ...actual,
IS_CE_EDITION: false, IS_CE_EDITION: false,
get ZENDESK_WIDGET_KEY() { return mockZendeskKey.value }, get ZENDESK_WIDGET_KEY() { return mockZendeskKey.value },
get SUPPORT_EMAIL_ADDRESS() { return mockSupportEmailKey.value },
} }
}) })
@@ -84,6 +89,7 @@ describe('Support', () => {
vi.clearAllMocks() vi.clearAllMocks()
window.zE = vi.fn() window.zE = vi.fn()
mockZendeskKey.value = 'test-key' mockZendeskKey.value = 'test-key'
mockSupportEmailKey.value = ''
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue) vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
vi.mocked(useProviderContext).mockReturnValue({ vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue, ...baseProviderContextValue,
@@ -96,7 +102,7 @@ describe('Support', () => {
const renderSupport = () => { const renderSupport = () => {
return render( return render(
<DropdownMenu open={true} onOpenChange={() => {}}> <DropdownMenu open={true} onOpenChange={() => { }}>
<DropdownMenuTrigger>open</DropdownMenuTrigger> <DropdownMenuTrigger>open</DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<Support closeAccountDropdown={mockCloseAccountDropdown} /> <Support closeAccountDropdown={mockCloseAccountDropdown} />
@@ -125,7 +131,7 @@ describe('Support', () => {
}) })
}) })
describe('Plan-based Channels', () => { describe('Dedicated Channels', () => {
it('should show "Contact Us" when ZENDESK_WIDGET_KEY is present', () => { it('should show "Contact Us" when ZENDESK_WIDGET_KEY is present', () => {
// Act // Act
renderSupport() renderSupport()
@@ -166,6 +172,27 @@ describe('Support', () => {
expect(screen.getByText('common.userProfile.emailSupport')).toBeInTheDocument() expect(screen.getByText('common.userProfile.emailSupport')).toBeInTheDocument()
expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument() expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument()
}) })
it('should show email support if specified in the config', () => {
// Arrange
mockZendeskKey.value = ''
mockSupportEmailKey.value = 'support@example.com'
vi.mocked(useProviderContext).mockReturnValue({
...baseProviderContextValue,
plan: {
...baseProviderContextValue.plan,
type: Plan.sandbox,
},
})
// Act
renderSupport()
fireEvent.click(screen.getByText('common.userProfile.support'))
// Assert
expect(screen.queryByText('common.userProfile.emailSupport')).toBeInTheDocument()
expect(screen.getByText('common.userProfile.emailSupport')?.closest('a')?.getAttribute('href')).toMatch(new RegExp(`^mailto:${mockSupportEmailKey.value}`))
})
}) })
describe('Interactions and Links', () => { describe('Interactions and Links', () => {

View File

@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu' import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu'
import { toggleZendeskWindow } from '@/app/components/base/zendesk/utils' import { toggleZendeskWindow } from '@/app/components/base/zendesk/utils'
import { Plan } from '@/app/components/billing/type' import { Plan } from '@/app/components/billing/type'
import { ZENDESK_WIDGET_KEY } from '@/config' import { SUPPORT_EMAIL_ADDRESS, ZENDESK_WIDGET_KEY } from '@/config'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { mailToSupport } from '../utils/util' import { mailToSupport } from '../utils/util'
@@ -17,8 +17,8 @@ export default function Support({ closeAccountDropdown }: SupportProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { plan } = useProviderContext() const { plan } = useProviderContext()
const { userProfile, langGeniusVersionInfo } = useAppContext() const { userProfile, langGeniusVersionInfo } = useAppContext()
const hasDedicatedChannel = plan.type !== Plan.sandbox const hasDedicatedChannel = plan.type !== Plan.sandbox || Boolean(SUPPORT_EMAIL_ADDRESS.trim())
const hasZendeskWidget = !!ZENDESK_WIDGET_KEY?.trim() const hasZendeskWidget = Boolean(ZENDESK_WIDGET_KEY.trim())
return ( return (
<DropdownMenuSub> <DropdownMenuSub>
@@ -49,7 +49,7 @@ export default function Support({ closeAccountDropdown }: SupportProps) {
{hasDedicatedChannel && !hasZendeskWidget && ( {hasDedicatedChannel && !hasZendeskWidget && (
<DropdownMenuItem <DropdownMenuItem
className="justify-between" className="justify-between"
render={<a href={mailToSupport(userProfile.email, plan.type, langGeniusVersionInfo?.current_version)} rel="noopener noreferrer" target="_blank" />} render={<a href={mailToSupport(userProfile.email, plan.type, langGeniusVersionInfo?.current_version, SUPPORT_EMAIL_ADDRESS)} rel="noopener noreferrer" target="_blank" />}
> >
<MenuItemContent <MenuItemContent
iconClassName="i-ri-mail-send-line" iconClassName="i-ri-mail-send-line"

View File

@@ -10,7 +10,7 @@ export const generateMailToLink = (email: string, subject?: string, body?: strin
return mailtoLink return mailtoLink
} }
export const mailToSupport = (account: string, plan: string, version: string) => { export const mailToSupport = (account: string, plan: string, version: string, supportEmailAddress?: string) => {
const subject = `Technical Support Request ${plan} ${account}` const subject = `Technical Support Request ${plan} ${account}`
const body = ` const body = `
Please do not remove the following information: Please do not remove the following information:
@@ -21,5 +21,5 @@ export const mailToSupport = (account: string, plan: string, version: string) =>
Platform: Platform:
Problem Description: Problem Description:
` `
return generateMailToLink('support@dify.ai', subject, body) return generateMailToLink(supportEmailAddress || 'support@dify.ai', subject, body)
} }

View File

@@ -342,6 +342,12 @@ export const ZENDESK_FIELD_IDS = {
'', '',
), ),
} }
export const SUPPORT_EMAIL_ADDRESS = getStringConfig(
env.NEXT_PUBLIC_SUPPORT_EMAIL_ADDRESS,
'',
)
export const APP_VERSION = pkg.version export const APP_VERSION = pkg.version
export const IS_MARKETPLACE = env.NEXT_PUBLIC_IS_MARKETPLACE export const IS_MARKETPLACE = env.NEXT_PUBLIC_IS_MARKETPLACE

View File

@@ -115,6 +115,7 @@ const clientSchema = {
*/ */
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(), NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
NEXT_PUBLIC_SITE_ABOUT: z.string().optional(), NEXT_PUBLIC_SITE_ABOUT: z.string().optional(),
NEXT_PUBLIC_SUPPORT_EMAIL_ADDRESS: z.email().optional(),
NEXT_PUBLIC_SUPPORT_MAIL_LOGIN: coercedBoolean.default(false), NEXT_PUBLIC_SUPPORT_MAIL_LOGIN: coercedBoolean.default(false),
/** /**
* The timeout for the text generation in millisecond * The timeout for the text generation in millisecond
@@ -184,6 +185,7 @@ export const env = createEnv({
NEXT_PUBLIC_PUBLIC_API_PREFIX: isServer ? process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX : getRuntimeEnvFromBody('publicApiPrefix'), NEXT_PUBLIC_PUBLIC_API_PREFIX: isServer ? process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX : getRuntimeEnvFromBody('publicApiPrefix'),
NEXT_PUBLIC_SENTRY_DSN: isServer ? process.env.NEXT_PUBLIC_SENTRY_DSN : getRuntimeEnvFromBody('sentryDsn'), NEXT_PUBLIC_SENTRY_DSN: isServer ? process.env.NEXT_PUBLIC_SENTRY_DSN : getRuntimeEnvFromBody('sentryDsn'),
NEXT_PUBLIC_SITE_ABOUT: isServer ? process.env.NEXT_PUBLIC_SITE_ABOUT : getRuntimeEnvFromBody('siteAbout'), NEXT_PUBLIC_SITE_ABOUT: isServer ? process.env.NEXT_PUBLIC_SITE_ABOUT : getRuntimeEnvFromBody('siteAbout'),
NEXT_PUBLIC_SUPPORT_EMAIL_ADDRESS: isServer ? process.env.NEXT_PUBLIC_SUPPORT_EMAIL_ADDRESS : getRuntimeEnvFromBody('supportEmailAddress'),
NEXT_PUBLIC_SUPPORT_MAIL_LOGIN: isServer ? process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN : getRuntimeEnvFromBody('supportMailLogin'), NEXT_PUBLIC_SUPPORT_MAIL_LOGIN: isServer ? process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN : getRuntimeEnvFromBody('supportMailLogin'),
NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS: isServer ? process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS : getRuntimeEnvFromBody('textGenerationTimeoutMs'), NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS: isServer ? process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS : getRuntimeEnvFromBody('textGenerationTimeoutMs'),
NEXT_PUBLIC_TOP_K_MAX_VALUE: isServer ? process.env.NEXT_PUBLIC_TOP_K_MAX_VALUE : getRuntimeEnvFromBody('topKMaxValue'), NEXT_PUBLIC_TOP_K_MAX_VALUE: isServer ? process.env.NEXT_PUBLIC_TOP_K_MAX_VALUE : getRuntimeEnvFromBody('topKMaxValue'),