mirror of
https://github.com/langgenius/dify.git
synced 2026-05-12 07:37:09 +08:00
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:
parent
74a04afe27
commit
dd1cdbbd41
@ -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
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
),
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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} />
|
||||
),
|
||||
}))
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -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[]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user