refactor(web): split premium badge button semantics (#36026)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh
2026-05-11 13:57:30 +08:00
committed by GitHub
parent 74a04afe27
commit dd1cdbbd41
21 changed files with 202 additions and 117 deletions

View File

@@ -1741,11 +1741,6 @@
"count": 4
}
},
"web/app/components/billing/upgrade-btn/index.tsx": {
"ts/no-explicit-any": {
"count": 3
}
},
"web/app/components/datasets/common/image-previewer/index.tsx": {
"no-irregular-whitespace": {
"count": 1

View File

@@ -166,7 +166,7 @@ export default function AccountPage() {
{userProfile.name}
{isEducationAccount && (
<PremiumBadge size="s" color="blue" className="ml-1 !px-2">
<RiGraduationCapFill className="mr-1 h-3 w-3" />
<RiGraduationCapFill aria-hidden="true" className="mr-1 h-3 w-3" />
<span className="system-2xs-medium">EDU</span>
</PremiumBadge>
)}

View File

@@ -62,7 +62,7 @@ export default function AppSelector() {
{userProfile.name}
{isEducationAccount && (
<PremiumBadge size="s" color="blue" className="ml-1 px-2!">
<span className="mr-1 i-ri-graduation-cap-fill h-3 w-3" />
<span aria-hidden="true" className="mr-1 i-ri-graduation-cap-fill h-3 w-3" />
<span className="system-2xs-medium">EDU</span>
</PremiumBadge>
)}

View File

@@ -17,7 +17,7 @@ import AppIcon from '@/app/components/base/app-icon'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import Divider from '@/app/components/base/divider'
import Input from '@/app/components/base/input'
import PremiumBadge from '@/app/components/base/premium-badge'
import { PremiumBadgeButton } from '@/app/components/base/premium-badge'
import Textarea from '@/app/components/base/textarea'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContext } from '@/context/modal-context'
@@ -395,14 +395,14 @@ const SettingsModal: FC<ISettingsModalProps> = ({
{/* upgrade button */}
{enableBilling && isFreePlan && (
<div className="h-[18px] select-none">
<PremiumBadge size="s" color="blue" allowHover={true} onClick={handlePlanClick}>
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
<span aria-hidden="true" className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<div className="system-xs-medium">
<span className="p-1">
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
</span>
</div>
</PremiumBadge>
</PremiumBadgeButton>
</div>
)}
</div>

View File

@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react'
import PremiumBadge from '../index'
import userEvent from '@testing-library/user-event'
import PremiumBadge, { PremiumBadgeButton } from '../index'
describe('PremiumBadge', () => {
it('renders with default props', () => {
@@ -24,9 +25,9 @@ describe('PremiumBadge', () => {
it('applies allowHover class when allowHover is true', () => {
render(
<PremiumBadge allowHover>
<PremiumBadgeButton>
Premium
</PremiumBadge>,
</PremiumBadgeButton>,
)
const badge = screen.getByText('Premium')
expect(badge).toBeInTheDocument()
@@ -43,4 +44,22 @@ describe('PremiumBadge', () => {
expect(badge).toBeInTheDocument()
expect(badge).toHaveStyle('background-color: red')
})
it('renders a static badge without button semantics', () => {
render(<PremiumBadge>Premium</PremiumBadge>)
expect(screen.queryByRole('button')).not.toBeInTheDocument()
})
it('renders an action badge as a button', async () => {
const user = userEvent.setup()
const handleClick = vi.fn()
render(<PremiumBadgeButton onClick={handleClick}>Upgrade</PremiumBadgeButton>)
const button = screen.getByRole('button', { name: 'Upgrade' })
await user.click(button)
expect(handleClick).toHaveBeenCalledTimes(1)
})
})

View File

@@ -1,5 +1,5 @@
@utility premium-badge {
@apply shrink-0 relative inline-flex justify-center items-center rounded-md box-border border border-transparent text-white shadow-xs hover:shadow-lg bg-origin-border overflow-hidden transition-all duration-100 ease-out;
@apply shrink-0 relative inline-flex justify-center items-center rounded-md box-border border border-transparent text-white shadow-xs hover:shadow-lg bg-origin-border overflow-hidden transition-[background-color,background-image,box-shadow] duration-100 ease-out motion-reduce:transition-none;
background-clip: padding-box, border-box;
}

View File

@@ -1,14 +1,12 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import PremiumBadge from '.'
import PremiumBadge, { PremiumBadgeButton } from '.'
const colors: Array<NonNullable<React.ComponentProps<typeof PremiumBadge>['color']>> = ['blue', 'indigo', 'gray', 'orange']
const PremiumBadgeGallery = ({
size = 'm',
allowHover = false,
}: {
size?: 's' | 'm'
allowHover?: boolean
}) => {
return (
<div className="flex w-full max-w-xl flex-col gap-4 rounded-2xl border border-divider-subtle bg-components-panel-bg p-6">
@@ -16,7 +14,7 @@ const PremiumBadgeGallery = ({
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{colors.map(color => (
<div key={color} className="flex flex-col items-center gap-2 rounded-xl border border-transparent px-2 py-4 hover:border-divider-subtle hover:bg-background-default-subtle">
<PremiumBadge color={color} size={size} allowHover={allowHover}>
<PremiumBadge color={color} size={size}>
<span className="px-2 text-xs font-semibold tracking-[0.14em] uppercase">Premium</span>
</PremiumBadge>
<span className="text-[11px] tracking-[0.16em] text-text-tertiary uppercase">{color}</span>
@@ -43,11 +41,9 @@ const meta = {
control: 'radio',
options: ['s', 'm'],
},
allowHover: { control: 'boolean' },
},
args: {
size: 'm',
allowHover: false,
},
tags: ['autodocs'],
} satisfies Meta<typeof PremiumBadgeGallery>
@@ -57,8 +53,10 @@ type Story = StoryObj<typeof meta>
export const Playground: Story = {}
export const HoverEnabled: Story = {
args: {
allowHover: true,
},
export const Action: Story = {
render: () => (
<PremiumBadgeButton color="blue" onClick={() => {}}>
<span className="px-2 text-xs font-semibold">Upgrade</span>
</PremiumBadgeButton>
),
}

View File

@@ -1,8 +1,7 @@
import type { VariantProps } from 'class-variance-authority'
import type { CSSProperties, ReactNode } from 'react'
import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { cva } from 'class-variance-authority'
import * as React from 'react'
import { Highlight } from '@/app/components/base/icons/src/public/common'
const PremiumBadgeVariants = cva(
@@ -38,31 +37,66 @@ type PremiumBadgeProps = {
color?: 'blue' | 'indigo' | 'gray' | 'orange'
allowHover?: boolean
styleCss?: CSSProperties
className?: string
children?: ReactNode
} & React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof PremiumBadgeVariants>
} & VariantProps<typeof PremiumBadgeVariants>
const PremiumBadge: React.FC<PremiumBadgeProps> = ({
type PremiumBadgeButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'color'> & Omit<PremiumBadgeProps, 'styleCss'> & {
style?: CSSProperties
}
function BadgeHighlight({ size }: { size?: PremiumBadgeProps['size'] }) {
return (
<Highlight
aria-hidden="true"
className={cn('absolute top-0 right-1/2 translate-x-[20%] opacity-50 transition-[opacity,transform] duration-100 ease-out hover:translate-x-[30%] hover:opacity-80 motion-reduce:transition-none', size === 's' ? 'h-[18px] w-12' : 'h-6 w-12')}
/>
)
}
function PremiumBadge({
className,
size,
color,
allowHover,
styleCss,
children,
...props
}) => {
}: PremiumBadgeProps) {
return (
<div
<span
className={cn(PremiumBadgeVariants({ size, color, allowHover, className }), 'relative text-nowrap')}
style={styleCss}
>
{children}
<BadgeHighlight size={size} />
</span>
)
}
export function PremiumBadgeButton({
className,
size,
color,
allowHover = true,
style,
children,
type = 'button',
...props
}: PremiumBadgeButtonProps) {
return (
<button
type={type}
className={cn(
PremiumBadgeVariants({ size, color, allowHover, className }),
'relative touch-manipulation text-nowrap focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
)}
style={style}
{...props}
>
{children}
<Highlight
className={cn('absolute top-0 right-1/2 translate-x-[20%] opacity-50 transition-all duration-100 ease-out hover:translate-x-[30%] hover:opacity-80', size === 's' ? 'h-[18px] w-12' : 'h-6 w-12')}
/>
</div>
<BadgeHighlight size={size} />
</button>
)
}
PremiumBadge.displayName = 'PremiumBadge'
export default PremiumBadge

View File

@@ -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(<UpgradeBtn />)
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(<UpgradeBtn className={customClass} />)
@@ -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(<UpgradeBtn style={customStyle} />)
@@ -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(<UpgradeBtn onClick={handleClick} />)
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(<UpgradeBtn />)
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(<UpgradeBtn loc={loc} />)
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(<UpgradeBtn />)
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(<UpgradeBtn loc="test-location" />)
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(<UpgradeBtn onClick={handleClick} loc={loc} />)
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(<UpgradeBtn onClick={undefined} />)
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(<UpgradeBtn loc={undefined} />)
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(<UpgradeBtn loc="" />)
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(<UpgradeBtn onClick={handleClick} />)
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(<UpgradeBtn />)
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(<UpgradeBtn isPlain />)
@@ -418,8 +426,8 @@ describe('UpgradeBtn', () => {
const user = userEvent.setup()
render(<UpgradeBtn />)
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(<UpgradeBtn onClick={handleClick} loc="integration-test" />)
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)

View File

@@ -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<I18nKeysWithPrefix<'billing'>, 'plans.community.features' | 'plans.enterprise.features' | 'plans.premium.features'>
}
type GtagHandler = (command: 'event', action: 'click_upgrade_btn', payload: { loc: string }) => void
const UpgradeBtn: FC<Props> = ({
className,
size = 'm',
@@ -36,12 +38,13 @@ const UpgradeBtn: FC<Props> = ({
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<Props> = ({
}
return (
<PremiumBadge
<PremiumBadgeButton
size={size}
color="blue"
allowHover={true}
onClick={onClick}
className={className}
style={style}
>
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<SparklesSoft aria-hidden="true" className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<div className="system-xs-medium">
<span className="p-1">
{label}
</span>
</div>
</PremiumBadge>
</PremiumBadgeButton>
)
}
export default React.memo(UpgradeBtn)

View File

@@ -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 }) => (
<button data-testid="plan-badge" onClick={onClick} data-plan={plan} />
),
}))

View File

@@ -65,7 +65,7 @@ function ComplianceDocActionVisual({
disabled={!canShowUpgradeTooltip}
render={(
<PremiumBadge color="blue" allowHover={true}>
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<SparklesSoft aria-hidden="true" className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<div className="px-1 system-xs-medium">
{upgradeText}
</div>

View File

@@ -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'

View File

@@ -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'

View File

@@ -17,7 +17,7 @@ const LicenseNav = () => {
const count = dayjs(expiredAt).diff(dayjs(), 'days')
return (
<PremiumBadge color="orange" className="select-none">
<RiHourglass2Fill className="flex size-3 items-center pl-0.5 text-components-premium-badge-indigo-text-stop-0" />
<RiHourglass2Fill aria-hidden="true" className="flex size-3 items-center pl-0.5 text-components-premium-badge-indigo-text-stop-0" />
{count <= 1 && <span className="px-0.5 system-xs-medium">{t('license.expiring', { ns: 'common', count })}</span>}
{count > 1 && <span className="px-0.5 system-xs-medium">{t('license.expiring_plural', { ns: 'common', count })}</span>}
</PremiumBadge>

View File

@@ -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(<PlanBadge plan={Plan.sandbox} sandboxAsUpgrade={true} onClick={handleClick} />)
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(<PlanBadge plan={Plan.sandbox} sandboxAsUpgrade={false} />)
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(<PlanBadge plan={Plan.team} onClick={handleClick} />)
fireEvent.click(screen.getByText(Plan.team))
fireEvent.click(screen.getByRole('button', { name: Plan.team }))
expect(handleClick).toHaveBeenCalledTimes(1)
})

View File

@@ -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<PlanBadgeProps> = ({ plan, allowHover, sandboxAsUpgrade = false, onClick }) => {
function PlanBadgeShell({
size,
color,
allowHover,
onClick,
children,
}: Pick<PlanBadgeProps, 'allowHover' | 'onClick'> & {
size?: 's' | 'm'
color: 'blue' | 'indigo' | 'gray'
children: ReactNode
}) {
if (onClick) {
return (
<PremiumBadgeButton className="select-none" size={size} color={color} allowHover={allowHover} onClick={onClick}>
{children}
</PremiumBadgeButton>
)
}
return (
<PremiumBadge className="select-none" size={size} color={color}>
{children}
</PremiumBadge>
)
}
export function PlanBadge({ plan, allowHover, sandboxAsUpgrade = false, onClick }: PlanBadgeProps) {
const { isFetchedPlan, isEducationWorkspace } = useProviderContext()
const { t } = useTranslation()
@@ -23,51 +49,49 @@ const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, sandboxAsUpgrade = fa
return null
if (plan === Plan.sandbox && sandboxAsUpgrade) {
return (
<PremiumBadge className="select-none" color="blue" allowHover={allowHover} onClick={onClick}>
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<PlanBadgeShell color="blue" allowHover={allowHover} onClick={onClick}>
<SparklesSoft aria-hidden="true" className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<div className="system-xs-medium">
<span className="p-1 whitespace-nowrap">
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
</span>
</div>
</PremiumBadge>
</PlanBadgeShell>
)
}
if (plan === Plan.sandbox) {
return (
<PremiumBadge className="select-none" size="s" color="gray" allowHover={allowHover} onClick={onClick}>
<PlanBadgeShell size="s" color="gray" allowHover={allowHover} onClick={onClick}>
<div className="system-2xs-medium-uppercase">
<span className="p-1">
{plan}
</span>
</div>
</PremiumBadge>
</PlanBadgeShell>
)
}
if (plan === Plan.professional) {
return (
<PremiumBadge className="select-none" size="s" color="blue" allowHover={allowHover} onClick={onClick}>
<PlanBadgeShell size="s" color="blue" allowHover={allowHover} onClick={onClick}>
<div className="system-2xs-medium-uppercase">
<span className="inline-flex items-center gap-1 p-1">
{isEducationWorkspace && <RiGraduationCapFill className="h-3 w-3" />}
{isEducationWorkspace && <RiGraduationCapFill aria-hidden="true" className="h-3 w-3" />}
pro
</span>
</div>
</PremiumBadge>
</PlanBadgeShell>
)
}
if (plan === Plan.team) {
return (
<PremiumBadge className="select-none" size="s" color="indigo" allowHover={allowHover} onClick={onClick}>
<PlanBadgeShell size="s" color="indigo" allowHover={allowHover} onClick={onClick}>
<div className="system-2xs-medium-uppercase">
<span className="p-1">
{plan}
</span>
</div>
</PremiumBadge>
</PlanBadgeShell>
)
}
return null
}
export default PlanBadge

View File

@@ -246,8 +246,8 @@ const Popup = ({
{t('common.publishAs', { ns: 'pipeline' })}
</span>
{!isAllowPublishAsCustomKnowledgePipelineTemplate && (
<PremiumBadge className="shrink-0 cursor-pointer select-none" size="s" color="indigo">
<SparklesSoft className="flex size-3 items-center text-components-premium-badge-indigo-text-stop-0" />
<PremiumBadge className="shrink-0 select-none" size="s" color="indigo">
<SparklesSoft aria-hidden="true" className="flex size-3 items-center text-components-premium-badge-indigo-text-stop-0" />
<span className="p-0.5 system-2xs-medium">
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
</span>

View File

@@ -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 }) => (
<button type="button" onClick={onClick}>
{children}
</button>
),
}))
describe('human-input/delivery-method/upgrade-modal', () => {
beforeEach(() => {
vi.clearAllMocks()

View File

@@ -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' })}
</Button>
<PremiumBadge
<PremiumBadgeButton
size="custom"
color="blue"
allowHover={true}
className="h-8 w-[93px]"
onClick={handleUpgrade}
>
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<SparklesSoft aria-hidden="true" className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
<div className="system-sm-medium">
<span className="p-1">
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
</span>
</div>
</PremiumBadge>
</PremiumBadgeButton>
</>
)}
/>

View File

@@ -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[]