& {
+ style?: CSSProperties
+}
+
+function BadgeHighlight({ size }: { size?: PremiumBadgeProps['size'] }) {
+ return (
+
+ )
+}
+
+function PremiumBadge({
className,
size,
color,
allowHover,
styleCss,
children,
- ...props
-}) => {
+}: PremiumBadgeProps) {
return (
-
+ {children}
+
+
+ )
+}
+
+export function PremiumBadgeButton({
+ className,
+ size,
+ color,
+ allowHover = true,
+ style,
+ children,
+ type = 'button',
+ ...props
+}: PremiumBadgeButtonProps) {
+ return (
+
+
+
)
}
-PremiumBadge.displayName = 'PremiumBadge'
export default PremiumBadge
diff --git a/web/app/components/billing/upgrade-btn/__tests__/index.spec.tsx b/web/app/components/billing/upgrade-btn/__tests__/index.spec.tsx
index 7eda24b944..5f6dc60038 100644
--- a/web/app/components/billing/upgrade-btn/__tests__/index.spec.tsx
+++ b/web/app/components/billing/upgrade-btn/__tests__/index.spec.tsx
@@ -38,10 +38,11 @@ describe('UpgradeBtn', () => {
expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
})
- it('should render premium badge by default', () => {
+ it('should render premium badge button by default', () => {
render()
- expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument()
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ expect(button).toHaveClass('premium-badge')
})
it('should render plain button when isPlain is true', () => {
@@ -75,7 +76,7 @@ describe('UpgradeBtn', () => {
// Props tests (REQUIRED)
describe('Props', () => {
- it('should apply custom className to premium badge', () => {
+ it('should apply custom className to premium badge button', () => {
const customClass = 'custom-upgrade-btn'
const { container } = render()
@@ -93,7 +94,7 @@ describe('UpgradeBtn', () => {
expect(button).toHaveClass(customClass)
})
- it('should apply custom style to premium badge', () => {
+ it('should apply custom style to premium badge button', () => {
const customStyle = { padding: '10px' }
const { container } = render()
@@ -132,13 +133,13 @@ describe('UpgradeBtn', () => {
// User Interactions
describe('User Interactions', () => {
- it('should call custom onClick when provided and premium badge is clicked', async () => {
+ it('should call custom onClick when provided and premium badge button is clicked', async () => {
const user = userEvent.setup()
const handleClick = vi.fn()
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
expect(handleClick).toHaveBeenCalledTimes(1)
expect(mockSetShowPricingModal).not.toHaveBeenCalled()
@@ -156,12 +157,12 @@ describe('UpgradeBtn', () => {
expect(mockSetShowPricingModal).not.toHaveBeenCalled()
})
- it('should open pricing modal when no custom onClick is provided and premium badge is clicked', async () => {
+ it('should open pricing modal when no custom onClick is provided and premium badge button is clicked', async () => {
const user = userEvent.setup()
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
})
@@ -176,13 +177,13 @@ describe('UpgradeBtn', () => {
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
})
- it('should track gtag event when loc is provided and badge is clicked', async () => {
+ it('should track gtag event when loc is provided and badge button is clicked', async () => {
const user = userEvent.setup()
const loc = 'header-navigation'
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
expect(mockGtag).toHaveBeenCalledTimes(1)
expect(mockGtag).toHaveBeenCalledWith('event', 'click_upgrade_btn', {
@@ -208,8 +209,8 @@ describe('UpgradeBtn', () => {
const user = userEvent.setup()
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
expect(mockGtag).not.toHaveBeenCalled()
})
@@ -219,8 +220,8 @@ describe('UpgradeBtn', () => {
delete gtagWindow.gtag
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
expect(mockGtag).not.toHaveBeenCalled()
})
@@ -231,8 +232,8 @@ describe('UpgradeBtn', () => {
const loc = 'settings-page'
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
expect(handleClick).toHaveBeenCalledTimes(1)
expect(mockGtag).toHaveBeenCalledTimes(1)
@@ -260,8 +261,8 @@ describe('UpgradeBtn', () => {
const user = userEvent.setup()
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
})
@@ -270,8 +271,8 @@ describe('UpgradeBtn', () => {
const user = userEvent.setup()
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
expect(mockGtag).not.toHaveBeenCalled()
})
@@ -292,8 +293,8 @@ describe('UpgradeBtn', () => {
const user = userEvent.setup()
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
expect(mockGtag).not.toHaveBeenCalled()
})
@@ -391,19 +392,26 @@ describe('UpgradeBtn', () => {
expect(handleClick).toHaveBeenCalledTimes(1)
})
- it('should be clickable for premium badge variant', async () => {
+ it('should be keyboard accessible for premium badge button variant', async () => {
const user = userEvent.setup()
const handleClick = vi.fn()
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
-
- // Click badge
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.tab()
+ expect(button).toHaveFocus()
+ await user.keyboard('{Enter}')
expect(handleClick).toHaveBeenCalledTimes(1)
})
+ it('should have proper button role for premium badge button variant', () => {
+ render()
+
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ expect(button).toHaveClass('premium-badge')
+ })
+
it('should have proper button role when isPlain is true', () => {
render()
@@ -418,8 +426,8 @@ describe('UpgradeBtn', () => {
const user = userEvent.setup()
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
await waitFor(() => {
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
@@ -431,8 +439,8 @@ describe('UpgradeBtn', () => {
const handleClick = vi.fn()
render()
- const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i)
- await user.click(badge)
+ const button = screen.getByRole('button', { name: /billing\.upgradeBtn\.encourage/i })
+ await user.click(button)
await waitFor(() => {
expect(handleClick).toHaveBeenCalledTimes(1)
diff --git a/web/app/components/billing/upgrade-btn/index.tsx b/web/app/components/billing/upgrade-btn/index.tsx
index 5eb1eb1d7f..e5b53555c2 100644
--- a/web/app/components/billing/upgrade-btn/index.tsx
+++ b/web/app/components/billing/upgrade-btn/index.tsx
@@ -6,7 +6,7 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import { useModalContext } from '@/context/modal-context'
-import PremiumBadge from '../../base/premium-badge'
+import { PremiumBadgeButton } from '../../base/premium-badge'
type Props = {
className?: string
@@ -20,6 +20,8 @@ type Props = {
labelKey?: Exclude, 'plans.community.features' | 'plans.enterprise.features' | 'plans.premium.features'>
}
+type GtagHandler = (command: 'event', action: 'click_upgrade_btn', payload: { loc: string }) => void
+
const UpgradeBtn: FC = ({
className,
size = 'm',
@@ -36,12 +38,13 @@ const UpgradeBtn: FC = ({
if (_onClick)
_onClick()
else
- (setShowPricingModal as any)()
+ setShowPricingModal()
}
const onClick = () => {
handleClick()
- if (loc && (window as any).gtag) {
- (window as any).gtag('event', 'click_upgrade_btn', {
+ const gtag = (window as Window & { gtag?: GtagHandler }).gtag
+ if (loc && gtag) {
+ gtag('event', 'click_upgrade_btn', {
loc,
})
}
@@ -63,21 +66,20 @@ const UpgradeBtn: FC = ({
}
return (
-
-
+
{label}
-
+
)
}
export default React.memo(UpgradeBtn)
diff --git a/web/app/components/header/__tests__/index.spec.tsx b/web/app/components/header/__tests__/index.spec.tsx
index ba284829f3..0e9ef6493d 100644
--- a/web/app/components/header/__tests__/index.spec.tsx
+++ b/web/app/components/header/__tests__/index.spec.tsx
@@ -45,7 +45,7 @@ vi.mock('@/app/components/header/tools-nav', () => ({
}))
vi.mock('@/app/components/header/plan-badge', () => ({
- default: ({ onClick, plan }: { onClick?: () => void, plan?: string }) => (
+ PlanBadge: ({ onClick, plan }: { onClick?: () => void, plan?: string }) => (
),
}))
diff --git a/web/app/components/header/account-dropdown/compliance.tsx b/web/app/components/header/account-dropdown/compliance.tsx
index 75d7200666..e1e134e864 100644
--- a/web/app/components/header/account-dropdown/compliance.tsx
+++ b/web/app/components/header/account-dropdown/compliance.tsx
@@ -65,7 +65,7 @@ function ComplianceDocActionVisual({
disabled={!canShowUpgradeTooltip}
render={(
-
+
{upgradeText}
diff --git a/web/app/components/header/account-dropdown/workplace-selector/index.tsx b/web/app/components/header/account-dropdown/workplace-selector/index.tsx
index a86da65797..934a003c4c 100644
--- a/web/app/components/header/account-dropdown/workplace-selector/index.tsx
+++ b/web/app/components/header/account-dropdown/workplace-selector/index.tsx
@@ -12,7 +12,7 @@ import {
import { toast } from '@langgenius/dify-ui/toast'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
-import PlanBadge from '@/app/components/header/plan-badge'
+import { PlanBadge } from '@/app/components/header/plan-badge'
import { useWorkspacesContext } from '@/context/workspace-context'
import { switchWorkspace } from '@/service/common'
import { basePath } from '@/utils/var'
diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx
index c7c12f1e35..8e94a831d4 100644
--- a/web/app/components/header/index.tsx
+++ b/web/app/components/header/index.tsx
@@ -18,7 +18,7 @@ import DatasetNav from './dataset-nav'
import EnvNav from './env-nav'
import ExploreNav from './explore-nav'
import LicenseNav from './license-env'
-import PlanBadge from './plan-badge'
+import { PlanBadge } from './plan-badge'
import PluginsNav from './plugins-nav'
import ToolsNav from './tools-nav'
diff --git a/web/app/components/header/license-env/index.tsx b/web/app/components/header/license-env/index.tsx
index fd360796b1..e7112a61f1 100644
--- a/web/app/components/header/license-env/index.tsx
+++ b/web/app/components/header/license-env/index.tsx
@@ -17,7 +17,7 @@ const LicenseNav = () => {
const count = dayjs(expiredAt).diff(dayjs(), 'days')
return (
-
+
{count <= 1 && {t('license.expiring', { ns: 'common', count })}}
{count > 1 && {t('license.expiring_plural', { ns: 'common', count })}}
diff --git a/web/app/components/header/plan-badge/__tests__/index.spec.tsx b/web/app/components/header/plan-badge/__tests__/index.spec.tsx
index 3abb791340..4e562ecf99 100644
--- a/web/app/components/header/plan-badge/__tests__/index.spec.tsx
+++ b/web/app/components/header/plan-badge/__tests__/index.spec.tsx
@@ -4,7 +4,7 @@ import { vi } from 'vitest'
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '../../../billing/type'
-import PlanBadge from '../index'
+import { PlanBadge } from '../index'
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
@@ -34,6 +34,20 @@ describe('PlanBadge', () => {
expect(
screen.getByText('billing.upgradeBtn.encourageShort'),
).toBeInTheDocument()
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
+ })
+
+ it('should render upgrade action as a button when onClick is provided', () => {
+ const handleClick = vi.fn()
+ mockUseProviderContext.mockReturnValue(
+ createMockProviderContextValue({ isFetchedPlan: true }),
+ )
+
+ render()
+
+ const button = screen.getByRole('button', { name: 'billing.upgradeBtn.encourageShort' })
+ fireEvent.click(button)
+ expect(handleClick).toHaveBeenCalledTimes(1)
})
it('should render sandbox badge when plan is sandbox and sandboxAsUpgrade is false', () => {
@@ -42,6 +56,7 @@ describe('PlanBadge', () => {
)
render()
expect(screen.getByText(Plan.sandbox)).toBeInTheDocument()
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
})
it('should render professional badge when plan is professional', () => {
@@ -87,7 +102,7 @@ describe('PlanBadge', () => {
createMockProviderContextValue({ isFetchedPlan: true }),
)
render()
- fireEvent.click(screen.getByText(Plan.team))
+ fireEvent.click(screen.getByRole('button', { name: Plan.team }))
expect(handleClick).toHaveBeenCalledTimes(1)
})
diff --git a/web/app/components/header/plan-badge/index.tsx b/web/app/components/header/plan-badge/index.tsx
index 9547ddf6f7..c889e9d5ae 100644
--- a/web/app/components/header/plan-badge/index.tsx
+++ b/web/app/components/header/plan-badge/index.tsx
@@ -1,11 +1,11 @@
-import type { FC } from 'react'
+import type { ReactNode } from 'react'
import {
RiGraduationCapFill,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useProviderContext } from '@/context/provider-context'
import { SparklesSoft } from '../../base/icons/src/public/common'
-import PremiumBadge from '../../base/premium-badge'
+import PremiumBadge, { PremiumBadgeButton } from '../../base/premium-badge'
import { Plan } from '../../billing/type'
type PlanBadgeProps = {
@@ -15,7 +15,33 @@ type PlanBadgeProps = {
onClick?: () => void
}
-const PlanBadge: FC = ({ plan, allowHover, sandboxAsUpgrade = false, onClick }) => {
+function PlanBadgeShell({
+ size,
+ color,
+ allowHover,
+ onClick,
+ children,
+}: Pick & {
+ size?: 's' | 'm'
+ color: 'blue' | 'indigo' | 'gray'
+ children: ReactNode
+}) {
+ if (onClick) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function PlanBadge({ plan, allowHover, sandboxAsUpgrade = false, onClick }: PlanBadgeProps) {
const { isFetchedPlan, isEducationWorkspace } = useProviderContext()
const { t } = useTranslation()
@@ -23,51 +49,49 @@ const PlanBadge: FC = ({ plan, allowHover, sandboxAsUpgrade = fa
return null
if (plan === Plan.sandbox && sandboxAsUpgrade) {
return (
-
-
+
+
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
-
+
)
}
if (plan === Plan.sandbox) {
return (
-
+
{plan}
-
+
)
}
if (plan === Plan.professional) {
return (
-
+
- {isEducationWorkspace && }
+ {isEducationWorkspace && }
pro
-
+
)
}
if (plan === Plan.team) {
return (
-
+
{plan}
-
+
)
}
return null
}
-
-export default PlanBadge
diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx
index 0970d66cfc..1c2f7177a5 100644
--- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx
+++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx
@@ -246,8 +246,8 @@ const Popup = ({
{t('common.publishAs', { ns: 'pipeline' })}
{!isAllowPublishAsCustomKnowledgePipelineTemplate && (
-
-
+
+
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/upgrade-modal.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/upgrade-modal.spec.tsx
index 0abae16f9a..b6af667022 100644
--- a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/upgrade-modal.spec.tsx
+++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/upgrade-modal.spec.tsx
@@ -8,15 +8,6 @@ vi.mock('@/context/modal-context', () => ({
mockUseModalContextSelector(selector),
}))
-vi.mock('@/app/components/base/premium-badge', () => ({
- __esModule: true,
- default: ({ children, onClick }: { children: React.ReactNode, onClick?: () => void }) => (
-
- ),
-}))
-
describe('human-input/delivery-method/upgrade-modal', () => {
beforeEach(() => {
vi.clearAllMocks()
diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/upgrade-modal.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/upgrade-modal.tsx
index 18a6e90796..3daa347448 100644
--- a/web/app/components/workflow/nodes/human-input/components/delivery-method/upgrade-modal.tsx
+++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/upgrade-modal.tsx
@@ -2,7 +2,7 @@ import { Button } from '@langgenius/dify-ui/button'
import { RiMailSendFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
-import PremiumBadge from '@/app/components/base/premium-badge'
+import { PremiumBadgeButton } from '@/app/components/base/premium-badge'
import { UpgradeModal as BaseUpgradeModal } from '@/app/components/base/upgrade-modal'
import { useModalContextSelector } from '@/context/modal-context'
@@ -39,20 +39,19 @@ export function UpgradeModal({
>
{t('nodes.humanInput.deliveryMethod.upgradeTipHide', { ns: 'workflow' })}
-
-
+
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
-
+
>
)}
/>
diff --git a/web/app/education-apply/applied-education-content.tsx b/web/app/education-apply/applied-education-content.tsx
index c3ff35b1b9..cbe2e11fda 100644
--- a/web/app/education-apply/applied-education-content.tsx
+++ b/web/app/education-apply/applied-education-content.tsx
@@ -10,7 +10,7 @@ import {
import { useTranslation } from 'react-i18next'
import { Plan } from '@/app/components/billing/type'
import { WorkplaceSelectorContent } from '@/app/components/header/account-dropdown/workplace-selector'
-import PlanBadge from '@/app/components/header/plan-badge'
+import { PlanBadge } from '@/app/components/header/plan-badge'
type AppliedEducationContentProps = {
workspaces: IWorkspace[]