mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
Merge branch 'main' into jzh
This commit is contained in:
commit
b7fe45d800
@ -58,20 +58,4 @@ describe('AutomaticBtn', () => {
|
|||||||
expect(mockOnClick).toHaveBeenCalledTimes(3)
|
expect(mockOnClick).toHaveBeenCalledTimes(3)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Styling', () => {
|
|
||||||
it('should have secondary-accent variant', () => {
|
|
||||||
render(<AutomaticBtn onClick={mockOnClick} />)
|
|
||||||
|
|
||||||
const button = screen.getByRole('button')
|
|
||||||
expect(button.className).toContain('secondary-accent')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have small size', () => {
|
|
||||||
render(<AutomaticBtn onClick={mockOnClick} />)
|
|
||||||
|
|
||||||
const button = screen.getByRole('button')
|
|
||||||
expect(button.className).toContain('small')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -95,12 +95,6 @@ describe('APIKeyInfoPanel - Cloud Edition', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Props and Styling', () => {
|
describe('Props and Styling', () => {
|
||||||
it('should render button with primary variant', () => {
|
|
||||||
scenarios.withAPIKeyNotSet()
|
|
||||||
const button = screen.getByRole('button')
|
|
||||||
expect(button).toHaveClass('btn-primary')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render panel container with correct classes', () => {
|
it('should render panel container with correct classes', () => {
|
||||||
const { container } = scenarios.withAPIKeyNotSet()
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
const panel = container.firstChild as HTMLElement
|
const panel = container.firstChild as HTMLElement
|
||||||
|
|||||||
@ -108,12 +108,6 @@ describe('APIKeyInfoPanel - Community Edition', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Props and Styling', () => {
|
describe('Props and Styling', () => {
|
||||||
it('should render button with primary variant', () => {
|
|
||||||
scenarios.withAPIKeyNotSet()
|
|
||||||
const button = screen.getByRole('button')
|
|
||||||
expect(button).toHaveClass('btn-primary')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render panel container with correct classes', () => {
|
it('should render panel container with correct classes', () => {
|
||||||
const { container } = scenarios.withAPIKeyNotSet()
|
const { container } = scenarios.withAPIKeyNotSet()
|
||||||
const panel = container.firstChild as HTMLElement
|
const panel = container.firstChild as HTMLElement
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { RenderOptions } from '@testing-library/react'
|
import type { RenderOptions } from '@testing-library/react'
|
||||||
import type { Mock, MockedFunction } from 'vitest'
|
import type { Mock, MockedFunction } from 'vitest'
|
||||||
import type { ModalContextState } from '@/context/modal-context'
|
import type { ModalContextState } from '@/context/modal-context'
|
||||||
import { fireEvent, render } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { noop } from 'es-toolkit/function'
|
import { noop } from 'es-toolkit/function'
|
||||||
import { defaultPlan } from '@/app/components/billing/config'
|
import { defaultPlan } from '@/app/components/billing/config'
|
||||||
import { useModalContext as actualUseModalContext } from '@/context/modal-context'
|
import { useModalContext as actualUseModalContext } from '@/context/modal-context'
|
||||||
@ -81,6 +81,8 @@ type APIKeyInfoPanelRenderOptions = {
|
|||||||
mockOverrides?: MockOverrides
|
mockOverrides?: MockOverrides
|
||||||
} & Omit<RenderOptions, 'wrapper'>
|
} & Omit<RenderOptions, 'wrapper'>
|
||||||
|
|
||||||
|
const mainButtonName = /appOverview\.apiKeyInfo\.setAPIBtn/
|
||||||
|
|
||||||
// Setup function to configure mocks
|
// Setup function to configure mocks
|
||||||
function setupMocks(overrides: MockOverrides = {}) {
|
function setupMocks(overrides: MockOverrides = {}) {
|
||||||
mockUseProviderContext.mockReturnValue({
|
mockUseProviderContext.mockReturnValue({
|
||||||
@ -137,7 +139,7 @@ export const scenarios = {
|
|||||||
export const assertions = {
|
export const assertions = {
|
||||||
// Should render main button
|
// Should render main button
|
||||||
shouldRenderMainButton: () => {
|
shouldRenderMainButton: () => {
|
||||||
const button = document.querySelector('button.btn-primary')
|
const button = screen.getByRole('button', { name: mainButtonName })
|
||||||
expect(button).toBeInTheDocument()
|
expect(button).toBeInTheDocument()
|
||||||
return button
|
return button
|
||||||
},
|
},
|
||||||
@ -174,9 +176,8 @@ export const assertions = {
|
|||||||
export const interactions = {
|
export const interactions = {
|
||||||
// Click the main button
|
// Click the main button
|
||||||
clickMainButton: () => {
|
clickMainButton: () => {
|
||||||
const button = document.querySelector('button.btn-primary')
|
const button = screen.getByRole('button', { name: mainButtonName })
|
||||||
if (button)
|
fireEvent.click(button)
|
||||||
fireEvent.click(button)
|
|
||||||
return button
|
return button
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -191,6 +192,7 @@ export const interactions = {
|
|||||||
|
|
||||||
// Text content keys for assertions
|
// Text content keys for assertions
|
||||||
export const textKeys = {
|
export const textKeys = {
|
||||||
|
button: mainButtonName,
|
||||||
selfHost: {
|
selfHost: {
|
||||||
titleRow1: /appOverview\.apiKeyInfo\.selfHost\.title\.row1/,
|
titleRow1: /appOverview\.apiKeyInfo\.selfHost\.title\.row1/,
|
||||||
titleRow2: /appOverview\.apiKeyInfo\.selfHost\.title\.row2/,
|
titleRow2: /appOverview\.apiKeyInfo\.selfHost\.title\.row2/,
|
||||||
|
|||||||
@ -84,49 +84,6 @@ describe('InlineDeleteConfirm', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Variant prop', () => {
|
|
||||||
it('should render with delete variant by default', () => {
|
|
||||||
const onConfirm = vi.fn()
|
|
||||||
const onCancel = vi.fn()
|
|
||||||
const { getByText } = render(
|
|
||||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
|
||||||
)
|
|
||||||
|
|
||||||
const confirmButton = getByText('Yes').closest('button')
|
|
||||||
expect(confirmButton?.className).toContain('btn-destructive-primary')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render without destructive class for warning variant', () => {
|
|
||||||
const onConfirm = vi.fn()
|
|
||||||
const onCancel = vi.fn()
|
|
||||||
const { getByText } = render(
|
|
||||||
<InlineDeleteConfirm
|
|
||||||
variant="warning"
|
|
||||||
onConfirm={onConfirm}
|
|
||||||
onCancel={onCancel}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
const confirmButton = getByText('Yes').closest('button')
|
|
||||||
expect(confirmButton?.className).not.toContain('btn-destructive-primary')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render without destructive class for info variant', () => {
|
|
||||||
const onConfirm = vi.fn()
|
|
||||||
const onCancel = vi.fn()
|
|
||||||
const { getByText } = render(
|
|
||||||
<InlineDeleteConfirm
|
|
||||||
variant="info"
|
|
||||||
onConfirm={onConfirm}
|
|
||||||
onCancel={onCancel}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
|
|
||||||
const confirmButton = getByText('Yes').closest('button')
|
|
||||||
expect(confirmButton?.className).not.toContain('btn-destructive-primary')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Custom className', () => {
|
describe('Custom className', () => {
|
||||||
it('should apply custom className to wrapper', () => {
|
it('should apply custom className to wrapper', () => {
|
||||||
const onConfirm = vi.fn()
|
const onConfirm = vi.fn()
|
||||||
|
|||||||
@ -22,7 +22,6 @@ describe('NotionConnector', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(button).toBeInTheDocument()
|
expect(button).toBeInTheDocument()
|
||||||
expect(button).toHaveClass('btn', 'btn-primary')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should trigger the onSetting callback when the real button is clicked', async () => {
|
it('should trigger the onSetting callback when the real button is clicked', async () => {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import {
|
|||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogActions,
|
AlertDialogActions,
|
||||||
AlertDialogCancelButton,
|
AlertDialogCancelButton,
|
||||||
AlertDialogClose,
|
|
||||||
AlertDialogConfirmButton,
|
AlertDialogConfirmButton,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
@ -70,14 +69,16 @@ describe('AlertDialog wrapper', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('User Interactions', () => {
|
describe('User Interactions', () => {
|
||||||
it('should open and close dialog when trigger and close are clicked', async () => {
|
it('should open and close dialog when trigger and cancel button are clicked', async () => {
|
||||||
render(
|
render(
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger>Open Dialog</AlertDialogTrigger>
|
<AlertDialogTrigger>Open Dialog</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogTitle>Action Required</AlertDialogTitle>
|
<AlertDialogTitle>Action Required</AlertDialogTitle>
|
||||||
<AlertDialogDescription>Please confirm the action.</AlertDialogDescription>
|
<AlertDialogDescription>Please confirm the action.</AlertDialogDescription>
|
||||||
<AlertDialogClose>Cancel</AlertDialogClose>
|
<AlertDialogActions>
|
||||||
|
<AlertDialogCancelButton>Cancel</AlertDialogCancelButton>
|
||||||
|
</AlertDialogActions>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>,
|
</AlertDialog>,
|
||||||
)
|
)
|
||||||
@ -109,8 +110,7 @@ describe('AlertDialog wrapper', () => {
|
|||||||
|
|
||||||
expect(screen.getByTestId('actions')).toHaveClass('flex', 'items-start', 'justify-end', 'gap-2', 'self-stretch', 'p-6', 'custom-actions')
|
expect(screen.getByTestId('actions')).toHaveClass('flex', 'items-start', 'justify-end', 'gap-2', 'self-stretch', 'p-6', 'custom-actions')
|
||||||
const confirmButton = screen.getByRole('button', { name: 'Confirm' })
|
const confirmButton = screen.getByRole('button', { name: 'Confirm' })
|
||||||
expect(confirmButton).toHaveClass('btn-primary')
|
expect(confirmButton).toHaveClass('bg-components-button-destructive-primary-bg')
|
||||||
expect(confirmButton).toHaveClass('btn-destructive-primary')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should keep dialog open after confirm click and close via cancel helper', async () => {
|
it('should keep dialog open after confirm click and close via cancel helper', async () => {
|
||||||
|
|||||||
@ -10,7 +10,6 @@ export const AlertDialog = BaseAlertDialog.Root
|
|||||||
export const AlertDialogTrigger = BaseAlertDialog.Trigger
|
export const AlertDialogTrigger = BaseAlertDialog.Trigger
|
||||||
export const AlertDialogTitle = BaseAlertDialog.Title
|
export const AlertDialogTitle = BaseAlertDialog.Title
|
||||||
export const AlertDialogDescription = BaseAlertDialog.Description
|
export const AlertDialogDescription = BaseAlertDialog.Description
|
||||||
export const AlertDialogClose = BaseAlertDialog.Close
|
|
||||||
|
|
||||||
type AlertDialogContentProps = {
|
type AlertDialogContentProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
|||||||
@ -53,7 +53,7 @@ function AvatarImage({
|
|||||||
}: AvatarImageProps) {
|
}: AvatarImageProps) {
|
||||||
return (
|
return (
|
||||||
<BaseAvatar.Image
|
<BaseAvatar.Image
|
||||||
className={cn('inset-0 absolute size-full object-cover', className)}
|
className={cn('absolute inset-0 size-full object-cover', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { Button } from '../index'
|
import { Button } from '../index'
|
||||||
|
|
||||||
afterEach(cleanup)
|
|
||||||
|
|
||||||
describe('Button', () => {
|
describe('Button', () => {
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
it('renders children text', () => {
|
it('renders children text', () => {
|
||||||
@ -31,58 +29,79 @@ describe('Button', () => {
|
|||||||
expect(link).toHaveTextContent('Link')
|
expect(link).toHaveTextContent('Link')
|
||||||
expect(link).toHaveAttribute('href', '/test')
|
expect(link).toHaveAttribute('href', '/test')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('applies base layout classes', () => {
|
||||||
|
render(<Button>Click me</Button>)
|
||||||
|
const btn = screen.getByRole('button')
|
||||||
|
expect(btn).toHaveClass('inline-flex', 'justify-center', 'items-center', 'cursor-pointer')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('variants', () => {
|
describe('variants', () => {
|
||||||
it('applies default secondary variant', () => {
|
it('applies default secondary variant', () => {
|
||||||
render(<Button>Click me</Button>)
|
render(<Button>Click me</Button>)
|
||||||
expect(screen.getByRole('button').className).toContain('btn-secondary')
|
const btn = screen.getByRole('button')
|
||||||
|
expect(btn).toHaveClass('bg-components-button-secondary-bg', 'text-components-button-secondary-text')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
'primary',
|
{ variant: 'primary' as const, expectedClass: 'bg-components-button-primary-bg' },
|
||||||
'secondary',
|
{ variant: 'secondary' as const, expectedClass: 'bg-components-button-secondary-bg' },
|
||||||
'secondary-accent',
|
{ variant: 'secondary-accent' as const, expectedClass: 'text-components-button-secondary-accent-text' },
|
||||||
'ghost',
|
{ variant: 'ghost' as const, expectedClass: 'text-components-button-ghost-text' },
|
||||||
'ghost-accent',
|
{ variant: 'ghost-accent' as const, expectedClass: 'hover:bg-state-accent-hover' },
|
||||||
'tertiary',
|
{ variant: 'tertiary' as const, expectedClass: 'bg-components-button-tertiary-bg' },
|
||||||
] as const)('applies %s variant', (variant) => {
|
])('applies $variant variant', ({ variant, expectedClass }) => {
|
||||||
render(<Button variant={variant}>Click me</Button>)
|
render(<Button variant={variant}>Click me</Button>)
|
||||||
expect(screen.getByRole('button').className).toContain(`btn-${variant}`)
|
expect(screen.getByRole('button')).toHaveClass(expectedClass)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies destructive tone with default variant', () => {
|
it('applies destructive tone with default variant', () => {
|
||||||
render(<Button tone="destructive">Click me</Button>)
|
render(<Button tone="destructive">Click me</Button>)
|
||||||
expect(screen.getByRole('button').className).toContain('btn-destructive-secondary')
|
expect(screen.getByRole('button')).toHaveClass('bg-components-button-destructive-secondary-bg')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies destructive tone with primary variant', () => {
|
it('applies destructive tone with primary variant', () => {
|
||||||
render(<Button variant="primary" tone="destructive">Click me</Button>)
|
render(<Button variant="primary" tone="destructive">Click me</Button>)
|
||||||
expect(screen.getByRole('button').className).toContain('btn-destructive-primary')
|
expect(screen.getByRole('button')).toHaveClass('bg-components-button-destructive-primary-bg')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies destructive tone with tertiary variant', () => {
|
||||||
|
render(<Button variant="tertiary" tone="destructive">Click me</Button>)
|
||||||
|
expect(screen.getByRole('button')).toHaveClass('bg-components-button-destructive-tertiary-bg')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies destructive tone with ghost variant', () => {
|
||||||
|
render(<Button variant="ghost" tone="destructive">Click me</Button>)
|
||||||
|
expect(screen.getByRole('button')).toHaveClass('text-components-button-destructive-ghost-text')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('sizes', () => {
|
describe('sizes', () => {
|
||||||
it('applies default medium size', () => {
|
it('applies default medium size', () => {
|
||||||
render(<Button>Click me</Button>)
|
render(<Button>Click me</Button>)
|
||||||
expect(screen.getByRole('button').className).toContain('btn-medium')
|
expect(screen.getByRole('button')).toHaveClass('h-8', 'rounded-lg')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each(['small', 'medium', 'large'] as const)('applies %s size', (size) => {
|
it.each([
|
||||||
|
{ size: 'small' as const, expectedClass: 'h-6' },
|
||||||
|
{ size: 'medium' as const, expectedClass: 'h-8' },
|
||||||
|
{ size: 'large' as const, expectedClass: 'h-9' },
|
||||||
|
])('applies $size size', ({ size, expectedClass }) => {
|
||||||
render(<Button size={size}>Click me</Button>)
|
render(<Button size={size}>Click me</Button>)
|
||||||
expect(screen.getByRole('button').className).toContain(`btn-${size}`)
|
expect(screen.getByRole('button')).toHaveClass(expectedClass)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('loading', () => {
|
describe('loading', () => {
|
||||||
it('shows spinner when loading', () => {
|
it('shows spinner when loading', () => {
|
||||||
render(<Button loading>Click me</Button>)
|
render(<Button loading>Click me</Button>)
|
||||||
expect(screen.getByRole('button').querySelector('.animate-spin')).toBeInTheDocument()
|
expect(screen.getByRole('button').querySelector('[aria-hidden="true"]')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('hides spinner when not loading', () => {
|
it('hides spinner when not loading', () => {
|
||||||
render(<Button loading={false}>Click me</Button>)
|
render(<Button loading={false}>Click me</Button>)
|
||||||
expect(screen.getByRole('button').querySelector('.animate-spin')).not.toBeInTheDocument()
|
expect(screen.getByRole('button').querySelector('[aria-hidden="true"]')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('auto-disables when loading', () => {
|
it('auto-disables when loading', () => {
|
||||||
@ -137,6 +156,15 @@ describe('Button', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('className merging', () => {
|
||||||
|
it('merges custom className with variant classes', () => {
|
||||||
|
render(<Button className="custom-class">Click me</Button>)
|
||||||
|
const btn = screen.getByRole('button')
|
||||||
|
expect(btn).toHaveClass('custom-class')
|
||||||
|
expect(btn).toHaveClass('inline-flex')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('ref forwarding', () => {
|
describe('ref forwarding', () => {
|
||||||
it('forwards ref to the button element', () => {
|
it('forwards ref to the button element', () => {
|
||||||
let buttonRef: HTMLButtonElement | null = null
|
let buttonRef: HTMLButtonElement | null = null
|
||||||
|
|||||||
@ -1,148 +0,0 @@
|
|||||||
@utility btn {
|
|
||||||
@apply inline-flex justify-center items-center cursor-pointer whitespace-nowrap
|
|
||||||
outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply cursor-not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-small {
|
|
||||||
@apply px-2 h-6 rounded-md text-xs font-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-medium {
|
|
||||||
@apply px-3.5 h-8 rounded-lg text-[13px] leading-4 font-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-large {
|
|
||||||
@apply px-4 h-9 rounded-[10px] text-sm font-semibold;
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-primary {
|
|
||||||
@apply shadow
|
|
||||||
bg-components-button-primary-bg
|
|
||||||
border-components-button-primary-border
|
|
||||||
hover:bg-components-button-primary-bg-hover
|
|
||||||
hover:border-components-button-primary-border-hover
|
|
||||||
text-components-button-primary-text;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply shadow-none
|
|
||||||
bg-components-button-primary-bg-disabled
|
|
||||||
border-components-button-primary-border-disabled
|
|
||||||
text-components-button-primary-text-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-secondary {
|
|
||||||
@apply border-[0.5px]
|
|
||||||
shadow-xs
|
|
||||||
backdrop-blur-[5px]
|
|
||||||
bg-components-button-secondary-bg
|
|
||||||
border-components-button-secondary-border
|
|
||||||
hover:bg-components-button-secondary-bg-hover
|
|
||||||
hover:border-components-button-secondary-border-hover
|
|
||||||
text-components-button-secondary-text;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply backdrop-blur-xs
|
|
||||||
bg-components-button-secondary-bg-disabled
|
|
||||||
border-components-button-secondary-border-disabled
|
|
||||||
text-components-button-secondary-text-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-secondary-accent {
|
|
||||||
@apply border-[0.5px]
|
|
||||||
shadow-xs
|
|
||||||
bg-components-button-secondary-bg
|
|
||||||
border-components-button-secondary-border
|
|
||||||
hover:bg-components-button-secondary-bg-hover
|
|
||||||
hover:border-components-button-secondary-border-hover
|
|
||||||
text-components-button-secondary-accent-text;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply bg-components-button-secondary-bg-disabled
|
|
||||||
border-components-button-secondary-border-disabled
|
|
||||||
text-components-button-secondary-accent-text-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-tertiary {
|
|
||||||
@apply bg-components-button-tertiary-bg
|
|
||||||
hover:bg-components-button-tertiary-bg-hover
|
|
||||||
text-components-button-tertiary-text;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply bg-components-button-tertiary-bg-disabled
|
|
||||||
text-components-button-tertiary-text-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-ghost {
|
|
||||||
@apply hover:bg-components-button-ghost-bg-hover
|
|
||||||
text-components-button-ghost-text;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply text-components-button-ghost-text-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-ghost-accent {
|
|
||||||
@apply hover:bg-state-accent-hover
|
|
||||||
text-components-button-secondary-accent-text;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply text-components-button-secondary-accent-text-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-destructive-primary {
|
|
||||||
@apply bg-components-button-destructive-primary-bg
|
|
||||||
border-components-button-destructive-primary-border
|
|
||||||
hover:bg-components-button-destructive-primary-bg-hover
|
|
||||||
hover:border-components-button-destructive-primary-border-hover
|
|
||||||
text-components-button-destructive-primary-text;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply shadow-none
|
|
||||||
bg-components-button-destructive-primary-bg-disabled
|
|
||||||
border-components-button-destructive-primary-border-disabled
|
|
||||||
text-components-button-destructive-primary-text-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-destructive-secondary {
|
|
||||||
@apply bg-components-button-destructive-secondary-bg
|
|
||||||
border-components-button-destructive-secondary-border
|
|
||||||
hover:bg-components-button-destructive-secondary-bg-hover
|
|
||||||
hover:border-components-button-destructive-secondary-border-hover
|
|
||||||
text-components-button-destructive-secondary-text;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply bg-components-button-destructive-secondary-bg-disabled
|
|
||||||
border-components-button-destructive-secondary-border-disabled
|
|
||||||
text-components-button-destructive-secondary-text-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-destructive-tertiary {
|
|
||||||
@apply bg-components-button-destructive-tertiary-bg
|
|
||||||
hover:bg-components-button-destructive-tertiary-bg-hover
|
|
||||||
text-components-button-destructive-tertiary-text;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply bg-components-button-destructive-tertiary-bg-disabled
|
|
||||||
text-components-button-destructive-tertiary-text-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility btn-destructive-ghost {
|
|
||||||
@apply hover:bg-components-button-destructive-ghost-bg-hover
|
|
||||||
text-components-button-destructive-ghost-text;
|
|
||||||
|
|
||||||
&:is(:disabled, [data-disabled]) {
|
|
||||||
@apply text-components-button-destructive-ghost-text-disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,21 +5,47 @@ import { cva } from 'class-variance-authority'
|
|||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
'btn',
|
'inline-flex cursor-pointer items-center justify-center whitespace-nowrap outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid data-[disabled]:cursor-not-allowed',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
'primary': 'btn-primary',
|
'primary': [
|
||||||
'secondary': 'btn-secondary',
|
'border-components-button-primary-border bg-components-button-primary-bg text-components-button-primary-text shadow',
|
||||||
'secondary-accent': 'btn-secondary-accent',
|
'hover:border-components-button-primary-border-hover hover:bg-components-button-primary-bg-hover',
|
||||||
'ghost': 'btn-ghost',
|
'data-[disabled]:border-components-button-primary-border-disabled data-[disabled]:bg-components-button-primary-bg-disabled data-[disabled]:text-components-button-primary-text-disabled data-[disabled]:shadow-none',
|
||||||
'ghost-accent': 'btn-ghost-accent',
|
],
|
||||||
'tertiary': 'btn-tertiary',
|
'secondary': [
|
||||||
|
'border-[0.5px] shadow-xs backdrop-blur-[5px]',
|
||||||
|
'border-components-button-secondary-border bg-components-button-secondary-bg text-components-button-secondary-text',
|
||||||
|
'hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover',
|
||||||
|
'data-[disabled]:border-components-button-secondary-border-disabled data-[disabled]:bg-components-button-secondary-bg-disabled data-[disabled]:text-components-button-secondary-text-disabled data-[disabled]:backdrop-blur-xs',
|
||||||
|
],
|
||||||
|
'secondary-accent': [
|
||||||
|
'border-[0.5px] shadow-xs',
|
||||||
|
'border-components-button-secondary-border bg-components-button-secondary-bg text-components-button-secondary-accent-text',
|
||||||
|
'hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover',
|
||||||
|
'data-[disabled]:border-components-button-secondary-border-disabled data-[disabled]:bg-components-button-secondary-bg-disabled data-[disabled]:text-components-button-secondary-accent-text-disabled',
|
||||||
|
],
|
||||||
|
'tertiary': [
|
||||||
|
'bg-components-button-tertiary-bg text-components-button-tertiary-text',
|
||||||
|
'hover:bg-components-button-tertiary-bg-hover',
|
||||||
|
'data-[disabled]:bg-components-button-tertiary-bg-disabled data-[disabled]:text-components-button-tertiary-text-disabled',
|
||||||
|
],
|
||||||
|
'ghost': [
|
||||||
|
'text-components-button-ghost-text',
|
||||||
|
'hover:bg-components-button-ghost-bg-hover',
|
||||||
|
'data-[disabled]:text-components-button-ghost-text-disabled',
|
||||||
|
],
|
||||||
|
'ghost-accent': [
|
||||||
|
'text-components-button-secondary-accent-text',
|
||||||
|
'hover:bg-state-accent-hover',
|
||||||
|
'data-[disabled]:text-components-button-secondary-accent-text-disabled',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
small: 'btn-small',
|
small: 'h-6 rounded-md px-2 text-xs font-medium',
|
||||||
medium: 'btn-medium',
|
medium: 'h-8 rounded-lg px-3.5 text-[13px] leading-4 font-medium',
|
||||||
large: 'btn-large',
|
large: 'h-9 rounded-[10px] px-4 text-sm font-semibold',
|
||||||
},
|
},
|
||||||
tone: {
|
tone: {
|
||||||
default: '',
|
default: '',
|
||||||
@ -27,10 +53,42 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
compoundVariants: [
|
compoundVariants: [
|
||||||
{ variant: 'primary', tone: 'destructive', class: 'btn-destructive-primary' },
|
{
|
||||||
{ variant: 'secondary', tone: 'destructive', class: 'btn-destructive-secondary' },
|
variant: 'primary',
|
||||||
{ variant: 'tertiary', tone: 'destructive', class: 'btn-destructive-tertiary' },
|
tone: 'destructive',
|
||||||
{ variant: 'ghost', tone: 'destructive', class: 'btn-destructive-ghost' },
|
class: [
|
||||||
|
'border-components-button-destructive-primary-border bg-components-button-destructive-primary-bg text-components-button-destructive-primary-text',
|
||||||
|
'hover:border-components-button-destructive-primary-border-hover hover:bg-components-button-destructive-primary-bg-hover',
|
||||||
|
'data-[disabled]:border-components-button-destructive-primary-border-disabled data-[disabled]:bg-components-button-destructive-primary-bg-disabled data-[disabled]:text-components-button-destructive-primary-text-disabled data-[disabled]:shadow-none',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'secondary',
|
||||||
|
tone: 'destructive',
|
||||||
|
class: [
|
||||||
|
'border-components-button-destructive-secondary-border bg-components-button-destructive-secondary-bg text-components-button-destructive-secondary-text',
|
||||||
|
'hover:border-components-button-destructive-secondary-border-hover hover:bg-components-button-destructive-secondary-bg-hover',
|
||||||
|
'data-[disabled]:border-components-button-destructive-secondary-border-disabled data-[disabled]:bg-components-button-destructive-secondary-bg-disabled data-[disabled]:text-components-button-destructive-secondary-text-disabled',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'tertiary',
|
||||||
|
tone: 'destructive',
|
||||||
|
class: [
|
||||||
|
'bg-components-button-destructive-tertiary-bg text-components-button-destructive-tertiary-text',
|
||||||
|
'hover:bg-components-button-destructive-tertiary-bg-hover',
|
||||||
|
'data-[disabled]:bg-components-button-destructive-tertiary-bg-disabled data-[disabled]:text-components-button-destructive-tertiary-text-disabled',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: 'ghost',
|
||||||
|
tone: 'destructive',
|
||||||
|
class: [
|
||||||
|
'text-components-button-destructive-ghost-text',
|
||||||
|
'hover:bg-components-button-destructive-ghost-bg-hover',
|
||||||
|
'data-[disabled]:text-components-button-destructive-ghost-text-disabled',
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'secondary',
|
variant: 'secondary',
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
|
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
|
||||||
DialogCloseButton,
|
DialogCloseButton,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogPortal,
|
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from '../index'
|
} from '../index'
|
||||||
|
|
||||||
describe('Dialog wrapper', () => {
|
describe('Dialog wrapper', () => {
|
||||||
@ -86,15 +82,4 @@ describe('Dialog wrapper', () => {
|
|||||||
expect(onClick).not.toHaveBeenCalled()
|
expect(onClick).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Exports', () => {
|
|
||||||
it('should map dialog aliases to the matching base dialog primitives', () => {
|
|
||||||
expect(Dialog).toBe(BaseDialog.Root)
|
|
||||||
expect(DialogTrigger).toBe(BaseDialog.Trigger)
|
|
||||||
expect(DialogTitle).toBe(BaseDialog.Title)
|
|
||||||
expect(DialogDescription).toBe(BaseDialog.Description)
|
|
||||||
expect(DialogClose).toBe(BaseDialog.Close)
|
|
||||||
expect(DialogPortal).toBe(BaseDialog.Portal)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -12,10 +12,10 @@ import * as React from 'react'
|
|||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
|
||||||
export const Dialog = BaseDialog.Root
|
export const Dialog = BaseDialog.Root
|
||||||
|
/** @public */
|
||||||
export const DialogTrigger = BaseDialog.Trigger
|
export const DialogTrigger = BaseDialog.Trigger
|
||||||
export const DialogTitle = BaseDialog.Title
|
export const DialogTitle = BaseDialog.Title
|
||||||
export const DialogDescription = BaseDialog.Description
|
export const DialogDescription = BaseDialog.Description
|
||||||
export const DialogClose = BaseDialog.Close
|
|
||||||
export const DialogPortal = BaseDialog.Portal
|
export const DialogPortal = BaseDialog.Portal
|
||||||
|
|
||||||
type DialogCloseButtonProps = Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Close>, 'children'>
|
type DialogCloseButtonProps = Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Close>, 'children'>
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react'
|
|
||||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import Link from '@/next/link'
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -14,21 +12,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '../index'
|
} from '../index'
|
||||||
|
|
||||||
vi.mock('@/next/link', () => ({
|
|
||||||
default: ({
|
|
||||||
href,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
href: string
|
|
||||||
children?: ReactNode
|
|
||||||
} & Omit<ComponentPropsWithoutRef<'a'>, 'href'>) => (
|
|
||||||
<a href={href} {...props}>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('dropdown-menu wrapper', () => {
|
describe('dropdown-menu wrapper', () => {
|
||||||
describe('DropdownMenuContent', () => {
|
describe('DropdownMenuContent', () => {
|
||||||
it('should position content at bottom-end with default placement when props are omitted', () => {
|
it('should position content at bottom-end with default placement when props are omitted', () => {
|
||||||
@ -295,13 +278,13 @@ describe('dropdown-menu wrapper', () => {
|
|||||||
expect(link).not.toHaveAttribute('closeOnClick')
|
expect(link).not.toHaveAttribute('closeOnClick')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve link semantics when render prop uses a custom link component', () => {
|
it('should preserve link semantics when render prop uses a custom anchor element', () => {
|
||||||
render(
|
render(
|
||||||
<DropdownMenu open>
|
<DropdownMenu open>
|
||||||
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
<DropdownMenuTrigger aria-label="menu trigger">Open</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuLinkItem
|
<DropdownMenuLinkItem
|
||||||
render={<Link href="/account" />}
|
render={<a href="/account" />}
|
||||||
aria-label="account link"
|
aria-label="account link"
|
||||||
>
|
>
|
||||||
Account settings
|
Account settings
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import type {
|
|||||||
NumberFieldInputProps,
|
NumberFieldInputProps,
|
||||||
NumberFieldUnitProps,
|
NumberFieldUnitProps,
|
||||||
} from '../index'
|
} from '../index'
|
||||||
import { NumberField as BaseNumberField } from '@base-ui/react/number-field'
|
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import {
|
import {
|
||||||
NumberField,
|
NumberField,
|
||||||
@ -67,17 +66,6 @@ const renderNumberField = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('NumberField wrapper', () => {
|
describe('NumberField wrapper', () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Export mapping should stay aligned with the Base UI primitive.
|
|
||||||
describe('Exports', () => {
|
|
||||||
it('should map NumberField to the matching base primitive root', () => {
|
|
||||||
expect(NumberField).toBe(BaseNumberField.Root)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Group and input wrappers should preserve the design-system variants and DOM defaults.
|
// Group and input wrappers should preserve the design-system variants and DOM defaults.
|
||||||
describe('Group and input', () => {
|
describe('Group and input', () => {
|
||||||
it('should apply regular group classes by default and merge custom className', () => {
|
it('should apply regular group classes by default and merge custom className', () => {
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
import { Popover as BasePopover } from '@base-ui/react/popover'
|
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverClose,
|
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverDescription,
|
|
||||||
PopoverTitle,
|
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '..'
|
} from '..'
|
||||||
|
|
||||||
@ -93,15 +89,3 @@ describe('PopoverContent', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Popover aliases', () => {
|
|
||||||
describe('Export mapping', () => {
|
|
||||||
it('should map aliases to the matching base popover primitives when wrapper exports are imported', () => {
|
|
||||||
expect(Popover).toBe(BasePopover.Root)
|
|
||||||
expect(PopoverTrigger).toBe(BasePopover.Trigger)
|
|
||||||
expect(PopoverClose).toBe(BasePopover.Close)
|
|
||||||
expect(PopoverTitle).toBe(BasePopover.Title)
|
|
||||||
expect(PopoverDescription).toBe(BasePopover.Description)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@ -9,7 +9,9 @@ import { cn } from '@/utils/classnames'
|
|||||||
export const Popover = BasePopover.Root
|
export const Popover = BasePopover.Root
|
||||||
export const PopoverTrigger = BasePopover.Trigger
|
export const PopoverTrigger = BasePopover.Trigger
|
||||||
export const PopoverClose = BasePopover.Close
|
export const PopoverClose = BasePopover.Close
|
||||||
|
/** @public */
|
||||||
export const PopoverTitle = BasePopover.Title
|
export const PopoverTitle = BasePopover.Title
|
||||||
|
/** @public */
|
||||||
export const PopoverDescription = BasePopover.Description
|
export const PopoverDescription = BasePopover.Description
|
||||||
|
|
||||||
type PopoverContentProps = {
|
type PopoverContentProps = {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
ScrollAreaThumb,
|
ScrollAreaThumb,
|
||||||
ScrollAreaViewport,
|
ScrollAreaViewport,
|
||||||
} from '../index'
|
} from '../index'
|
||||||
import styles from '../index.module.css'
|
|
||||||
|
|
||||||
const renderScrollArea = (options: {
|
const renderScrollArea = (options: {
|
||||||
rootClassName?: string
|
rootClassName?: string
|
||||||
@ -106,7 +105,7 @@ describe('scroll-area wrapper', () => {
|
|||||||
const thumb = screen.getByTestId('scroll-area-vertical-thumb')
|
const thumb = screen.getByTestId('scroll-area-vertical-thumb')
|
||||||
|
|
||||||
expect(scrollbar).toHaveAttribute('data-orientation', 'vertical')
|
expect(scrollbar).toHaveAttribute('data-orientation', 'vertical')
|
||||||
expect(scrollbar).toHaveClass(styles.scrollbar)
|
expect(scrollbar).toHaveAttribute('data-dify-scrollbar')
|
||||||
expect(scrollbar).toHaveClass(
|
expect(scrollbar).toHaveClass(
|
||||||
'flex',
|
'flex',
|
||||||
'overflow-clip',
|
'overflow-clip',
|
||||||
@ -144,7 +143,7 @@ describe('scroll-area wrapper', () => {
|
|||||||
const thumb = screen.getByTestId('scroll-area-horizontal-thumb')
|
const thumb = screen.getByTestId('scroll-area-horizontal-thumb')
|
||||||
|
|
||||||
expect(scrollbar).toHaveAttribute('data-orientation', 'horizontal')
|
expect(scrollbar).toHaveAttribute('data-orientation', 'horizontal')
|
||||||
expect(scrollbar).toHaveClass(styles.scrollbar)
|
expect(scrollbar).toHaveAttribute('data-dify-scrollbar')
|
||||||
expect(scrollbar).toHaveClass(
|
expect(scrollbar).toHaveClass(
|
||||||
'flex',
|
'flex',
|
||||||
'overflow-clip',
|
'overflow-clip',
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area'
|
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import styles from './index.module.css'
|
import './scroll-area.css'
|
||||||
|
|
||||||
export const ScrollAreaRoot = BaseScrollArea.Root
|
export const ScrollAreaRoot = BaseScrollArea.Root
|
||||||
type ScrollAreaRootProps = React.ComponentPropsWithRef<typeof BaseScrollArea.Root>
|
type ScrollAreaRootProps = React.ComponentPropsWithRef<typeof BaseScrollArea.Root>
|
||||||
@ -25,7 +25,6 @@ type ScrollAreaProps = Omit<ScrollAreaRootProps, 'children'> & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scrollAreaScrollbarClassName = cn(
|
const scrollAreaScrollbarClassName = cn(
|
||||||
styles.scrollbar,
|
|
||||||
'flex touch-none overflow-clip p-1 opacity-100 transition-opacity select-none motion-reduce:transition-none',
|
'flex touch-none overflow-clip p-1 opacity-100 transition-opacity select-none motion-reduce:transition-none',
|
||||||
'pointer-events-none data-hovering:pointer-events-auto',
|
'pointer-events-none data-hovering:pointer-events-auto',
|
||||||
'data-scrolling:pointer-events-auto',
|
'data-scrolling:pointer-events-auto',
|
||||||
@ -68,6 +67,7 @@ export function ScrollAreaScrollbar({
|
|||||||
}: ScrollAreaScrollbarProps) {
|
}: ScrollAreaScrollbarProps) {
|
||||||
return (
|
return (
|
||||||
<BaseScrollArea.Scrollbar
|
<BaseScrollArea.Scrollbar
|
||||||
|
data-dify-scrollbar=""
|
||||||
className={cn(scrollAreaScrollbarClassName, className)}
|
className={cn(scrollAreaScrollbarClassName, className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.scrollbar::before,
|
[data-dify-scrollbar]::before,
|
||||||
.scrollbar::after {
|
[data-dify-scrollbar]::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -9,7 +9,7 @@
|
|||||||
transition: opacity 150ms ease;
|
transition: opacity 150ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar[data-orientation='vertical']::before {
|
[data-dify-scrollbar][data-orientation='vertical']::before {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
width: 4px;
|
width: 4px;
|
||||||
@ -18,7 +18,7 @@
|
|||||||
background: linear-gradient(to bottom, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
background: linear-gradient(to bottom, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar[data-orientation='vertical']::after {
|
[data-dify-scrollbar][data-orientation='vertical']::after {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
width: 4px;
|
width: 4px;
|
||||||
@ -27,7 +27,7 @@
|
|||||||
background: linear-gradient(to top, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
background: linear-gradient(to top, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar[data-orientation='horizontal']::before {
|
[data-dify-scrollbar][data-orientation='horizontal']::before {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 4px;
|
left: 4px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
@ -36,7 +36,7 @@
|
|||||||
background: linear-gradient(to right, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
background: linear-gradient(to right, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar[data-orientation='horizontal']::after {
|
[data-dify-scrollbar][data-orientation='horizontal']::after {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
@ -45,31 +45,31 @@
|
|||||||
background: linear-gradient(to left, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
background: linear-gradient(to left, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar[data-orientation='vertical']:not([data-overflow-y-start])::before {
|
[data-dify-scrollbar][data-orientation='vertical']:not([data-overflow-y-start])::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar[data-orientation='vertical']:not([data-overflow-y-end])::after {
|
[data-dify-scrollbar][data-orientation='vertical']:not([data-overflow-y-end])::after {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar[data-orientation='horizontal']:not([data-overflow-x-start])::before {
|
[data-dify-scrollbar][data-orientation='horizontal']:not([data-overflow-x-start])::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar[data-orientation='horizontal']:not([data-overflow-x-end])::after {
|
[data-dify-scrollbar][data-orientation='horizontal']:not([data-overflow-x-end])::after {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar[data-hovering] > [data-orientation],
|
[data-dify-scrollbar][data-hovering] > [data-orientation],
|
||||||
.scrollbar[data-scrolling] > [data-orientation],
|
[data-dify-scrollbar][data-scrolling] > [data-orientation],
|
||||||
.scrollbar > [data-orientation]:active {
|
[data-dify-scrollbar] > [data-orientation]:active {
|
||||||
background-color: var(--scroll-area-thumb-bg-active, var(--color-state-base-handle-hover));
|
background-color: var(--scroll-area-thumb-bg-active, var(--color-state-base-handle-hover));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.scrollbar::before,
|
[data-dify-scrollbar]::before,
|
||||||
.scrollbar::after {
|
[data-dify-scrollbar]::after {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip'
|
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../index'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../index'
|
||||||
|
|
||||||
describe('TooltipContent', () => {
|
describe('TooltipContent', () => {
|
||||||
describe('Placement and offsets', () => {
|
describe('Placement and offsets', () => {
|
||||||
@ -105,11 +104,3 @@ describe('TooltipContent', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Tooltip aliases', () => {
|
|
||||||
it('should map alias exports to BaseTooltip components when wrapper exports are imported', () => {
|
|
||||||
expect(TooltipProvider).toBe(BaseTooltip.Provider)
|
|
||||||
expect(Tooltip).toBe(BaseTooltip.Root)
|
|
||||||
expect(TooltipTrigger).toBe(BaseTooltip.Trigger)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@ -85,21 +85,6 @@ describe('Actions', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Button Variants Tests
|
|
||||||
describe('Button Variants', () => {
|
|
||||||
it('should have primary variant for choose button', () => {
|
|
||||||
render(<Actions {...defaultProps} />)
|
|
||||||
const chooseButton = screen.getByText(/operations\.choose/i).closest('button')
|
|
||||||
expect(chooseButton).toHaveClass('btn-primary')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have secondary variant for details button', () => {
|
|
||||||
render(<Actions {...defaultProps} />)
|
|
||||||
const detailsButton = screen.getByText(/operations\.details/i).closest('button')
|
|
||||||
expect(detailsButton).toHaveClass('btn-secondary')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Layout', () => {
|
describe('Layout', () => {
|
||||||
it('should have absolute positioning', () => {
|
it('should have absolute positioning', () => {
|
||||||
const { container } = render(<Actions {...defaultProps} />)
|
const { container } = render(<Actions {...defaultProps} />)
|
||||||
|
|||||||
@ -160,24 +160,6 @@ describe('CSVUploader', () => {
|
|||||||
expect(mockUpdateFile).toHaveBeenCalled()
|
expect(mockUpdateFile).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call updateFile with undefined when remove is clicked', () => {
|
|
||||||
const mockUpdateFile = vi.fn()
|
|
||||||
const mockFile: FileItem = {
|
|
||||||
fileID: 'file-1',
|
|
||||||
file: new File(['content'], 'test.csv', { type: 'text/csv' }) as CustomFile,
|
|
||||||
progress: 100,
|
|
||||||
}
|
|
||||||
const { container } = render(
|
|
||||||
<CSVUploader {...defaultProps} file={mockFile} updateFile={mockUpdateFile} />,
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteButton = container.querySelector('.cursor-pointer')
|
|
||||||
if (deleteButton)
|
|
||||||
fireEvent.click(deleteButton)
|
|
||||||
|
|
||||||
expect(mockUpdateFile).toHaveBeenCalledWith()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Validation', () => {
|
describe('Validation', () => {
|
||||||
|
|||||||
@ -187,10 +187,7 @@ describe('EditMetadataBatchModal', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Find the primary save button (not the one in SelectMetadataModal)
|
// Find the primary save button (not the one in SelectMetadataModal)
|
||||||
const saveButtons = screen.getAllByText(/save/i)
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||||
const modalSaveButton = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
|
|
||||||
if (modalSaveButton)
|
|
||||||
fireEvent.click(modalSaveButton)
|
|
||||||
|
|
||||||
expect(onSave).toHaveBeenCalled()
|
expect(onSave).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -443,13 +440,10 @@ describe('EditMetadataBatchModal', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Find the primary save button
|
// Find the primary save button
|
||||||
const saveButtons = screen.getAllByText(/save/i)
|
const saveBtn = screen.getByRole('button', { name: 'common.operation.save' })
|
||||||
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
|
fireEvent.click(saveBtn)
|
||||||
if (saveBtn) {
|
fireEvent.click(saveBtn)
|
||||||
fireEvent.click(saveBtn)
|
fireEvent.click(saveBtn)
|
||||||
fireEvent.click(saveBtn)
|
|
||||||
fireEvent.click(saveBtn)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(onSave).toHaveBeenCalledTimes(3)
|
expect(onSave).toHaveBeenCalledTimes(3)
|
||||||
})
|
})
|
||||||
@ -462,10 +456,7 @@ describe('EditMetadataBatchModal', () => {
|
|||||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveButtons = screen.getAllByText(/save/i)
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||||
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
|
|
||||||
if (saveBtn)
|
|
||||||
fireEvent.click(saveBtn)
|
|
||||||
|
|
||||||
expect(onSave).toHaveBeenCalledWith(
|
expect(onSave).toHaveBeenCalledWith(
|
||||||
expect.any(Array),
|
expect.any(Array),
|
||||||
@ -486,10 +477,7 @@ describe('EditMetadataBatchModal', () => {
|
|||||||
if (checkboxContainer)
|
if (checkboxContainer)
|
||||||
fireEvent.click(checkboxContainer)
|
fireEvent.click(checkboxContainer)
|
||||||
|
|
||||||
const saveButtons = screen.getAllByText(/save/i)
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||||
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
|
|
||||||
if (saveBtn)
|
|
||||||
fireEvent.click(saveBtn)
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onSave).toHaveBeenCalledWith(
|
expect(onSave).toHaveBeenCalledWith(
|
||||||
@ -511,10 +499,7 @@ describe('EditMetadataBatchModal', () => {
|
|||||||
// Remove an item
|
// Remove an item
|
||||||
fireEvent.click(screen.getByTestId('remove-1'))
|
fireEvent.click(screen.getByTestId('remove-1'))
|
||||||
|
|
||||||
const saveButtons = screen.getAllByText(/save/i)
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||||
const saveBtn = saveButtons.find(btn => btn.closest('button')?.classList.contains('btn-primary'))
|
|
||||||
if (saveBtn)
|
|
||||||
fireEvent.click(saveBtn)
|
|
||||||
|
|
||||||
expect(onSave).toHaveBeenCalled()
|
expect(onSave).toHaveBeenCalled()
|
||||||
// The first argument should not contain the deleted item (id '1')
|
// The first argument should not contain the deleted item (id '1')
|
||||||
|
|||||||
@ -284,12 +284,7 @@ describe('DatasetMetadataDrawer', () => {
|
|||||||
fireEvent.change(inputs[0], { target: { value: 'renamed_field' } })
|
fireEvent.change(inputs[0], { target: { value: 'renamed_field' } })
|
||||||
|
|
||||||
// Find and click save button
|
// Find and click save button
|
||||||
const saveBtns = screen.getAllByText(/save/i)
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||||
const primaryBtn = saveBtns.find(btn =>
|
|
||||||
btn.closest('button')?.classList.contains('btn-primary'),
|
|
||||||
)
|
|
||||||
if (primaryBtn)
|
|
||||||
fireEvent.click(primaryBtn)
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onRename).toHaveBeenCalled()
|
expect(onRename).toHaveBeenCalled()
|
||||||
|
|||||||
@ -139,18 +139,6 @@ describe('SecretKeyButton', () => {
|
|||||||
const button = screen.getByRole('button')
|
const button = screen.getByRole('button')
|
||||||
expect(button.className).toContain('px-3')
|
expect(button.className).toContain('px-3')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have small size', () => {
|
|
||||||
render(<SecretKeyButton />)
|
|
||||||
const button = screen.getByRole('button')
|
|
||||||
expect(button.className).toContain('btn-small')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have ghost variant', () => {
|
|
||||||
render(<SecretKeyButton />)
|
|
||||||
const button = screen.getByRole('button')
|
|
||||||
expect(button.className).toContain('btn-ghost')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('icon styling', () => {
|
describe('icon styling', () => {
|
||||||
|
|||||||
@ -120,12 +120,6 @@ describe('SystemModel', () => {
|
|||||||
expect(screen.getByRole('button', { name: /system model settings/i })).toBeDisabled()
|
expect(screen.getByRole('button', { name: /system model settings/i })).toBeDisabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render the primary button variant when configuration is required', () => {
|
|
||||||
render(<SystemModel {...defaultProps} notConfigured />)
|
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: /system model settings/i })).toHaveClass('btn-primary')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should close dialog when cancel is clicked', async () => {
|
it('should close dialog when cancel is clicked', async () => {
|
||||||
render(<SystemModel {...defaultProps} />)
|
render(<SystemModel {...defaultProps} />)
|
||||||
fireEvent.click(screen.getByRole('button', { name: /system model settings/i }))
|
fireEvent.click(screen.getByRole('button', { name: /system model settings/i }))
|
||||||
|
|||||||
@ -156,29 +156,6 @@ describe('AddApiKeyButton', () => {
|
|||||||
|
|
||||||
expect(screen.getByRole('button')).toHaveTextContent('Custom API Key')
|
expect(screen.getByRole('button')).toHaveTextContent('Custom API Key')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should apply button variant', () => {
|
|
||||||
const pluginPayload = createPluginPayload()
|
|
||||||
|
|
||||||
render(
|
|
||||||
<AddApiKeyButton
|
|
||||||
pluginPayload={pluginPayload}
|
|
||||||
buttonVariant="primary"
|
|
||||||
/>,
|
|
||||||
{ wrapper: createWrapper() },
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(screen.getByRole('button').className).toContain('btn-primary')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should use secondary-accent variant by default', () => {
|
|
||||||
const pluginPayload = createPluginPayload()
|
|
||||||
|
|
||||||
render(<AddApiKeyButton pluginPayload={pluginPayload} />, { wrapper: createWrapper() })
|
|
||||||
|
|
||||||
// Verify the default button has secondary-accent variant class
|
|
||||||
expect(screen.getByRole('button').className).toContain('btn-secondary-accent')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Props Testing', () => {
|
describe('Props Testing', () => {
|
||||||
@ -372,25 +349,6 @@ describe('AddOAuthButton', () => {
|
|||||||
|
|
||||||
expect(screen.getByText('plugin.auth.setupOAuth')).toBeInTheDocument()
|
expect(screen.getByText('plugin.auth.setupOAuth')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should apply button variant to setup button', () => {
|
|
||||||
const pluginPayload = createPluginPayload()
|
|
||||||
mockGetPluginOAuthClientSchema.mockReturnValue({
|
|
||||||
schema: [],
|
|
||||||
is_oauth_custom_client_enabled: false,
|
|
||||||
is_system_oauth_params_exists: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
render(
|
|
||||||
<AddOAuthButton
|
|
||||||
pluginPayload={pluginPayload}
|
|
||||||
buttonVariant="secondary"
|
|
||||||
/>,
|
|
||||||
{ wrapper: createWrapper() },
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(screen.getByRole('button').className).toContain('btn-secondary')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Rendering - Configured State', () => {
|
describe('Rendering - Configured State', () => {
|
||||||
|
|||||||
@ -88,23 +88,6 @@ describe('VersionMismatchModal', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('button variants', () => {
|
|
||||||
it('should render cancel button with secondary variant', () => {
|
|
||||||
render(<VersionMismatchModal {...defaultProps} />)
|
|
||||||
|
|
||||||
const cancelBtn = screen.getByRole('button', { name: /app\.newApp\.Cancel/ })
|
|
||||||
expect(cancelBtn).toHaveClass('btn-secondary')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render confirm button with primary destructive variant', () => {
|
|
||||||
render(<VersionMismatchModal {...defaultProps} />)
|
|
||||||
|
|
||||||
const confirmBtn = screen.getByRole('button', { name: /app\.newApp\.Confirm/ })
|
|
||||||
expect(confirmBtn).toHaveClass('btn-primary')
|
|
||||||
expect(confirmBtn).toHaveClass('btn-destructive-primary')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('edge cases', () => {
|
describe('edge cases', () => {
|
||||||
it('should handle undefined versions gracefully', () => {
|
it('should handle undefined versions gracefully', () => {
|
||||||
render(<VersionMismatchModal {...defaultProps} versions={undefined} />)
|
render(<VersionMismatchModal {...defaultProps} versions={undefined} />)
|
||||||
|
|||||||
@ -157,16 +157,6 @@ describe('ConfirmModal', () => {
|
|||||||
// Act & Assert - This will fail the test if user.click throws an unhandled error
|
// Act & Assert - This will fail the test if user.click throws an unhandled error
|
||||||
await user.click(confirmButton)
|
await user.click(confirmButton)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have correct button variants', () => {
|
|
||||||
// Arrange & Act
|
|
||||||
renderComponent()
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const confirmButton = screen.getByText('common.operation.confirm')
|
|
||||||
expect(confirmButton).toHaveClass('btn-primary')
|
|
||||||
expect(confirmButton).toHaveClass('btn-destructive-primary')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Edge Cases (REQUIRED)
|
// Edge Cases (REQUIRED)
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
@import '../components/base/action-button/index.css';
|
@import '../components/base/action-button/index.css';
|
||||||
@import '../components/base/badge/index.css';
|
@import '../components/base/badge/index.css';
|
||||||
@import '../components/base/ui/button/index.css';
|
|
||||||
@import '../components/base/modal/index.css' layer(base);
|
@import '../components/base/modal/index.css' layer(base);
|
||||||
@import '../components/base/premium-badge/index.css';
|
@import '../components/base/premium-badge/index.css';
|
||||||
@import '../components/base/segmented-control/index.css';
|
@import '../components/base/segmented-control/index.css';
|
||||||
|
|||||||
@ -3314,11 +3314,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/base/ui/avatar/index.tsx": {
|
|
||||||
"tailwindcss/enforce-consistent-class-order": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"app/components/base/video-gallery/VideoPlayer.tsx": {
|
"app/components/base/video-gallery/VideoPlayer.tsx": {
|
||||||
"react/set-state-in-effect": {
|
"react/set-state-in-effect": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user