refactor: migrate workflow onboarding modal to base dialog (#32915)

This commit is contained in:
yyh 2026-03-04 11:13:43 +08:00 committed by GitHub
parent 3398962bfa
commit b68ee600c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 239 additions and 437 deletions

View File

@ -1,11 +1,13 @@
import { Dialog as BaseDialog } from '@base-ui/react/dialog' import { Dialog as BaseDialog } from '@base-ui/react/dialog'
import { render, screen } from '@testing-library/react' import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
DialogCloseButton,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogPortal,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '../index' } from '../index'
@ -29,7 +31,7 @@ describe('Dialog wrapper', () => {
}) })
describe('Props', () => { describe('Props', () => {
it('should not render close button when closable is omitted', () => { it('should not render close button when DialogCloseButton is not provided', () => {
render( render(
<Dialog open> <Dialog open>
<DialogContent> <DialogContent>
@ -41,20 +43,47 @@ describe('Dialog wrapper', () => {
expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument() expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
}) })
it('should render close button when closable is true', () => { it('should render explicit close button with custom aria-label', () => {
render( render(
<Dialog open> <Dialog open>
<DialogContent closable> <DialogContent>
<DialogCloseButton aria-label="Dismiss dialog" />
<span>Dialog body</span> <span>Dialog body</span>
</DialogContent> </DialogContent>
</Dialog>, </Dialog>,
) )
const dialog = screen.getByRole('dialog') expect(screen.getByRole('button', { name: 'Dismiss dialog' })).toBeInTheDocument()
const closeButton = screen.getByRole('button', { name: 'Close' }) })
expect(dialog).toContainElement(closeButton) it('should render default close button label when aria-label is omitted', () => {
expect(closeButton).toHaveAttribute('aria-label', 'Close') render(
<Dialog open>
<DialogContent>
<DialogCloseButton />
<span>Dialog body</span>
</DialogContent>
</Dialog>,
)
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument()
})
it('should forward close button props to base primitive', () => {
const onClick = vi.fn()
render(
<Dialog open>
<DialogContent>
<DialogCloseButton data-testid="close-button" disabled onClick={onClick} />
<span>Dialog body</span>
</DialogContent>
</Dialog>,
)
const closeButton = screen.getByTestId('close-button')
expect(closeButton).toBeDisabled()
fireEvent.click(closeButton)
expect(onClick).not.toHaveBeenCalled()
}) })
}) })
@ -65,6 +94,7 @@ describe('Dialog wrapper', () => {
expect(DialogTitle).toBe(BaseDialog.Title) expect(DialogTitle).toBe(BaseDialog.Title)
expect(DialogDescription).toBe(BaseDialog.Description) expect(DialogDescription).toBe(BaseDialog.Description)
expect(DialogClose).toBe(BaseDialog.Close) expect(DialogClose).toBe(BaseDialog.Close)
expect(DialogPortal).toBe(BaseDialog.Portal)
}) })
}) })
}) })

View File

@ -16,22 +16,42 @@ 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 DialogClose = BaseDialog.Close
export const DialogPortal = BaseDialog.Portal
type DialogCloseButtonProps = Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Close>, 'children'>
export function DialogCloseButton({
className,
'aria-label': ariaLabel = 'Close',
...props
}: DialogCloseButtonProps) {
return (
<BaseDialog.Close
aria-label={ariaLabel}
{...props}
className={cn(
'absolute right-6 top-6 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-2xl hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-components-input-border-hover disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
>
<span aria-hidden="true" className="i-ri-close-line h-4 w-4 text-text-tertiary" />
</BaseDialog.Close>
)
}
type DialogContentProps = { type DialogContentProps = {
children: React.ReactNode children: React.ReactNode
className?: string className?: string
overlayClassName?: string overlayClassName?: string
closable?: boolean
} }
export function DialogContent({ export function DialogContent({
children, children,
className, className,
overlayClassName, overlayClassName,
closable = false,
}: DialogContentProps) { }: DialogContentProps) {
return ( return (
<BaseDialog.Portal> <DialogPortal>
<BaseDialog.Backdrop <BaseDialog.Backdrop
className={cn( className={cn(
'fixed inset-0 z-50 bg-background-overlay', 'fixed inset-0 z-50 bg-background-overlay',
@ -41,18 +61,13 @@ export function DialogContent({
/> />
<BaseDialog.Popup <BaseDialog.Popup
className={cn( className={cn(
'fixed left-1/2 top-1/2 z-50 max-h-[80dvh] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl', 'fixed left-1/2 top-1/2 z-50 max-h-[80dvh] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
'transition-[transform,scale,opacity] duration-150 data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none', 'transition-[transform,scale,opacity] duration-150 data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none',
className, className,
)} )}
> >
{closable && (
<BaseDialog.Close aria-label="Close" className="absolute right-6 top-6 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-2xl hover:bg-state-base-hover">
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
</BaseDialog.Close>
)}
{children} {children}
</BaseDialog.Popup> </BaseDialog.Popup>
</BaseDialog.Portal> </DialogPortal>
) )
} }

View File

@ -147,7 +147,6 @@ const WorkflowChildren = () => {
handleSyncWorkflowDraft(true, false, { handleSyncWorkflowDraft(true, false, {
onSuccess: () => { onSuccess: () => {
autoGenerateWebhookUrl(newNode.id) autoGenerateWebhookUrl(newNode.id)
console.log('Node successfully saved to draft')
}, },
onError: () => { onError: () => {
console.error('Failed to save node to draft') console.error('Failed to save node to draft')

View File

@ -1,65 +1,32 @@
import type { ReactNode } from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { BlockEnum } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types'
import WorkflowOnboardingModal from '../index' import WorkflowOnboardingModal from '../index'
// Mock Modal component vi.mock('@/app/components/workflow/block-selector', () => ({
vi.mock('@/app/components/base/modal', () => ({ default: function MockNodeSelector({
default: function MockModal({ open,
isShow, onSelect,
onClose, trigger,
children,
closable,
}: { }: {
isShow: boolean open?: boolean
onClose?: () => void onSelect: (type: BlockEnum, config?: Record<string, unknown>) => void
children?: React.ReactNode trigger?: ((open: boolean) => ReactNode) | ReactNode
closable?: boolean
}) { }) {
if (!isShow)
return null
return ( return (
<div data-testid="modal" role="dialog"> <div data-testid="mock-node-selector">
{closable && ( {typeof trigger === 'function' ? trigger(Boolean(open)) : trigger}
<button data-testid="modal-close-button" onClick={onClose}> {open && (
Close <div>
</button> <button data-testid="select-trigger-schedule" onClick={() => onSelect(BlockEnum.TriggerSchedule)}>
Select Trigger Schedule
</button>
<button data-testid="select-trigger-webhook" onClick={() => onSelect(BlockEnum.TriggerWebhook, { config: 'test' })}>
Select Trigger Webhook
</button>
</div>
)} )}
{children}
</div>
)
},
}))
// Mock StartNodeSelectionPanel (using real component would be better for integration,
// but for this test we'll mock to control behavior)
vi.mock('../start-node-selection-panel', () => ({
default: function MockStartNodeSelectionPanel({
onSelectUserInput,
onSelectTrigger,
}: {
onSelectUserInput?: () => void
onSelectTrigger?: (type: BlockEnum, config?: Record<string, unknown>) => void
}) {
return (
<div data-testid="start-node-selection-panel">
<button data-testid="select-user-input" onClick={onSelectUserInput}>
Select User Input
</button>
<button
data-testid="select-trigger-schedule"
onClick={() => onSelectTrigger?.(BlockEnum.TriggerSchedule)}
>
Select Trigger Schedule
</button>
<button
data-testid="select-trigger-webhook"
onClick={() => onSelectTrigger?.(BlockEnum.TriggerWebhook, { config: 'test' })}
>
Select Trigger Webhook
</button>
</div> </div>
) )
}, },
@ -79,401 +46,292 @@ describe('WorkflowOnboardingModal', () => {
vi.clearAllMocks() vi.clearAllMocks()
}) })
// Helper function to render component
const renderComponent = (props = {}) => { const renderComponent = (props = {}) => {
return render(<WorkflowOnboardingModal {...defaultProps} {...props} />) return render(<WorkflowOnboardingModal {...defaultProps} {...props} />)
} }
const getBackdrop = () => document.body.querySelector('.bg-workflow-canvas-canvas-overlay')
const getUserInputHeading = () => screen.getByRole('heading', { name: 'workflow.onboarding.userInputFull' })
const getTriggerHeading = () => screen.getByRole('heading', { name: 'workflow.onboarding.trigger' })
// Rendering tests (REQUIRED)
describe('Rendering', () => { describe('Rendering', () => {
it('should render without crashing', () => { it('should render without crashing', () => {
// Arrange & Act
renderComponent() renderComponent()
// Assert
expect(screen.getByRole('dialog')).toBeInTheDocument() expect(screen.getByRole('dialog')).toBeInTheDocument()
}) })
it('should render modal when isShow is true', () => { it('should render dialog when isShow is true', () => {
// Arrange & Act
renderComponent({ isShow: true }) renderComponent({ isShow: true })
// Assert expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByTestId('modal')).toBeInTheDocument()
}) })
it('should not render modal when isShow is false', () => { it('should not render dialog when isShow is false', () => {
// Arrange & Act
renderComponent({ isShow: false }) renderComponent({ isShow: false })
// Assert expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
}) })
it('should render modal title', () => { it('should render title', () => {
// Arrange & Act
renderComponent() renderComponent()
// Assert
expect(screen.getByText('workflow.onboarding.title')).toBeInTheDocument() expect(screen.getByText('workflow.onboarding.title')).toBeInTheDocument()
}) })
it('should render modal description', () => { it('should render description', () => {
// Arrange & Act renderComponent()
const { container } = renderComponent()
// Assert - Check both parts of description (separated by link) expect(screen.getByText('workflow.onboarding.description')).toBeInTheDocument()
const descriptionDiv = container.querySelector('.body-xs-regular.leading-4')
expect(descriptionDiv).toBeInTheDocument()
expect(descriptionDiv).toHaveTextContent('workflow.onboarding.description')
}) })
it('should render StartNodeSelectionPanel', () => { it('should render StartNodeSelectionPanel', () => {
// Arrange & Act
renderComponent() renderComponent()
// Assert expect(getUserInputHeading()).toBeInTheDocument()
expect(screen.getByTestId('start-node-selection-panel')).toBeInTheDocument() expect(getTriggerHeading()).toBeInTheDocument()
}) })
it('should render ESC tip when modal is shown', () => { it('should render ESC tip when shown', () => {
// Arrange & Act
renderComponent({ isShow: true }) renderComponent({ isShow: true })
// Assert
expect(screen.getByText('workflow.onboarding.escTip.press')).toBeInTheDocument() expect(screen.getByText('workflow.onboarding.escTip.press')).toBeInTheDocument()
expect(screen.getByText('workflow.onboarding.escTip.key')).toBeInTheDocument() expect(screen.getByText('workflow.onboarding.escTip.key')).toBeInTheDocument()
expect(screen.getByText('workflow.onboarding.escTip.toDismiss')).toBeInTheDocument() expect(screen.getByText('workflow.onboarding.escTip.toDismiss')).toBeInTheDocument()
}) })
it('should not render ESC tip when modal is hidden', () => { it('should not render ESC tip when hidden', () => {
// Arrange & Act
renderComponent({ isShow: false }) renderComponent({ isShow: false })
// Assert
expect(screen.queryByText('workflow.onboarding.escTip.press')).not.toBeInTheDocument() expect(screen.queryByText('workflow.onboarding.escTip.press')).not.toBeInTheDocument()
}) })
it('should have correct styling for title', () => { it('should have correct styling for title', () => {
// Arrange & Act
renderComponent() renderComponent()
// Assert
const title = screen.getByText('workflow.onboarding.title') const title = screen.getByText('workflow.onboarding.title')
expect(title).toHaveClass('title-2xl-semi-bold') expect(title).toHaveClass('title-2xl-semi-bold')
expect(title).toHaveClass('text-text-primary') expect(title).toHaveClass('text-text-primary')
}) })
it('should have modal close button', () => { it('should have close button', () => {
// Arrange & Act
renderComponent() renderComponent()
// Assert expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument()
expect(screen.getByTestId('modal-close-button')).toBeInTheDocument() })
it('should render workflow canvas backdrop when shown', () => {
renderComponent({ isShow: true })
const backdrop = getBackdrop()
expect(backdrop).toBeInTheDocument()
expect(backdrop).not.toHaveClass('opacity-20')
}) })
}) })
// Props tests (REQUIRED)
describe('Props', () => { describe('Props', () => {
it('should accept isShow prop', () => { it('should accept isShow prop', () => {
// Arrange & Act
const { rerender } = renderComponent({ isShow: false }) const { rerender } = renderComponent({ isShow: false })
// Assert expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
// Act
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} />) rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} />)
// Assert expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByTestId('modal')).toBeInTheDocument()
}) })
it('should accept onClose prop', () => { it('should accept onClose prop', () => {
// Arrange
const customOnClose = vi.fn() const customOnClose = vi.fn()
// Act
renderComponent({ onClose: customOnClose }) renderComponent({ onClose: customOnClose })
// Assert expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByTestId('modal')).toBeInTheDocument()
}) })
it('should accept onSelectStartNode prop', () => { it('should accept onSelectStartNode prop', () => {
// Arrange
const customHandler = vi.fn() const customHandler = vi.fn()
// Act
renderComponent({ onSelectStartNode: customHandler }) renderComponent({ onSelectStartNode: customHandler })
// Assert expect(getUserInputHeading()).toBeInTheDocument()
expect(screen.getByTestId('start-node-selection-panel')).toBeInTheDocument()
})
it('should handle undefined onClose gracefully', () => {
// Arrange & Act
expect(() => {
renderComponent({ onClose: undefined })
}).not.toThrow()
// Assert
expect(screen.getByTestId('modal')).toBeInTheDocument()
})
it('should handle undefined onSelectStartNode gracefully', () => {
// Arrange & Act
expect(() => {
renderComponent({ onSelectStartNode: undefined })
}).not.toThrow()
// Assert
expect(screen.getByTestId('modal')).toBeInTheDocument()
}) })
}) })
// User Interactions - Start Node Selection
describe('User Interactions - Start Node Selection', () => { describe('User Interactions - Start Node Selection', () => {
it('should call onSelectStartNode with Start block when user input is selected', async () => { it('should call onSelectStartNode with Start block when user input is selected', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
renderComponent() renderComponent()
// Act await user.click(getUserInputHeading())
const userInputButton = screen.getByTestId('select-user-input')
await user.click(userInputButton)
// Assert
expect(mockOnSelectStartNode).toHaveBeenCalledTimes(1) expect(mockOnSelectStartNode).toHaveBeenCalledTimes(1)
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.Start) expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.Start)
}) })
it('should call onClose after selecting user input', async () => { it('should not call onClose when selecting user input (parent handles closing)', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
renderComponent() renderComponent()
// Act await user.click(getUserInputHeading())
const userInputButton = screen.getByTestId('select-user-input')
await user.click(userInputButton)
// Assert expect(mockOnClose).not.toHaveBeenCalled()
expect(mockOnClose).toHaveBeenCalledTimes(1)
}) })
it('should call onSelectStartNode with trigger type when trigger is selected', async () => { it('should call onSelectStartNode with trigger type when trigger is selected', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
renderComponent() renderComponent()
// Act await user.click(getTriggerHeading())
const triggerButton = screen.getByTestId('select-trigger-schedule') await user.click(screen.getByTestId('select-trigger-schedule'))
await user.click(triggerButton)
// Assert
expect(mockOnSelectStartNode).toHaveBeenCalledTimes(1) expect(mockOnSelectStartNode).toHaveBeenCalledTimes(1)
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerSchedule, undefined) expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerSchedule, undefined)
}) })
it('should call onClose after selecting trigger', async () => { it('should not call onClose when selecting trigger (parent handles closing)', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
renderComponent() renderComponent()
// Act await user.click(getTriggerHeading())
const triggerButton = screen.getByTestId('select-trigger-schedule') await user.click(screen.getByTestId('select-trigger-schedule'))
await user.click(triggerButton)
// Assert expect(mockOnClose).not.toHaveBeenCalled()
expect(mockOnClose).toHaveBeenCalledTimes(1)
}) })
it('should pass tool config when selecting trigger with config', async () => { it('should pass tool config when selecting trigger with config', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
renderComponent() renderComponent()
// Act await user.click(getTriggerHeading())
const webhookButton = screen.getByTestId('select-trigger-webhook') await user.click(screen.getByTestId('select-trigger-webhook'))
await user.click(webhookButton)
// Assert
expect(mockOnSelectStartNode).toHaveBeenCalledTimes(1) expect(mockOnSelectStartNode).toHaveBeenCalledTimes(1)
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerWebhook, { config: 'test' }) expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerWebhook, { config: 'test' })
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).not.toHaveBeenCalled()
}) })
}) })
// User Interactions - Modal Close describe('User Interactions - Dialog Close', () => {
describe('User Interactions - Modal Close', () => {
it('should call onClose when close button is clicked', async () => { it('should call onClose when close button is clicked', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
renderComponent() renderComponent()
// Act await user.click(screen.getByRole('button', { name: 'Close' }))
const closeButton = screen.getByTestId('modal-close-button')
await user.click(closeButton)
// Assert
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).toHaveBeenCalledTimes(1)
}) })
it('should not call onSelectStartNode when closing without selection', async () => { it('should not call onSelectStartNode when closing without selection', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
renderComponent() renderComponent()
// Act await user.click(screen.getByRole('button', { name: 'Close' }))
const closeButton = screen.getByTestId('modal-close-button')
await user.click(closeButton)
// Assert
expect(mockOnSelectStartNode).not.toHaveBeenCalled() expect(mockOnSelectStartNode).not.toHaveBeenCalled()
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).toHaveBeenCalledTimes(1)
}) })
it('should call onClose exactly once when close button is clicked (no double-close)', async () => {
const user = userEvent.setup()
const onClose = vi.fn()
renderComponent({ onClose })
await user.click(screen.getByRole('button', { name: 'Close' }))
expect(onClose).toHaveBeenCalledTimes(1)
})
it('should not call onClose when clicking backdrop', async () => {
const user = userEvent.setup()
renderComponent()
const backdrop = getBackdrop()
expect(backdrop).toBeInTheDocument()
if (!backdrop)
throw new Error('backdrop should exist when dialog is open')
await user.click(backdrop)
expect(mockOnClose).not.toHaveBeenCalled()
})
}) })
// Keyboard Event Handling
describe('Keyboard Event Handling', () => { describe('Keyboard Event Handling', () => {
it('should call onClose when ESC key is pressed', () => { it('should call onClose when ESC key is pressed', () => {
// Arrange
renderComponent({ isShow: true }) renderComponent({ isShow: true })
// Act fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' })
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
// Assert
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).toHaveBeenCalledTimes(1)
}) })
it('should not call onClose when other keys are pressed', () => { it('should not call onClose when ESC is pressed but dialog is hidden', () => {
// Arrange
renderComponent({ isShow: true })
// Act
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' })
fireEvent.keyDown(document, { key: 'Tab', code: 'Tab' })
fireEvent.keyDown(document, { key: 'a', code: 'KeyA' })
// Assert
expect(mockOnClose).not.toHaveBeenCalled()
})
it('should not call onClose when ESC is pressed but modal is hidden', () => {
// Arrange
renderComponent({ isShow: false }) renderComponent({ isShow: false })
// Act fireEvent.keyDown(document, { key: 'Escape' })
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
// Assert
expect(mockOnClose).not.toHaveBeenCalled() expect(mockOnClose).not.toHaveBeenCalled()
}) })
it('should clean up event listener on unmount', () => { it('should clean up on unmount', () => {
// Arrange
const { unmount } = renderComponent({ isShow: true }) const { unmount } = renderComponent({ isShow: true })
// Act
unmount() unmount()
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' }) fireEvent.keyDown(document, { key: 'Escape' })
// Assert
expect(mockOnClose).not.toHaveBeenCalled() expect(mockOnClose).not.toHaveBeenCalled()
}) })
it('should update event listener when isShow changes', () => { it('should respond to ESC based on open state', () => {
// Arrange
const { rerender } = renderComponent({ isShow: true }) const { rerender } = renderComponent({ isShow: true })
// Act - Press ESC when shown fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' })
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
// Assert
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).toHaveBeenCalledTimes(1)
// Act - Hide modal and clear mock
mockOnClose.mockClear() mockOnClose.mockClear()
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={false} />) rerender(<WorkflowOnboardingModal {...defaultProps} isShow={false} />)
// Act - Press ESC when hidden fireEvent.keyDown(document, { key: 'Escape' })
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
// Assert
expect(mockOnClose).not.toHaveBeenCalled() expect(mockOnClose).not.toHaveBeenCalled()
}) })
it('should handle multiple ESC key presses', () => {
// Arrange
renderComponent({ isShow: true })
// Act
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
// Assert
expect(mockOnClose).toHaveBeenCalledTimes(3)
})
}) })
// Edge Cases (REQUIRED)
describe('Edge Cases', () => { describe('Edge Cases', () => {
it('should handle rapid modal show/hide toggling', async () => { it('should handle rapid show/hide toggling', async () => {
// Arrange
const { rerender } = renderComponent({ isShow: false }) const { rerender } = renderComponent({ isShow: false })
// Assert expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
// Act
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} />) rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} />)
expect(screen.getByRole('dialog')).toBeInTheDocument()
// Assert
expect(screen.getByTestId('modal')).toBeInTheDocument()
// Act
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={false} />) rerender(<WorkflowOnboardingModal {...defaultProps} isShow={false} />)
// Assert
await waitFor(() => { await waitFor(() => {
expect(screen.queryByTestId('modal')).not.toBeInTheDocument() expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
}) })
}) })
it('should handle selecting multiple nodes in sequence', async () => { it('should handle selecting multiple nodes in sequence', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
const { rerender } = renderComponent() const { rerender } = renderComponent()
// Act - Select user input await user.click(getUserInputHeading())
await user.click(screen.getByTestId('select-user-input'))
// Assert
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.Start) expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.Start)
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).not.toHaveBeenCalled()
// Act - Re-show modal and select trigger
mockOnClose.mockClear()
mockOnSelectStartNode.mockClear() mockOnSelectStartNode.mockClear()
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} />) rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} />)
await user.click(getTriggerHeading())
await user.click(screen.getByTestId('select-trigger-schedule')) await user.click(screen.getByTestId('select-trigger-schedule'))
// Assert
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerSchedule, undefined) expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerSchedule, undefined)
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).not.toHaveBeenCalled()
}) })
it('should handle prop updates correctly', () => { it('should handle prop updates correctly', () => {
// Arrange
const { rerender } = renderComponent({ isShow: true }) const { rerender } = renderComponent({ isShow: true })
// Assert expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByTestId('modal')).toBeInTheDocument()
// Act - Update props
const newOnClose = vi.fn() const newOnClose = vi.fn()
const newOnSelectStartNode = vi.fn() const newOnSelectStartNode = vi.fn()
rerender( rerender(
@ -484,169 +342,120 @@ describe('WorkflowOnboardingModal', () => {
/>, />,
) )
// Assert - Modal still renders with new props expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByTestId('modal')).toBeInTheDocument()
}) })
it('should handle onClose being called multiple times', async () => { it('should maintain dialog when props change', () => {
// Arrange
const user = userEvent.setup()
renderComponent()
// Act
await user.click(screen.getByTestId('modal-close-button'))
await user.click(screen.getByTestId('modal-close-button'))
// Assert
expect(mockOnClose).toHaveBeenCalledTimes(2)
})
it('should maintain modal state when props change', () => {
// Arrange
const { rerender } = renderComponent({ isShow: true }) const { rerender } = renderComponent({ isShow: true })
// Assert expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByTestId('modal')).toBeInTheDocument()
// Act - Change onClose handler
const newOnClose = vi.fn() const newOnClose = vi.fn()
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} onClose={newOnClose} />) rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} onClose={newOnClose} />)
// Assert - Modal should still be visible expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByTestId('modal')).toBeInTheDocument()
}) })
}) })
// Accessibility Tests
describe('Accessibility', () => { describe('Accessibility', () => {
it('should have dialog role', () => { it('should have dialog role', () => {
// Arrange & Act
renderComponent() renderComponent()
// Assert
expect(screen.getByRole('dialog')).toBeInTheDocument() expect(screen.getByRole('dialog')).toBeInTheDocument()
}) })
it('should have proper heading hierarchy', () => { it('should have proper heading hierarchy', () => {
// Arrange & Act renderComponent()
const { container } = renderComponent()
// Assert const heading = screen.getByRole('heading', { name: 'workflow.onboarding.title' })
const heading = container.querySelector('h3')
expect(heading).toBeInTheDocument() expect(heading).toBeInTheDocument()
expect(heading).toHaveTextContent('workflow.onboarding.title') expect(heading).toHaveTextContent('workflow.onboarding.title')
}) })
it('should have keyboard navigation support via ESC key', () => { it('should expose dialog accessible name from title', () => {
// Arrange renderComponent()
expect(screen.getByRole('dialog', { name: 'workflow.onboarding.title' })).toBeInTheDocument()
})
it('should support ESC key dismissal', () => {
renderComponent({ isShow: true }) renderComponent({ isShow: true })
// Act fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' })
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
// Assert
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).toHaveBeenCalledTimes(1)
}) })
it('should have visible ESC key hint', () => { it('should have visible ESC key hint', () => {
// Arrange & Act
renderComponent({ isShow: true }) renderComponent({ isShow: true })
// Assert - ShortcutsName component renders keys in div elements with system-kbd class
const escKey = screen.getByText('workflow.onboarding.escTip.key') const escKey = screen.getByText('workflow.onboarding.escTip.key')
// ShortcutsName renders a <div> with class system-kbd, not a <kbd> element
expect(escKey.closest('.system-kbd')).toBeInTheDocument() expect(escKey.closest('.system-kbd')).toBeInTheDocument()
}) })
it('should have descriptive text for ESC functionality', () => { it('should have descriptive text for ESC functionality', () => {
// Arrange & Act
renderComponent({ isShow: true }) renderComponent({ isShow: true })
// Assert
expect(screen.getByText('workflow.onboarding.escTip.press')).toBeInTheDocument() expect(screen.getByText('workflow.onboarding.escTip.press')).toBeInTheDocument()
expect(screen.getByText('workflow.onboarding.escTip.toDismiss')).toBeInTheDocument() expect(screen.getByText('workflow.onboarding.escTip.toDismiss')).toBeInTheDocument()
}) })
it('should have proper text color classes', () => { it('should have proper text color classes', () => {
// Arrange & Act
renderComponent() renderComponent()
// Assert
const title = screen.getByText('workflow.onboarding.title') const title = screen.getByText('workflow.onboarding.title')
expect(title).toHaveClass('text-text-primary') expect(title).toHaveClass('text-text-primary')
}) })
}) })
// Integration Tests
describe('Integration', () => { describe('Integration', () => {
it('should complete full flow of selecting user input node', async () => { it('should complete full flow of selecting user input node', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
renderComponent() renderComponent()
// Assert - Initial state expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByTestId('modal')).toBeInTheDocument()
expect(screen.getByText('workflow.onboarding.title')).toBeInTheDocument() expect(screen.getByText('workflow.onboarding.title')).toBeInTheDocument()
expect(screen.getByTestId('start-node-selection-panel')).toBeInTheDocument() expect(getUserInputHeading()).toBeInTheDocument()
// Act - Select user input await user.click(getUserInputHeading())
await user.click(screen.getByTestId('select-user-input'))
// Assert - Callbacks called
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.Start) expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.Start)
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).not.toHaveBeenCalled()
}) })
it('should complete full flow of selecting trigger node', async () => { it('should complete full flow of selecting trigger node', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
renderComponent() renderComponent()
// Assert - Initial state expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByTestId('modal')).toBeInTheDocument()
// Act - Select trigger await user.click(getTriggerHeading())
await user.click(screen.getByTestId('select-trigger-webhook')) await user.click(screen.getByTestId('select-trigger-webhook'))
// Assert - Callbacks called with config
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerWebhook, { config: 'test' }) expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerWebhook, { config: 'test' })
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).not.toHaveBeenCalled()
}) })
it('should render all components in correct hierarchy', () => { it('should render all components in correct hierarchy', () => {
// Arrange & Act renderComponent()
const { container } = renderComponent()
// Assert - Modal is the root const dialog = screen.getByRole('dialog')
expect(screen.getByTestId('modal')).toBeInTheDocument() expect(dialog).toBeInTheDocument()
expect(screen.getByText('workflow.onboarding.title')).toBeInTheDocument()
// Assert - Header elements expect(getUserInputHeading()).toBeInTheDocument()
const heading = container.querySelector('h3')
expect(heading).toBeInTheDocument()
// Assert - Selection panel
expect(screen.getByTestId('start-node-selection-panel')).toBeInTheDocument()
// Assert - ESC tip
expect(screen.getByText('workflow.onboarding.escTip.key')).toBeInTheDocument() expect(screen.getByText('workflow.onboarding.escTip.key')).toBeInTheDocument()
expect(dialog).not.toContainElement(screen.getByText('workflow.onboarding.escTip.key'))
}) })
it('should coordinate between keyboard and click interactions', async () => { it('should coordinate between keyboard and click interactions', async () => {
// Arrange
const user = userEvent.setup() const user = userEvent.setup()
renderComponent() renderComponent()
// Act - Click close button await user.click(screen.getByRole('button', { name: 'Close' }))
await user.click(screen.getByTestId('modal-close-button'))
// Assert
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).toHaveBeenCalledTimes(1)
// Act - Clear and try ESC key
mockOnClose.mockClear() mockOnClose.mockClear()
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' }) fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' })
// Assert
expect(mockOnClose).toHaveBeenCalledTimes(1) expect(mockOnClose).toHaveBeenCalledTimes(1)
}) })
}) })

View File

@ -47,7 +47,6 @@ describe('StartNodeOption', () => {
// Assert // Assert
const title = screen.getByText('Test Title') const title = screen.getByText('Test Title')
expect(title).toBeInTheDocument() expect(title).toBeInTheDocument()
expect(title).toHaveClass('system-md-semi-bold')
expect(title).toHaveClass('text-text-primary') expect(title).toHaveClass('text-text-primary')
}) })

View File

@ -1,12 +1,8 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types' import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
import {
useCallback,
useEffect,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal' import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogPortal, DialogTitle } from '@/app/components/base/ui/dialog'
import ShortcutsName from '@/app/components/workflow/shortcuts-name' import ShortcutsName from '@/app/components/workflow/shortcuts-name'
import { BlockEnum } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types'
import StartNodeSelectionPanel from './start-node-selection-panel' import StartNodeSelectionPanel from './start-node-selection-panel'
@ -24,63 +20,39 @@ const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const handleSelectUserInput = useCallback(() => {
onSelectStartNode(BlockEnum.Start)
onClose() // Close modal after selection
}, [onSelectStartNode, onClose])
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
onSelectStartNode(nodeType, toolConfig)
onClose() // Close modal after selection
}, [onSelectStartNode, onClose])
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isShow)
onClose()
}
document.addEventListener('keydown', handleEsc)
return () => document.removeEventListener('keydown', handleEsc)
}, [isShow, onClose])
return ( return (
<> <Dialog open={isShow} onOpenChange={onClose} disablePointerDismissal>
<Modal <DialogContent
isShow={isShow}
onClose={onClose}
className="w-[618px] max-w-[618px] rounded-2xl border border-effects-highlight bg-background-default-subtle shadow-lg" className="w-[618px] max-w-[618px] rounded-2xl border border-effects-highlight bg-background-default-subtle shadow-lg"
overlayOpacity overlayClassName="bg-workflow-canvas-canvas-overlay"
closable
clickOutsideNotClose
> >
<DialogCloseButton />
<div className="pb-4"> <div className="pb-4">
{/* Header */}
<div className="mb-6"> <div className="mb-6">
<h3 className="title-2xl-semi-bold mb-2 text-text-primary"> <DialogTitle className="mb-2 text-text-primary title-2xl-semi-bold">
{t('onboarding.title', { ns: 'workflow' })} {t('onboarding.title', { ns: 'workflow' })}
</h3> </DialogTitle>
<div className="body-xs-regular leading-4 text-text-tertiary"> <DialogDescription className="leading-4 text-text-tertiary body-xs-regular">
{t('onboarding.description', { ns: 'workflow' })} {t('onboarding.description', { ns: 'workflow' })}
</div> </DialogDescription>
</div> </div>
{/* Content */}
<StartNodeSelectionPanel <StartNodeSelectionPanel
onSelectUserInput={handleSelectUserInput} onSelectUserInput={() => onSelectStartNode(BlockEnum.Start)}
onSelectTrigger={handleTriggerSelect} onSelectTrigger={onSelectStartNode}
/> />
</div> </div>
</Modal> </DialogContent>
{/* ESC tip below modal */} <DialogPortal>
{isShow && ( <div className="pointer-events-none fixed left-1/2 top-1/2 z-50 flex -translate-x-1/2 translate-y-[165px] items-center gap-1 text-text-quaternary body-xs-regular">
<div className="body-xs-regular pointer-events-none fixed left-1/2 top-1/2 z-[70] flex -translate-x-1/2 translate-y-[165px] items-center gap-1 text-text-quaternary">
<span>{t('onboarding.escTip.press', { ns: 'workflow' })}</span> <span>{t('onboarding.escTip.press', { ns: 'workflow' })}</span>
<ShortcutsName keys={[t('onboarding.escTip.key', { ns: 'workflow' })]} textColor="secondary" /> <ShortcutsName keys={[t('onboarding.escTip.key', { ns: 'workflow' })]} textColor="secondary" />
<span>{t('onboarding.escTip.toDismiss', { ns: 'workflow' })}</span> <span>{t('onboarding.escTip.toDismiss', { ns: 'workflow' })}</span>
</div> </div>
)} </DialogPortal>
</> </Dialog>
) )
} }

View File

@ -1,6 +1,5 @@
'use client' 'use client'
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
import { cn } from '@/utils/classnames'
type StartNodeOptionProps = { type StartNodeOptionProps = {
icon: ReactNode icon: ReactNode
@ -20,22 +19,18 @@ const StartNodeOption: FC<StartNodeOptionProps> = ({
return ( return (
<div <div
onClick={onClick} onClick={onClick}
className={cn( className="flex h-40 w-[280px] cursor-pointer flex-col gap-2 rounded-xl border-[0.5px] border-components-option-card-option-border bg-components-panel-on-panel-item-bg p-4 shadow-sm transition-all hover:shadow-md"
'hover:border-components-panel-border-active flex h-40 w-[280px] cursor-pointer flex-col gap-2 rounded-xl border-[0.5px] border-components-option-card-option-border bg-components-panel-on-panel-item-bg p-4 shadow-sm transition-all hover:shadow-md',
)}
> >
{/* Icon */}
<div className="shrink-0"> <div className="shrink-0">
{icon} {icon}
</div> </div>
{/* Text content */}
<div className="flex h-[74px] flex-col gap-1 py-0.5"> <div className="flex h-[74px] flex-col gap-1 py-0.5">
<div className="h-5 leading-5"> <div className="h-5 leading-5">
<h3 className="system-md-semi-bold text-text-primary"> <h3 className="text-text-primary">
{title} {title}
{subtitle && ( {subtitle && (
<span className="system-md-regular text-text-quaternary"> <span className="text-text-quaternary system-md-regular">
{' '} {' '}
{subtitle} {subtitle}
</span> </span>
@ -44,7 +39,7 @@ const StartNodeOption: FC<StartNodeOptionProps> = ({
</div> </div>
<div className="h-12 leading-4"> <div className="h-12 leading-4">
<p className="system-xs-regular text-text-tertiary"> <p className="text-text-tertiary system-xs-regular">
{description} {description}
</p> </p>
</div> </div>

View File

@ -21,10 +21,6 @@ const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
const { t } = useTranslation() const { t } = useTranslation()
const [showTriggerSelector, setShowTriggerSelector] = useState(false) const [showTriggerSelector, setShowTriggerSelector] = useState(false)
const handleTriggerClick = useCallback(() => {
setShowTriggerSelector(true)
}, [])
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => { const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
setShowTriggerSelector(false) setShowTriggerSelector(false)
onSelectTrigger(nodeType, toolConfig) onSelectTrigger(nodeType, toolConfig)
@ -67,10 +63,9 @@ const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
)} )}
title={t('onboarding.trigger', { ns: 'workflow' })} title={t('onboarding.trigger', { ns: 'workflow' })}
description={t('onboarding.triggerDescription', { ns: 'workflow' })} description={t('onboarding.triggerDescription', { ns: 'workflow' })}
onClick={handleTriggerClick} onClick={() => setShowTriggerSelector(true)}
/> />
)} )}
popupClassName="z-[1200]"
/> />
</div> </div>
) )

View File

@ -1,10 +1,14 @@
# Overlay Migration Guide # Overlay Migration Guide
This document tracks the migration away from legacy `portal-to-follow-elem` APIs. This document tracks the migration away from legacy overlay APIs.
## Scope ## Scope
- Deprecated API: `@/app/components/base/portal-to-follow-elem` - Deprecated imports:
- `@/app/components/base/portal-to-follow-elem`
- `@/app/components/base/tooltip`
- `@/app/components/base/modal`
- `@/app/components/base/select` (including `custom` / `pure`)
- Replacement primitives: - Replacement primitives:
- `@/app/components/base/ui/tooltip` - `@/app/components/base/ui/tooltip`
- `@/app/components/base/ui/dropdown-menu` - `@/app/components/base/ui/dropdown-menu`
@ -15,33 +19,33 @@ This document tracks the migration away from legacy `portal-to-follow-elem` APIs
## ESLint policy ## ESLint policy
- `no-restricted-imports` blocks new usage of `portal-to-follow-elem`. - `no-restricted-imports` blocks all deprecated imports listed above.
- The rule is enabled for normal source files and test files are excluded. - The rule is enabled for normal source files (`.ts` / `.tsx`) and test files are excluded.
- Legacy `app/components/base/*` callers are temporarily allowlisted in ESLint config. - Legacy `app/components/base/*` callers are temporarily allowlisted in `OVERLAY_MIGRATION_LEGACY_BASE_FILES` (`web/eslint.constants.mjs`).
- New files must not be added to the allowlist without migration owner approval. - New files must not be added to the allowlist without migration owner approval.
## Migration phases ## Migration phases
1. Business/UI features outside `app/components/base/**` 1. Business/UI features outside `app/components/base/**`
- Migrate old calls to semantic primitives. - Migrate old calls to semantic primitives from `@/app/components/base/ui/**`.
- Keep `eslint-suppressions.json` stable or shrinking. - Keep deprecated imports out of newly touched files.
1. Legacy base components in allowlist 1. Legacy base components in allowlist
- Migrate allowlisted base callers gradually. - Migrate allowlisted base callers gradually.
- Remove migrated files from allowlist immediately. - Remove migrated files from `OVERLAY_MIGRATION_LEGACY_BASE_FILES` immediately.
1. Cleanup 1. Cleanup
- Remove remaining suppressions for `no-restricted-imports`. - Remove remaining allowlist entries.
- Remove legacy `portal-to-follow-elem` implementation. - Remove legacy overlay implementations when import count reaches zero.
## Suppression maintenance ## Allowlist maintenance
- After each migration batch, run: - After each migration batch, run:
```sh ```sh
pnpm eslint --prune-suppressions --pass-on-unpruned-suppressions <changed-files> pnpm -C web lint:fix --prune-suppressions <changed-files>
``` ```
- Never increase suppressions to bypass new code. - If a migrated file was in the allowlist, remove it from `web/eslint.constants.mjs` in the same PR.
- Prefer direct migration over adding suppression entries. - Never increase allowlist scope to bypass new code.
## React Refresh policy for base UI primitives ## React Refresh policy for base UI primitives

View File

@ -6298,9 +6298,6 @@
} }
}, },
"app/components/workflow-app/components/workflow-children.tsx": { "app/components/workflow-app/components/workflow-children.tsx": {
"no-console": {
"count": 1
},
"ts/no-explicit-any": { "ts/no-explicit-any": {
"count": 3 "count": 3
} }
@ -6310,19 +6307,6 @@
"count": 2 "count": 2
} }
}, },
"app/components/workflow-app/components/workflow-onboarding-modal/index.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 3
}
},
"app/components/workflow-app/components/workflow-onboarding-modal/start-node-option.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/workflow-app/hooks/use-DSL.ts": { "app/components/workflow-app/hooks/use-DSL.ts": {
"ts/no-explicit-any": { "ts/no-explicit-any": {
"count": 1 "count": 1