refactor: migrate checkbox to dify-ui (#36295)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-05-18 13:27:42 +08:00 committed by GitHub
parent 1925d58369
commit e2c52c9b0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 757 additions and 1864 deletions

View File

@ -789,14 +789,6 @@
"count": 10
}
},
"web/app/components/base/checkbox/index.stories.tsx": {
"no-console": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/base/chip/index.tsx": {
"ts/no-explicit-any": {
"count": 3

View File

@ -100,7 +100,7 @@ describe('Base Notion Page Selector Flow', () => {
/>,
)
await user.click(screen.getByTestId('checkbox-notion-page-checkbox-root-1'))
await user.click(screen.getByRole('checkbox', { name: 'Root 1' }))
expect(onSelect).toHaveBeenLastCalledWith(expect.arrayContaining([
expect.objectContaining({ page_id: 'root-1', workspace_id: 'w1' }),

View File

@ -184,7 +184,7 @@ describe('Document Management Flow', () => {
})
describe('Document Selection Integration', () => {
it('should manage selection state externally', () => {
it('should keep checkbox selection state owned outside the hook', () => {
const docs = [
createDoc({ id: 'doc-1' }),
createDoc({ id: 'doc-2' }),
@ -198,62 +198,9 @@ describe('Document Management Flow', () => {
onSelectedIdChange,
}))
expect(result.current.isAllSelected).toBe(false)
expect(result.current.isSomeSelected).toBe(false)
})
it('should select all documents', () => {
const docs = [
createDoc({ id: 'doc-1' }),
createDoc({ id: 'doc-2' }),
]
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() => useDocumentSelection({
documents: docs,
selectedIds: [],
onSelectedIdChange,
}))
act(() => {
result.current.onSelectAll()
})
expect(onSelectedIdChange).toHaveBeenCalledWith(
expect.arrayContaining(['doc-1', 'doc-2']),
)
})
it('should detect all-selected state', () => {
const docs = [
createDoc({ id: 'doc-1' }),
createDoc({ id: 'doc-2' }),
]
const { result } = renderHook(() => useDocumentSelection({
documents: docs,
selectedIds: ['doc-1', 'doc-2'],
onSelectedIdChange: vi.fn(),
}))
expect(result.current.isAllSelected).toBe(true)
})
it('should detect partial selection', () => {
const docs = [
createDoc({ id: 'doc-1' }),
createDoc({ id: 'doc-2' }),
createDoc({ id: 'doc-3' }),
]
const { result } = renderHook(() => useDocumentSelection({
documents: docs,
selectedIds: ['doc-1'],
onSelectedIdChange: vi.fn(),
}))
expect(result.current.isSomeSelected).toBe(true)
expect(result.current.isAllSelected).toBe(false)
expect(result.current.downloadableSelectedIds).toEqual([])
expect(result.current.hasErrorDocumentsSelected).toBe(false)
expect(onSelectedIdChange).not.toHaveBeenCalled()
})
it('should identify downloadable selected documents (FILE type only)', () => {
@ -311,8 +258,9 @@ describe('Document Management Flow', () => {
expect(sortResult.current.sortField).toBe('created_at')
expect(sortResult.current.sortOrder).toBe('desc')
// Selection starts empty
expect(selResult.current.isAllSelected).toBe(false)
// Selection-derived batch metadata starts empty.
expect(selResult.current.downloadableSelectedIds).toEqual([])
expect(selResult.current.hasErrorDocumentsSelected).toBe(false)
})
})
})

View File

@ -19,8 +19,6 @@ const createAnnotation = (overrides: Partial<AnnotationItem> = {}): AnnotationIt
hit_count: overrides.hit_count ?? 2,
})
const getCheckboxes = (container: HTMLElement) => container.querySelectorAll('[data-testid^="checkbox"]')
describe('List', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -51,7 +49,7 @@ describe('List', () => {
it('should toggle single and bulk selection states', () => {
const list = [createAnnotation({ id: 'a', question: 'A' }), createAnnotation({ id: 'b', question: 'B' })]
const onSelectedIdsChange = vi.fn()
const { container, rerender } = render(
const { rerender } = render(
<List
list={list}
onView={vi.fn()}
@ -63,8 +61,7 @@ describe('List', () => {
/>,
)
const checkboxes = getCheckboxes(container)
fireEvent.click(checkboxes[1]!)
fireEvent.click(screen.getByRole('checkbox', { name: 'A' }))
expect(onSelectedIdsChange).toHaveBeenCalledWith(['a'])
rerender(
@ -78,11 +75,10 @@ describe('List', () => {
onCancel={vi.fn()}
/>,
)
const updatedCheckboxes = getCheckboxes(container)
fireEvent.click(updatedCheckboxes[1]!)
fireEvent.click(screen.getByRole('checkbox', { name: 'A' }))
expect(onSelectedIdsChange).toHaveBeenCalledWith([])
fireEvent.click(updatedCheckboxes[0]!)
fireEvent.click(screen.getByRole('checkbox', { name: 'common.operation.selectAll' }))
expect(onSelectedIdsChange).toHaveBeenCalledWith(['a', 'b'])
})

View File

@ -91,7 +91,7 @@ describe('AddAnnotationModal', () => {
typeQuestion('Question value')
typeAnswer('Answer value')
fireEvent.click(screen.getByTestId('checkbox-create-next-checkbox'))
fireEvent.click(screen.getByText('appAnnotation.addModal.createNext'))
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
@ -106,8 +106,7 @@ describe('AddAnnotationModal', () => {
typeQuestion('Question value')
typeAnswer('Answer value')
const createNextToggle = screen.getByText('appAnnotation.addModal.createNext').previousElementSibling as HTMLElement
fireEvent.click(createNextToggle)
fireEvent.click(screen.getByText('appAnnotation.addModal.createNext'))
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { AnnotationItemBasic } from '../type'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import {
Drawer,
DrawerBackdrop,
@ -16,7 +17,6 @@ import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import AnnotationFull from '@/app/components/billing/annotation-full'
import { useProviderContext } from '@/context/provider-context'
import EditItem, { EditItemType } from './edit-item'
@ -128,10 +128,10 @@ const AddAnnotationModal: FC<Props> = ({
</div>
)}
<div className="flex h-16 items-center justify-between rounded-br-xl rounded-bl-xl border-t border-divider-subtle bg-background-section-burn px-4 system-sm-medium text-text-tertiary">
<div className="flex items-center space-x-2">
<Checkbox id="create-next-checkbox" checked={isCreateNext} onCheck={() => setIsCreateNext(!isCreateNext)} />
<div>{t('addModal.createNext', { ns: 'appAnnotation' })}</div>
</div>
<label className="flex items-center space-x-2">
<Checkbox checked={isCreateNext} onCheckedChange={setIsCreateNext} />
<span>{t('addModal.createNext', { ns: 'appAnnotation' })}</span>
</label>
<div className="mt-2 flex space-x-2">
<Button className="h-7 text-xs" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button className="h-7 text-xs" variant="primary" onClick={handleSave} loading={isSaving} disabled={isAnnotationFull}>{t('operation.add', { ns: 'common' })}</Button>

View File

@ -1,13 +1,13 @@
'use client'
import type { FC } from 'react'
import type { AnnotationItem } from './type'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Checkbox from '@/app/components/base/checkbox'
import useTimestamp from '@/hooks/use-timestamp'
import BatchAction from './batch-action'
import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal'
@ -44,15 +44,15 @@ const List: FC<Props> = ({
return list.some(item => selectedIds.includes(item.id))
}, [list, selectedIds])
const handleSelectAll = useCallback(() => {
const handleSelectAll = useCallback((checked: boolean) => {
const currentPageIds = list.map(item => item.id)
const otherPageIds = selectedIds.filter(id => !currentPageIds.includes(id))
if (isAllSelected)
onSelectedIdsChange(otherPageIds)
else
if (checked)
onSelectedIdsChange([...otherPageIds, ...currentPageIds])
}, [isAllSelected, list, selectedIds, onSelectedIdsChange])
else
onSelectedIdsChange(otherPageIds)
}, [list, selectedIds, onSelectedIdsChange])
return (
<>
@ -65,7 +65,8 @@ const List: FC<Props> = ({
className="mr-2"
checked={isAllSelected}
indeterminate={!isAllSelected && isSomeSelected}
onCheck={handleSelectAll}
aria-label={t('operation.selectAll', { ns: 'common' })}
onCheckedChange={handleSelectAll}
/>
</td>
<td className="w-5 bg-background-section-burn pr-1 pl-2 whitespace-nowrap">{t('table.header.question', { ns: 'appAnnotation' })}</td>
@ -90,11 +91,12 @@ const List: FC<Props> = ({
<Checkbox
className="mr-2"
checked={selectedIds.includes(item.id)}
onCheck={() => {
if (selectedIds.includes(item.id))
onSelectedIdsChange(selectedIds.filter(id => id !== item.id))
else
aria-label={item.question}
onCheckedChange={(checked) => {
if (checked)
onSelectedIdsChange([...selectedIds, item.id])
else
onSelectedIdsChange(selectedIds.filter(id => id !== item.id))
}}
/>
</td>

View File

@ -70,12 +70,6 @@ vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', (
),
}))
vi.mock('@/app/components/base/checkbox', () => ({
default: ({ onCheck, checked }: { onCheck: () => void, checked: boolean }) => (
<button type="button" onClick={onCheck}>{checked ? 'checked' : 'unchecked'}</button>
),
}))
vi.mock('@langgenius/dify-ui/select', async (importOriginal) => {
const actual = await importOriginal<typeof import('@langgenius/dify-ui/select')>()
@ -230,7 +224,7 @@ describe('ConfigModalFormFields', () => {
expect(screen.queryByText('variableConfig.hiddenDescription')).not.toBeInTheDocument()
fireEvent.click(screen.getByText('single-file-setting'))
fireEvent.click(screen.getByText('upload-file'))
fireEvent.click(screen.getAllByText('unchecked')[0]!)
fireEvent.click(screen.getByRole('checkbox', { name: 'variableConfig.required' }))
expect(singleFileProps.onFilePayloadChange).toHaveBeenCalledWith({ number_limits: 1 })
expect(singleFileProps.payloadChangeHandlers.default).toHaveBeenCalledWith(expect.objectContaining({
@ -416,17 +410,14 @@ describe('ConfigModalFormFields', () => {
requiredProps.tempPayload = { ...requiredProps.tempPayload, type: InputVarType.textInput, required: true, hide: false }
const { unmount } = render(<ConfigModalFormFields {...requiredProps} />)
const buttons = screen.getAllByRole('button')
const hideButton = buttons.find(btn => btn.textContent === 'unchecked' && btn !== buttons[0])
expect(hideButton).toBeDefined()
expect(screen.getByRole('checkbox', { name: 'variableConfig.hidden' })).toHaveAttribute('aria-disabled', 'true')
unmount()
const hideProps = createBaseProps()
hideProps.tempPayload = { ...hideProps.tempPayload, type: InputVarType.textInput, required: false, hide: true }
render(<ConfigModalFormFields {...hideProps} />)
const allButtons = screen.getAllByRole('button')
const checkedHideButton = allButtons.find(btn => btn.textContent === 'checked')
expect(checkedHideButton).toBeDefined()
expect(screen.getByRole('checkbox', { name: 'variableConfig.required' })).toHaveAttribute('aria-disabled', 'true')
expect(screen.getByRole('checkbox', { name: 'variableConfig.hidden' })).toHaveAttribute('aria-checked', 'true')
})
})

View File

@ -3,6 +3,7 @@ import type { ChangeEvent, FC } from 'react'
import type { Item as SelectOptionItem } from './type-select'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { InputVar, UploadFileSetting } from '@/app/components/workflow/types'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import {
Select,
SelectContent,
@ -14,7 +15,6 @@ import {
} from '@langgenius/dify-ui/select'
import * as React from 'react'
import { Trans } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { Infotip } from '@/app/components/base/infotip'
import Input from '@/app/components/base/input'
@ -232,16 +232,26 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
</Field>
)}
<div className="mt-5! flex h-6 items-center space-x-2">
<Checkbox checked={tempPayload.required} disabled={!isFileInput && tempPayload.hide} onCheck={() => onPayloadChange('required')(!tempPayload.required)} />
<label className="mt-5! flex h-6 items-center space-x-2">
<Checkbox
checked={tempPayload.required}
disabled={!isFileInput && tempPayload.hide}
onCheckedChange={checked => onPayloadChange('required')(checked)}
/>
<span className="system-sm-semibold text-text-secondary">{t('variableConfig.required', { ns: 'appDebug' })}</span>
</div>
</label>
{!isFileInput && (
<div className="mt-5! flex h-6 items-center space-x-2">
<Checkbox checked={tempPayload.hide} disabled={tempPayload.required} onCheck={() => onPayloadChange('hide')(!tempPayload.hide)} />
<div className="flex items-center gap-1">
<div className="mt-5! flex h-6 items-center gap-2">
<label className="flex items-center gap-2">
<Checkbox
checked={tempPayload.hide}
disabled={tempPayload.required}
onCheckedChange={checked => onPayloadChange('hide')(checked)}
/>
<span className="system-sm-semibold text-text-secondary">{t('variableConfig.hidden', { ns: 'appDebug' })}</span>
</label>
<div className="flex items-center gap-1">
<Infotip
aria-label={hiddenDescriptionAriaLabel}
popupClassName="max-w-[300px]"

View File

@ -11,6 +11,7 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
@ -19,7 +20,6 @@ import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import AppIcon from '@/app/components/base/app-icon'
import Checkbox from '@/app/components/base/checkbox'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Input from '@/app/components/base/input'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
@ -165,14 +165,12 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
{isAppsFull && <AppsFull loc="app-switch" />}
<div className="flex items-center justify-between pt-6">
<div className="flex items-center">
<Checkbox className="shrink-0" checked={removeOriginal} onCheck={() => setRemoveOriginal(!removeOriginal)} />
<button
type="button"
className="ml-2 cursor-pointer border-none bg-transparent p-0 text-left text-sm leading-5 text-text-secondary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
onClick={() => setRemoveOriginal(!removeOriginal)}
>
{t('removeOriginal', { ns: 'app' })}
</button>
<label className="flex cursor-pointer items-center">
<Checkbox className="shrink-0" checked={removeOriginal} onCheckedChange={setRemoveOriginal} />
<span className="ml-2 text-left text-sm leading-5 text-text-secondary">
{t('removeOriginal', { ns: 'app' })}
</span>
</label>
</div>
<div className="flex items-center">
<Button className="mr-2" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>

View File

@ -415,7 +415,7 @@ describe('List', () => {
it('should handle checkbox change', () => {
renderList()
const checkbox = screen.getByTestId('checkbox-undefined')
const checkbox = screen.getByRole('checkbox', { name: 'app.showMyCreatedAppsOnly' })
fireEvent.click(checkbox)
expect(mockSetIsCreatedByMe).toHaveBeenCalledWith(true)

View File

@ -2,12 +2,12 @@
import type { FC } from 'react'
import type { AppListQuery } from '@/contract/console/apps'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { keepPreviousData, useInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Input from '@/app/components/base/input'
import TabSliderNew from '@/app/components/base/tab-slider-new'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
@ -158,9 +158,9 @@ const List: FC<Props> = ({
return () => observer?.disconnect()
}, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, isCurrentWorkspaceDatasetOperator])
const handleCreatedByMeChange = useCallback(() => {
setIsCreatedByMe(!isCreatedByMe)
}, [isCreatedByMe, setIsCreatedByMe])
const handleCreatedByMeChange = useCallback((checked: boolean) => {
setIsCreatedByMe(checked)
}, [setIsCreatedByMe])
const pages = useMemo(() => data?.pages ?? [], [data?.pages])
const apps = useMemo(() => pages.flatMap(({ data: pageApps }) => pageApps), [pages])
@ -204,7 +204,7 @@ const List: FC<Props> = ({
/>
<div className="flex items-center gap-2">
<label className="mr-2 flex h-7 items-center space-x-2">
<Checkbox checked={isCreatedByMe} onCheck={handleCreatedByMeChange} />
<Checkbox checked={isCreatedByMe} onCheckedChange={handleCreatedByMeChange} />
<div className="text-sm font-normal text-text-secondary">
{t('showMyCreatedAppsOnly', { ns: 'app' })}
</div>

View File

@ -179,7 +179,7 @@ describe('InputsFormContent', () => {
it('should handle bool input changes', async () => {
render(<InputsFormContent />)
const checkbox = screen.getByTestId(/checkbox-/i)
const checkbox = screen.getByRole('checkbox', { name: 'Bool Label' })
await user.click(checkbox)
expect(mockContextValue.setCurrentConversationInputs).toHaveBeenCalled()

View File

@ -1,110 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react'
import Checkbox from '../index'
describe('Checkbox Component', () => {
const mockProps = {
id: 'test',
}
it('renders unchecked checkbox by default', () => {
render(<Checkbox {...mockProps} />)
const checkbox = screen.getByTestId('checkbox-test')
expect(checkbox).toBeInTheDocument()
expect(checkbox).not.toHaveClass('bg-components-checkbox-bg')
})
it('renders checked checkbox when checked prop is true', () => {
render(<Checkbox {...mockProps} checked />)
const checkbox = screen.getByTestId('checkbox-test')
expect(checkbox).toHaveClass('bg-components-checkbox-bg')
expect(screen.getByTestId('check-icon-test')).toBeInTheDocument()
})
it('renders indeterminate state correctly', () => {
render(<Checkbox {...mockProps} indeterminate />)
expect(screen.getByTestId('indeterminate-icon')).toBeInTheDocument()
})
it('handles click events when not disabled', () => {
const onCheck = vi.fn()
render(<Checkbox {...mockProps} onCheck={onCheck} />)
const checkbox = screen.getByTestId('checkbox-test')
fireEvent.click(checkbox)
expect(onCheck).toHaveBeenCalledTimes(1)
})
it('does not handle click events when disabled', () => {
const onCheck = vi.fn()
render(<Checkbox {...mockProps} disabled onCheck={onCheck} />)
const checkbox = screen.getByTestId('checkbox-test')
fireEvent.click(checkbox)
expect(onCheck).not.toHaveBeenCalled()
expect(checkbox).toHaveClass('cursor-not-allowed')
})
it('applies custom className when provided', () => {
const customClass = 'custom-class'
render(<Checkbox {...mockProps} className={customClass} />)
const checkbox = screen.getByTestId('checkbox-test')
expect(checkbox).toHaveClass(customClass)
})
it('applies correct styles for disabled checked state', () => {
render(<Checkbox {...mockProps} checked disabled />)
const checkbox = screen.getByTestId('checkbox-test')
expect(checkbox).toHaveClass('bg-components-checkbox-bg-disabled-checked')
expect(checkbox).toHaveClass('cursor-not-allowed')
})
it('applies correct styles for disabled unchecked state', () => {
render(<Checkbox {...mockProps} disabled />)
const checkbox = screen.getByTestId('checkbox-test')
expect(checkbox).toHaveClass('bg-components-checkbox-bg-disabled')
expect(checkbox).toHaveClass('cursor-not-allowed')
})
it('handles keyboard events (Space and Enter) when not disabled', () => {
const onCheck = vi.fn()
render(<Checkbox {...mockProps} onCheck={onCheck} />)
const checkbox = screen.getByTestId('checkbox-test')
fireEvent.keyDown(checkbox, { key: ' ' })
expect(onCheck).toHaveBeenCalledTimes(1)
fireEvent.keyDown(checkbox, { key: 'Enter' })
expect(onCheck).toHaveBeenCalledTimes(2)
})
it('does not handle keyboard events when disabled', () => {
const onCheck = vi.fn()
render(<Checkbox {...mockProps} disabled onCheck={onCheck} />)
const checkbox = screen.getByTestId('checkbox-test')
fireEvent.keyDown(checkbox, { key: ' ' })
expect(onCheck).not.toHaveBeenCalled()
fireEvent.keyDown(checkbox, { key: 'Enter' })
expect(onCheck).not.toHaveBeenCalled()
})
it('exposes aria-disabled attribute', () => {
const { rerender } = render(<Checkbox {...mockProps} />)
expect(screen.getByTestId('checkbox-test')).toHaveAttribute('aria-disabled', 'false')
rerender(<Checkbox {...mockProps} disabled />)
expect(screen.getByTestId('checkbox-test')).toHaveAttribute('aria-disabled', 'true')
})
it('normalizes aria-checked attribute', () => {
const { rerender } = render(<Checkbox {...mockProps} />)
expect(screen.getByTestId('checkbox-test')).toHaveAttribute('aria-checked', 'false')
rerender(<Checkbox {...mockProps} checked />)
expect(screen.getByTestId('checkbox-test')).toHaveAttribute('aria-checked', 'true')
rerender(<Checkbox {...mockProps} indeterminate />)
expect(screen.getByTestId('checkbox-test')).toHaveAttribute('aria-checked', 'mixed')
})
})

View File

@ -1,17 +0,0 @@
import { render, screen } from '@testing-library/react'
import IndeterminateIcon from '../indeterminate-icon'
describe('IndeterminateIcon', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
render(<IndeterminateIcon />)
expect(screen.getByTestId('indeterminate-icon')).toBeInTheDocument()
})
it('should render an svg element', () => {
const { container } = render(<IndeterminateIcon />)
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
})
})

View File

@ -1,11 +0,0 @@
const IndeterminateIcon = () => {
return (
<div data-testid="indeterminate-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2.5 6H9.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
</div>
)
}
export default IndeterminateIcon

View File

@ -1,399 +0,0 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { useState } from 'react'
import Checkbox from '.'
// Helper function for toggling items in an array
const createToggleItem = <T extends { id: string, checked: boolean }>(
items: T[],
setItems: (items: T[]) => void,
) => (id: string) => {
setItems(items.map(item =>
item.id === id ? { ...item, checked: !item.checked } as T : item,
))
}
const meta = {
title: 'Base/Data Entry/Checkbox',
component: Checkbox,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Checkbox component with support for checked, unchecked, indeterminate, and disabled states.',
},
},
},
tags: ['autodocs'],
argTypes: {
checked: {
control: 'boolean',
description: 'Checked state',
},
indeterminate: {
control: 'boolean',
description: 'Indeterminate state (partially checked)',
},
disabled: {
control: 'boolean',
description: 'Disabled state',
},
className: {
control: 'text',
description: 'Additional CSS classes',
},
id: {
control: 'text',
description: 'HTML id attribute',
},
},
} satisfies Meta<typeof Checkbox>
export default meta
type Story = StoryObj<typeof meta>
// Interactive demo wrapper
const CheckboxDemo = (args: any) => {
const [checked, setChecked] = useState(args.checked || false)
return (
<div className="flex items-center gap-3">
<Checkbox
{...args}
checked={checked}
onCheck={() => {
if (!args.disabled) {
setChecked(!checked)
console.log('Checkbox toggled:', !checked)
}
}}
/>
<span className="text-sm text-gray-700">
{checked ? 'Checked' : 'Unchecked'}
</span>
</div>
)
}
// Default unchecked
export const Default: Story = {
render: args => <CheckboxDemo {...args} />,
args: {
checked: false,
disabled: false,
indeterminate: false,
},
}
// Checked state
export const Checked: Story = {
render: args => <CheckboxDemo {...args} />,
args: {
checked: true,
disabled: false,
indeterminate: false,
},
}
// Indeterminate state
export const Indeterminate: Story = {
render: args => <CheckboxDemo {...args} />,
args: {
checked: false,
disabled: false,
indeterminate: true,
},
}
// Disabled unchecked
export const DisabledUnchecked: Story = {
render: args => <CheckboxDemo {...args} />,
args: {
checked: false,
disabled: true,
indeterminate: false,
},
}
// Disabled checked
export const DisabledChecked: Story = {
render: args => <CheckboxDemo {...args} />,
args: {
checked: true,
disabled: true,
indeterminate: false,
},
}
// Disabled indeterminate
export const DisabledIndeterminate: Story = {
render: args => <CheckboxDemo {...args} />,
args: {
checked: false,
disabled: true,
indeterminate: true,
},
}
// State comparison
export const StateComparison: Story = {
render: () => (
<div className="flex flex-col gap-6">
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-2">
<Checkbox checked={false} onCheck={() => undefined} />
<span className="text-xs text-gray-600">Unchecked</span>
</div>
<div className="flex flex-col items-center gap-2">
<Checkbox checked={true} onCheck={() => undefined} />
<span className="text-xs text-gray-600">Checked</span>
</div>
<div className="flex flex-col items-center gap-2">
<Checkbox checked={false} indeterminate={true} onCheck={() => undefined} />
<span className="text-xs text-gray-600">Indeterminate</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-2">
<Checkbox checked={false} disabled={true} onCheck={() => undefined} />
<span className="text-xs text-gray-600">Disabled</span>
</div>
<div className="flex flex-col items-center gap-2">
<Checkbox checked={true} disabled={true} onCheck={() => undefined} />
<span className="text-xs text-gray-600">Disabled Checked</span>
</div>
<div className="flex flex-col items-center gap-2">
<Checkbox checked={false} indeterminate={true} disabled={true} onCheck={() => undefined} />
<span className="text-xs text-gray-600">Disabled Indeterminate</span>
</div>
</div>
</div>
),
}
// With labels
const WithLabelsDemo = () => {
const [items, setItems] = useState([
{ id: '1', label: 'Enable notifications', checked: true },
{ id: '2', label: 'Enable email updates', checked: false },
{ id: '3', label: 'Enable SMS alerts', checked: false },
])
const toggleItem = createToggleItem(items, setItems)
return (
<div className="flex flex-col gap-3">
{items.map(item => (
<div key={item.id} className="flex items-center gap-3">
<Checkbox
id={item.id}
checked={item.checked}
onCheck={() => toggleItem(item.id)}
/>
<label
htmlFor={item.id}
className="cursor-pointer text-sm text-gray-700"
onClick={() => toggleItem(item.id)}
>
{item.label}
</label>
</div>
))}
</div>
)
}
export const WithLabels: Story = {
render: () => <WithLabelsDemo />,
}
// Select all example
const SelectAllExampleDemo = () => {
const [items, setItems] = useState([
{ id: '1', label: 'Item 1', checked: false },
{ id: '2', label: 'Item 2', checked: false },
{ id: '3', label: 'Item 3', checked: false },
])
const allChecked = items.every(item => item.checked)
const someChecked = items.some(item => item.checked)
const indeterminate = someChecked && !allChecked
const toggleAll = () => {
const newChecked = !allChecked
setItems(items.map(item => ({ ...item, checked: newChecked })))
}
const toggleItem = createToggleItem(items, setItems)
return (
<div className="flex flex-col gap-3 rounded-lg bg-gray-50 p-4">
<div className="flex items-center gap-3 border-b border-gray-200 pb-3">
<Checkbox
checked={allChecked}
indeterminate={indeterminate}
onCheck={toggleAll}
/>
<span className="text-sm font-medium text-gray-700">Select All</span>
</div>
<div className="flex flex-col gap-2 pl-7">
{items.map(item => (
<div key={item.id} className="flex items-center gap-3">
<Checkbox
id={item.id}
checked={item.checked}
onCheck={() => toggleItem(item.id)}
/>
<label
htmlFor={item.id}
className="cursor-pointer text-sm text-gray-600"
onClick={() => toggleItem(item.id)}
>
{item.label}
</label>
</div>
))}
</div>
</div>
)
}
export const SelectAllExample: Story = {
render: () => <SelectAllExampleDemo />,
}
// Form example
const FormExampleDemo = () => {
const [formData, setFormData] = useState({
terms: false,
newsletter: false,
privacy: false,
})
return (
<div className="w-96 rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-4 text-lg font-semibold">Account Settings</h3>
<div className="flex flex-col gap-4">
<div className="flex items-start gap-3">
<Checkbox
id="terms"
checked={formData.terms}
onCheck={() => setFormData({ ...formData, terms: !formData.terms })}
/>
<div>
<label htmlFor="terms" className="cursor-pointer text-sm font-medium text-gray-700">
I agree to the terms and conditions
</label>
<p className="mt-1 text-xs text-gray-500">
Required to continue
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Checkbox
id="newsletter"
checked={formData.newsletter}
onCheck={() => setFormData({ ...formData, newsletter: !formData.newsletter })}
/>
<div>
<label htmlFor="newsletter" className="cursor-pointer text-sm font-medium text-gray-700">
Subscribe to newsletter
</label>
<p className="mt-1 text-xs text-gray-500">
Get updates about new features
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Checkbox
id="privacy"
checked={formData.privacy}
onCheck={() => setFormData({ ...formData, privacy: !formData.privacy })}
/>
<div>
<label htmlFor="privacy" className="cursor-pointer text-sm font-medium text-gray-700">
I have read the privacy policy
</label>
<p className="mt-1 text-xs text-gray-500">
Required to continue
</p>
</div>
</div>
</div>
</div>
)
}
export const FormExample: Story = {
render: () => <FormExampleDemo />,
}
// Task list example
const TaskListExampleDemo = () => {
const [tasks, setTasks] = useState([
{ id: '1', title: 'Review pull request', completed: true },
{ id: '2', title: 'Update documentation', completed: true },
{ id: '3', title: 'Fix navigation bug', completed: false },
{ id: '4', title: 'Deploy to staging', completed: false },
])
const toggleTask = (id: string) => {
setTasks(tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task,
))
}
const completedCount = tasks.filter(t => t.completed).length
return (
<div className="w-96 rounded-lg border border-gray-200 bg-white p-4">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-700">Today's Tasks</h3>
<span className="text-xs text-gray-500">
{completedCount}
{' '}
of
{tasks.length}
{' '}
completed
</span>
</div>
<div className="flex flex-col gap-2">
{tasks.map(task => (
<div
key={task.id}
className="flex items-center gap-3 rounded-sm p-2 hover:bg-gray-50"
>
<Checkbox
id={task.id}
checked={task.completed}
onCheck={() => toggleTask(task.id)}
/>
<span
className={`cursor-pointer text-sm ${
task.completed ? 'text-gray-400 line-through' : 'text-gray-700'
}`}
onClick={() => toggleTask(task.id)}
>
{task.title}
</span>
</div>
))}
</div>
</div>
)
}
export const TaskListExample: Story = {
render: () => <TaskListExampleDemo />,
}
// Interactive playground
export const Playground: Story = {
render: args => <CheckboxDemo {...args} />,
args: {
checked: false,
indeterminate: false,
disabled: false,
id: 'playground-checkbox',
},
}

View File

@ -1,69 +0,0 @@
import { cn } from '@langgenius/dify-ui/cn'
import IndeterminateIcon from './assets/indeterminate-icon'
type CheckboxProps = {
id?: string
checked?: boolean
onCheck?: (event: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => void
className?: string
disabled?: boolean
indeterminate?: boolean
ariaLabel?: string
ariaLabelledBy?: string
}
const Checkbox = ({
id,
checked,
onCheck,
className,
disabled,
indeterminate,
ariaLabel,
ariaLabelledBy,
}: CheckboxProps) => {
const checkClassName = (checked || indeterminate)
? 'bg-components-checkbox-bg text-components-checkbox-icon hover:bg-components-checkbox-bg-hover'
: 'border border-components-checkbox-border bg-components-checkbox-bg-unchecked hover:bg-components-checkbox-bg-unchecked-hover hover:border-components-checkbox-border-hover'
const disabledClassName = (checked || indeterminate)
? 'cursor-not-allowed bg-components-checkbox-bg-disabled-checked text-components-checkbox-icon-disabled hover:bg-components-checkbox-bg-disabled-checked'
: 'cursor-not-allowed border-components-checkbox-border-disabled bg-components-checkbox-bg-disabled hover:border-components-checkbox-border-disabled hover:bg-components-checkbox-bg-disabled'
return (
<div
id={id}
className={cn(
'flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded-sm shadow-xs shadow-shadow-shadow-3',
checkClassName,
disabled && disabledClassName,
className,
)}
onClick={(event) => {
if (disabled)
return
onCheck?.(event)
}}
onKeyDown={(event) => {
if (disabled)
return
if (event.key === ' ' || event.key === 'Enter') {
if (event.key === ' ')
event.preventDefault()
onCheck?.(event)
}
}}
data-testid={`checkbox-${id}`}
role="checkbox"
aria-checked={indeterminate ? 'mixed' : !!checked}
aria-disabled={!!disabled}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
tabIndex={disabled ? -1 : 0}
>
{!checked && indeterminate && <IndeterminateIcon />}
{checked && <div className="i-ri-check-line h-3 w-3" data-testid={`check-icon-${id}`} />}
</div>
)
}
export default Checkbox

View File

@ -1,6 +1,6 @@
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { useFieldContext } from '../..'
import Checkbox from '../../../checkbox'
type CheckboxFieldProps = {
label: string
@ -14,30 +14,22 @@ const CheckboxField = ({
const field = useFieldContext<boolean>()
return (
<div className="flex gap-2">
<div className="flex h-6 shrink-0 items-center">
<label className="flex cursor-pointer gap-2">
<span className="flex h-6 shrink-0 items-center">
<Checkbox
id={field.name}
checked={field.state.value}
ariaLabel={label}
onCheck={() => {
field.handleChange(!field.state.value)
}}
onCheckedChange={checked => field.handleChange(checked)}
/>
</div>
<label
htmlFor={field.name}
</span>
<span
className={cn(
'grow cursor-pointer pt-1 system-sm-medium text-text-secondary',
'grow pt-1 system-sm-medium text-text-secondary',
labelClassName,
)}
onClick={() => {
field.handleChange(!field.state.value)
}}
>
{label}
</label>
</div>
</span>
</label>
)
}

View File

@ -291,7 +291,25 @@ describe('MarkdownForm', () => {
render(<MarkdownForm node={node} />)
await user.click(screen.getByTestId('checkbox-acceptTerms'))
await user.click(screen.getByRole('checkbox', { name: 'Accept terms' }))
await user.click(screen.getByRole('button', { name: 'Submit' }))
await waitFor(() => {
expect(mockOnSend).toHaveBeenCalledWith('acceptTerms: true')
})
})
it('should toggle checkbox when external label is clicked', async () => {
const user = userEvent.setup()
const node = createRootNode([
createElementNode('label', { htmlFor: 'acceptTerms' }, [createTextNode('Accept terms')]),
createElementNode('input', { type: 'checkbox', name: 'acceptTerms', value: false }),
createElementNode('button', {}, [createTextNode('Submit')]),
])
render(<MarkdownForm node={node} />)
await user.click(screen.getByText('Accept terms'))
await user.click(screen.getByRole('button', { name: 'Submit' }))
await waitFor(() => {
@ -689,7 +707,7 @@ describe('MarkdownForm', () => {
render(<MarkdownForm node={node} />)
expect(screen.getByTestId('checkbox-agree'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'agree' }))!.toBeInTheDocument()
})
it('should render select with no options when dataOptions is missing', () => {

View File

@ -1,11 +1,11 @@
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { Dayjs } from 'dayjs'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger, SelectValue } from '@langgenius/dify-ui/select'
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useChatContext } from '@/app/components/base/chat/chat/context'
import Checkbox from '@/app/components/base/checkbox'
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
import { formatDateForOutput, toDayjs } from '@/app/components/base/date-and-time-picker/utils/dayjs'
@ -87,6 +87,10 @@ function getTextContent(node: HastElement): string {
return textChild?.value ?? ''
}
function getLabelTarget(node: HastElement): string {
return str(node.properties.htmlFor || node.properties.for || node.properties.name)
}
function str(val: unknown): string {
if (val == null)
return ''
@ -243,7 +247,7 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
return (
<label
key={key}
htmlFor={str(child.properties.htmlFor || child.properties.name)}
htmlFor={getLabelTarget(child)}
className="my-2 system-md-semibold text-text-secondary"
data-testid="label-field"
>
@ -281,14 +285,20 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
)
}
if (type === SUPPORTED_TYPES.CHECKBOX) {
const label = str(child.properties.dataTip || child.properties['data-tip'])
const hasExternalLabel = elementChildren.some(node =>
node.tagName === SUPPORTED_TAGS.LABEL && getLabelTarget(node) === name,
)
const checkboxAriaLabel = label || (hasExternalLabel ? undefined : name)
return (
<div className="mt-2 flex h-6 items-center space-x-2" key={key}>
<Checkbox
checked={!!formValues[name]}
onCheck={() => updateValue(name, !formValues[name])}
id={name}
checked={!!formValues[name]}
aria-label={checkboxAriaLabel}
onCheckedChange={checked => updateValue(name, checked)}
/>
<span>{str(child.properties.dataTip || child.properties['data-tip'])}</span>
{label && <span>{label}</span>}
</div>
)
}

View File

@ -120,7 +120,7 @@ describe('NotionPageSelector Base', () => {
expect(screen.getByTestId('notion-page-selector-base')).toBeInTheDocument()
expect(screen.getByTestId('notion-page-name-root-1')).toBeInTheDocument()
const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-root-1')
const checkbox = screen.getByRole('checkbox', { name: 'Root 1' })
await user.click(checkbox)
expect(handleSelect).toHaveBeenCalled()
@ -137,8 +137,8 @@ describe('NotionPageSelector Base', () => {
const user = userEvent.setup()
render(<NotionPageSelector credentialList={mockCredentialList} onSelect={handleSelect} />)
const boundCheckbox = screen.getByTestId('checkbox-notion-page-checkbox-bound-1')
expect(screen.getByTestId('check-icon-notion-page-checkbox-bound-1')).toBeInTheDocument()
const boundCheckbox = screen.getByRole('checkbox', { name: 'Bound 1' })
expect(boundCheckbox).toHaveAttribute('aria-checked', 'true')
await user.click(boundCheckbox)
expect(handleSelect).not.toHaveBeenCalled()
})
@ -263,10 +263,10 @@ describe('NotionPageSelector Base', () => {
const { rerender } = render(
<NotionPageSelector credentialList={mockCredentialList} onSelect={vi.fn()} value={['root-1']} />,
)
expect(screen.getByTestId('check-icon-notion-page-checkbox-root-1')).toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'Root 1' })).toHaveAttribute('aria-checked', 'true')
rerender(<NotionPageSelector credentialList={mockCredentialList} onSelect={vi.fn()} value={[]} />)
expect(screen.queryByTestId('check-icon-notion-page-checkbox-root-1')).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'Root 1' })).toHaveAttribute('aria-checked', 'false')
})
it('should hide preview actions when canPreview is false', () => {

View File

@ -58,7 +58,7 @@ describe('PageSelector', () => {
const user = userEvent.setup()
render(<PageSelector value={new Set()} disabledValue={new Set()} searchValue="" pagesMap={mockPagesMap} list={[mockList[0]!, mockList[1]!, mockList[2]!]} onSelect={handleSelect} />)
const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-root-1')
const checkbox = screen.getByRole('checkbox', { name: 'Root 1' })
await user.click(checkbox)
expect(handleSelect).toHaveBeenCalledWith(new Set(['root-1', 'child-1', 'grandchild-1']))
@ -69,7 +69,7 @@ describe('PageSelector', () => {
const user = userEvent.setup()
render(<PageSelector value={new Set(['root-1', 'child-1', 'grandchild-1'])} disabledValue={new Set()} searchValue="" pagesMap={mockPagesMap} list={mockList} onSelect={handleSelect} />)
const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-root-1')
const checkbox = screen.getByRole('checkbox', { name: 'Root 1' })
await user.click(checkbox)
expect(handleSelect).toHaveBeenCalledWith(new Set())
@ -103,7 +103,7 @@ describe('PageSelector', () => {
const user = userEvent.setup()
render(<PageSelector value={new Set()} disabledValue={new Set()} searchValue="Child" pagesMap={mockPagesMap} list={mockList} onSelect={handleSelect} />)
const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-child-1')
const checkbox = screen.getByRole('checkbox', { name: 'Child 1' })
await user.click(checkbox)
expect(handleSelect).toHaveBeenCalledWith(new Set(['child-1']))
@ -135,7 +135,7 @@ describe('PageSelector', () => {
const user = userEvent.setup()
render(<PageSelector value={new Set()} disabledValue={new Set(['root-1'])} searchValue="" pagesMap={mockPagesMap} list={mockList} onSelect={handleSelect} />)
const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-root-1')
const checkbox = screen.getByRole('checkbox', { name: 'Root 1' })
await user.click(checkbox)
expect(handleSelect).not.toHaveBeenCalled()
})
@ -169,8 +169,8 @@ describe('PageSelector', () => {
const user = userEvent.setup()
const { rerender } = render(<PageSelector value={new Set()} disabledValue={new Set()} searchValue="Child" pagesMap={mockPagesMap} list={mockList} onSelect={handleSelect} />)
const checkbox1 = screen.getByTestId('checkbox-notion-page-checkbox-child-1')
const checkbox2 = screen.getByTestId('checkbox-notion-page-checkbox-child-2')
const checkbox1 = screen.getByRole('checkbox', { name: 'Child 1' })
const checkbox2 = screen.getByRole('checkbox', { name: 'Child 2' })
await user.click(checkbox1)
expect(handleSelect).toHaveBeenCalledWith(new Set(['child-1']))
@ -219,7 +219,7 @@ describe('PageSelector', () => {
const user = userEvent.setup()
render(<PageSelector value={new Set(['root-1', 'child-1', 'grandchild-1', 'child-2'])} disabledValue={new Set()} searchValue="" pagesMap={mockPagesMap} list={mockList} onSelect={handleSelect} />)
const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-root-1')
const checkbox = screen.getByRole('checkbox', { name: 'Root 1' })
await user.click(checkbox)
expect(handleSelect).toHaveBeenCalledWith(new Set())
@ -230,7 +230,7 @@ describe('PageSelector', () => {
const user = userEvent.setup()
render(<PageSelector value={new Set()} disabledValue={new Set()} searchValue="Child" pagesMap={mockPagesMap} list={[mockList[1]!]} onSelect={handleSelect} />)
const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-child-1')
const checkbox = screen.getByRole('checkbox', { name: 'Child 1' })
await user.click(checkbox)
// When searching, only the item itself is selected, not descendants
@ -242,7 +242,7 @@ describe('PageSelector', () => {
const user = userEvent.setup()
render(<PageSelector value={new Set(['child-1'])} disabledValue={new Set()} searchValue="Child" pagesMap={mockPagesMap} list={[mockList[1]!]} onSelect={handleSelect} />)
const checkbox = screen.getByTestId('checkbox-notion-page-checkbox-child-1')
const checkbox = screen.getByRole('checkbox', { name: 'Child 1' })
await user.click(checkbox)
expect(handleSelect).toHaveBeenCalledWith(new Set())

View File

@ -54,7 +54,7 @@ describe('PageRow', () => {
renderPageRow({ onSelect })
await user.click(screen.getByTestId('checkbox-notion-page-checkbox-page-1'))
await user.click(screen.getByRole('checkbox', { name: 'Page 1' }))
expect(onSelect).toHaveBeenCalledWith('page-1')
})

View File

@ -1,10 +1,10 @@
import type { CSSProperties } from 'react'
import type { NotionPageRow as NotionPageRowData, NotionPageSelectionMode } from './types'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import NotionIcon from '@/app/components/base/notion-icon'
import Radio from '@/app/components/base/radio/ui'
@ -51,8 +51,8 @@ const NotionPageRow = ({
className="mr-2 shrink-0"
checked={checked}
disabled={disabled}
onCheck={() => onSelect(pageId)}
id={`notion-page-checkbox-${pageId}`}
aria-label={row.page.page_name}
onCheckedChange={() => onSelect(pageId)}
/>
)
: (

View File

@ -3,12 +3,12 @@
import type { FC } from 'react'
import type { PreProcessingRule, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import {
RiAlertFill,
RiSearchEyeLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Divider from '@/app/components/base/divider'
import { Infotip } from '@/app/components/base/infotip'
import SummaryIndexSetting from '@/app/components/datasets/settings/summary-index-setting'
@ -141,16 +141,18 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
</div>
<div className="mt-1">
{rules.map(rule => (
<div
<label
key={rule.id}
className={s.ruleItem}
onClick={() => onRuleToggle(rule.id)}
className={`${s.ruleItem} cursor-pointer`}
>
<Checkbox checked={rule.enabled} />
<label className="ml-2 cursor-pointer system-sm-regular text-text-secondary">
<Checkbox
checked={rule.enabled}
onCheckedChange={() => onRuleToggle(rule.id)}
/>
<span className="ml-2 system-sm-regular text-text-secondary">
{getRuleName(rule.id)}
</label>
</div>
</span>
</label>
))}
{
showSummaryIndexSetting && IS_CE_EDITION && (
@ -167,25 +169,25 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
<>
<Divider type="horizontal" className="my-4 bg-divider-subtle" />
<div className="flex items-center py-0.5">
<div
className="flex items-center"
onClick={() => {
if (hasCurrentDatasetDocForm)
return
if (currentDocForm === ChunkingMode.qa)
onDocFormChange(ChunkingMode.text)
else
onDocFormChange(ChunkingMode.qa)
}}
<label
className={`flex items-center ${hasCurrentDatasetDocForm ? '' : 'cursor-pointer'}`}
>
<Checkbox
checked={currentDocForm === ChunkingMode.qa}
disabled={hasCurrentDatasetDocForm}
onCheckedChange={() => {
if (hasCurrentDatasetDocForm)
return
if (currentDocForm === ChunkingMode.qa)
onDocFormChange(ChunkingMode.text)
else
onDocFormChange(ChunkingMode.qa)
}}
/>
<label className="ml-2 cursor-pointer system-sm-regular text-text-secondary">
<span className="ml-2 system-sm-regular text-text-secondary">
{t('stepTwo.useQALanguage', { ns: 'datasetCreation' })}
</label>
</div>
</span>
</label>
<LanguageSelect
currentLanguage={docLanguage || locale}
onSelect={onDocLanguageChange}

View File

@ -4,9 +4,9 @@ import type { FC } from 'react'
import type { ParentChildConfig } from '../hooks'
import type { ParentMode, PreProcessingRule, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { RiSearchEyeLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Divider from '@/app/components/base/divider'
import { ParentChildChunk } from '@/app/components/base/icons/src/vender/knowledge'
import RadioCard from '@/app/components/base/radio-card'
@ -179,16 +179,18 @@ export const ParentChildOptions: FC<ParentChildOptionsProps> = ({
</div>
<div className="mt-1">
{rules.map(rule => (
<div
<label
key={rule.id}
className={s.ruleItem}
onClick={() => onRuleToggle(rule.id)}
className={`${s.ruleItem} cursor-pointer`}
>
<Checkbox checked={rule.enabled} />
<label className="ml-2 cursor-pointer system-sm-regular text-text-secondary">
<Checkbox
checked={rule.enabled}
onCheckedChange={() => onRuleToggle(rule.id)}
/>
<span className="ml-2 system-sm-regular text-text-secondary">
{getRuleName(rule.id)}
</label>
</div>
</span>
</label>
))}
{
showSummaryIndexSetting && IS_CE_EDITION && (

View File

@ -277,8 +277,7 @@ describe('CrawledResultItem', () => {
const props = createItemProps()
render(<CrawledResultItem {...props} />)
// Find checkbox by data-testid
const checkbox = screen.getByTestId('checkbox-test-item')
const checkbox = screen.getByRole('checkbox', { name: /test page title/i })
expect(checkbox)!.toBeInTheDocument()
})
@ -296,7 +295,7 @@ describe('CrawledResultItem', () => {
const props = createItemProps({ isChecked: false, onCheckChange })
render(<CrawledResultItem {...props} />)
const checkbox = screen.getByTestId('checkbox-test-item')
const checkbox = screen.getByRole('checkbox', { name: /test page title/i })
await userEvent.click(checkbox)
expect(onCheckChange).toHaveBeenCalledWith(true)
@ -307,7 +306,7 @@ describe('CrawledResultItem', () => {
const props = createItemProps({ isChecked: true, onCheckChange })
render(<CrawledResultItem {...props} />)
const checkbox = screen.getByTestId('checkbox-test-item')
const checkbox = screen.getByRole('checkbox', { name: /test page title/i })
await userEvent.click(checkbox)
expect(onCheckChange).toHaveBeenCalledWith(false)
@ -365,9 +364,8 @@ describe('CrawledResult', () => {
...overrides,
})
// Helper functions to get checkboxes by data-testid
const getSelectAllCheckbox = () => screen.getByTestId('checkbox-select-all')
const getItemCheckbox = (index: number) => screen.getByTestId(`checkbox-item-${index}`)
const getSelectAllCheckbox = () => screen.getByRole('checkbox', { name: /selectAll|resetAll/ })
const getItemCheckbox = (index: number) => screen.getByRole('checkbox', { name: new RegExp(`Page ${index + 1}`) })
describe('Rendering', () => {
it('should render all items in list', () => {

View File

@ -32,8 +32,8 @@ describe('CheckboxWithLabel', () => {
})
it('should toggle checked state on checkbox click', () => {
render(<CheckboxWithLabel isChecked={false} onChange={onChange} label="Toggle" testId="my-check" />)
fireEvent.click(screen.getByTestId('checkbox-my-check'))
render(<CheckboxWithLabel isChecked={false} onChange={onChange} label="Toggle" />)
fireEvent.click(screen.getByRole('checkbox', { name: 'Toggle' }))
expect(onChange).toHaveBeenCalledWith(true)
})
})

View File

@ -28,15 +28,15 @@ describe('CrawledResultItem', () => {
})
it('should call onCheckChange with true when unchecked checkbox is clicked', () => {
render(<CrawledResultItem {...defaultProps} isChecked={false} testId="crawl-item" />)
const checkbox = screen.getByTestId('checkbox-crawl-item')
render(<CrawledResultItem {...defaultProps} isChecked={false} />)
const checkbox = screen.getByRole('checkbox', { name: 'Example Page https://example.com/page' })
fireEvent.click(checkbox)
expect(defaultProps.onCheckChange).toHaveBeenCalledWith(true)
})
it('should call onCheckChange with false when checked checkbox is clicked', () => {
render(<CrawledResultItem {...defaultProps} isChecked={true} testId="crawl-item" />)
const checkbox = screen.getByTestId('checkbox-crawl-item')
render(<CrawledResultItem {...defaultProps} isChecked={true} />)
const checkbox = screen.getByRole('checkbox', { name: 'Example Page https://example.com/page' })
fireEvent.click(checkbox)
expect(defaultProps.onCheckChange).toHaveBeenCalledWith(false)
})

View File

@ -3,48 +3,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CrawledResult from '../crawled-result'
vi.mock('../checkbox-with-label', () => ({
default: ({ isChecked, onChange, label, testId }: {
isChecked: boolean
onChange: (checked: boolean) => void
label: string
testId?: string
}) => (
<label data-testid={testId}>
<input
type="checkbox"
checked={isChecked}
onChange={() => onChange(!isChecked)}
data-testid={`checkbox-${testId}`}
/>
<span>{label}</span>
</label>
),
}))
vi.mock('../crawled-result-item', () => ({
default: ({ payload, isChecked, isPreview, onCheckChange, onPreview, testId }: {
payload: CrawlResultItem
isChecked: boolean
isPreview: boolean
onCheckChange: (checked: boolean) => void
onPreview: () => void
testId?: string
}) => (
<div data-testid={testId} data-preview={isPreview}>
<input
type="checkbox"
checked={isChecked}
onChange={() => onCheckChange(!isChecked)}
data-testid={`check-${testId}`}
/>
<span>{payload.title}</span>
<span>{payload.source_url}</span>
<button onClick={onPreview} data-testid={`preview-${testId}`}>Preview</button>
</div>
),
}))
const createMockItem = (overrides: Partial<CrawlResultItem> = {}): CrawlResultItem => ({
title: 'Test Page',
markdown: '# Test',
@ -80,7 +38,7 @@ describe('CrawledResult', () => {
/>,
)
expect(screen.getByTestId('select-all'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /selectAll/i })).toBeInTheDocument()
})
it('should render all items from list', () => {
@ -95,9 +53,9 @@ describe('CrawledResult', () => {
/>,
)
expect(screen.getByTestId('item-0'))!.toBeInTheDocument()
expect(screen.getByTestId('item-1'))!.toBeInTheDocument()
expect(screen.getByTestId('item-2'))!.toBeInTheDocument()
expect(screen.getByText('Page 1')).toBeInTheDocument()
expect(screen.getByText('Page 2')).toBeInTheDocument()
expect(screen.getByText('Page 3')).toBeInTheDocument()
})
it('should render scrap time info', () => {
@ -146,7 +104,7 @@ describe('CrawledResult', () => {
/>,
)
const selectAllCheckbox = screen.getByTestId('checkbox-select-all')
const selectAllCheckbox = screen.getByRole('checkbox', { name: /selectAll/i })
fireEvent.click(selectAllCheckbox)
expect(mockOnSelectedChange).toHaveBeenCalledWith(list)
@ -164,7 +122,7 @@ describe('CrawledResult', () => {
/>,
)
const selectAllCheckbox = screen.getByTestId('checkbox-select-all')
const selectAllCheckbox = screen.getByRole('checkbox', { name: /resetAll/i })
fireEvent.click(selectAllCheckbox)
expect(mockOnSelectedChange).toHaveBeenCalledWith([])
@ -215,7 +173,7 @@ describe('CrawledResult', () => {
/>,
)
const item1Checkbox = screen.getByTestId('check-item-1')
const item1Checkbox = screen.getByRole('checkbox', { name: /Page 2/ })
fireEvent.click(item1Checkbox)
expect(mockOnSelectedChange).toHaveBeenCalledWith([list[0], list[1]])
@ -234,7 +192,7 @@ describe('CrawledResult', () => {
/>,
)
const item0Checkbox = screen.getByTestId('check-item-0')
const item0Checkbox = screen.getByRole('checkbox', { name: /Page 1/ })
fireEvent.click(item0Checkbox)
expect(mockOnSelectedChange).toHaveBeenCalledWith([list[1]])
@ -254,7 +212,7 @@ describe('CrawledResult', () => {
/>,
)
const previewButton = screen.getByTestId('preview-item-1')
const previewButton = screen.getAllByRole('button', { name: /preview/i })[1]!
fireEvent.click(previewButton)
expect(mockOnPreview).toHaveBeenCalledWith(list[1])
@ -272,11 +230,11 @@ describe('CrawledResult', () => {
/>,
)
const previewButton = screen.getByTestId('preview-item-0')
const previewButton = screen.getAllByRole('button', { name: /preview/i })[0]!
fireEvent.click(previewButton)
const item0 = screen.getByTestId('item-0')
expect(item0)!.toHaveAttribute('data-preview', 'true')
const item0 = screen.getByText('Page 1').closest('.rounded-lg')
expect(item0).toHaveClass('bg-state-base-active')
})
})
@ -292,7 +250,7 @@ describe('CrawledResult', () => {
/>,
)
expect(screen.getByTestId('select-all'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /resetAll/i })).toBeInTheDocument()
})
it('should handle single item list', () => {
@ -307,7 +265,7 @@ describe('CrawledResult', () => {
/>,
)
expect(screen.getByTestId('item-0'))!.toBeInTheDocument()
expect(screen.getByText('Test Page')).toBeInTheDocument()
})
})
})

View File

@ -1,9 +1,6 @@
'use client'
import type { FC } from 'react'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react'
import { useId } from 'react'
import Checkbox from '@/app/components/base/checkbox'
import { Infotip } from '@/app/components/base/infotip'
type Props = {
@ -13,33 +10,28 @@ type Props = {
label: string
labelClassName?: string
tooltip?: string
testId?: string
}
const CheckboxWithLabel: FC<Props> = ({
export default function CheckboxWithLabel({
className = '',
isChecked,
onChange,
label,
labelClassName,
tooltip,
testId,
}) => {
const labelId = useId()
const handleToggle = () => onChange(!isChecked)
}: Props) {
return (
<div className={cn(className, 'flex h-7 items-center')}>
<Checkbox checked={isChecked} onCheck={handleToggle} id={testId} ariaLabelledBy={labelId} />
<div className="ml-2 flex min-w-0 items-center gap-1">
<button
type="button"
id={labelId}
className={cn('min-w-0 cursor-pointer border-0 bg-transparent p-0 text-left text-sm font-normal text-text-secondary', labelClassName)}
onClick={handleToggle}
>
<label className="flex min-w-0 cursor-pointer items-center">
<Checkbox
checked={isChecked}
onCheckedChange={checked => onChange(checked)}
/>
<span className={cn('ml-2 min-w-0 text-left text-sm font-normal text-text-secondary', labelClassName)}>
{label}
</button>
</span>
</label>
<div className="ml-1 flex min-w-0 items-center">
{tooltip && (
<Infotip aria-label={tooltip} popupClassName="w-[200px]">
{tooltip}
@ -49,4 +41,3 @@ const CheckboxWithLabel: FC<Props> = ({
</div>
)
}
export default React.memo(CheckboxWithLabel)

View File

@ -2,11 +2,10 @@
import type { FC } from 'react'
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
type Props = {
payload: CrawlResultItemType
@ -14,7 +13,6 @@ type Props = {
isPreview: boolean
onCheckChange: (checked: boolean) => void
onPreview: () => void
testId?: string
}
const CrawledResultItem: FC<Props> = ({
@ -23,33 +21,34 @@ const CrawledResultItem: FC<Props> = ({
isChecked,
onCheckChange,
onPreview,
testId,
}) => {
const { t } = useTranslation()
const handleCheckChange = useCallback(() => {
onCheckChange(!isChecked)
}, [isChecked, onCheckChange])
return (
<div className={cn(isPreview ? 'bg-state-base-active' : 'group hover:bg-state-base-hover', 'cursor-pointer rounded-lg p-2')}>
<div className={cn(isPreview ? 'bg-state-base-active' : 'group hover:bg-state-base-hover', 'rounded-lg p-2')}>
<div className="relative flex">
<div className="flex h-5 items-center">
<Checkbox className="mr-2 shrink-0" checked={isChecked} onCheck={handleCheckChange} id={testId} />
</div>
<div className="flex min-w-0 grow flex-col">
<div
className="truncate text-sm font-medium text-text-secondary"
title={payload.title}
>
{payload.title}
<label className="flex min-w-0 grow cursor-pointer">
<div className="flex h-5 items-center">
<Checkbox
className="mr-2 shrink-0"
checked={isChecked}
onCheckedChange={checked => onCheckChange(checked)}
/>
</div>
<div
className="mt-0.5 truncate text-xs text-text-tertiary"
title={payload.source_url}
>
{payload.source_url}
<div className="flex min-w-0 grow flex-col">
<div
className="truncate text-sm font-medium text-text-secondary"
title={payload.title}
>
{payload.title}
</div>
<div
className="mt-0.5 truncate text-xs text-text-tertiary"
title={payload.source_url}
>
{payload.source_url}
</div>
</div>
</div>
</label>
<Button
onClick={onPreview}
className="top-0 right-0 hidden h-6 px-1.5 text-xs font-medium uppercase group-hover:absolute group-hover:block"

View File

@ -65,7 +65,6 @@ const CrawledResult: FC<Props> = ({
onChange={handleCheckedAll}
label={isCheckAll ? t(`${I18N_PREFIX}.resetAll`, { ns: 'datasetCreation' }) : t(`${I18N_PREFIX}.selectAll`, { ns: 'datasetCreation' })}
labelClassName="system-[13px] leading-[16px] font-medium text-text-secondary"
testId="select-all"
/>
<div className="text-xs text-text-tertiary">
{t(`${I18N_PREFIX}.scrapTimeInfo`, {
@ -84,7 +83,6 @@ const CrawledResult: FC<Props> = ({
payload={item}
isChecked={checkedList.some(checkedItem => checkedItem.source_url === item.source_url)}
onCheckChange={handleItemCheckChange(item)}
testId={`item-${index}`}
/>
))}
</div>

View File

@ -662,11 +662,9 @@ describe('FireCrawl', () => {
})
it('should call onCrawlOptionsChange when checkbox changes', () => {
const { container } = render(<FireCrawl {...defaultProps} />)
render(<FireCrawl {...defaultProps} />)
// Use data-testid to find checkboxes since they are custom div elements
const checkboxes = container.querySelectorAll('[data-testid^="checkbox-"]')
fireEvent.click(checkboxes[0]!) // crawl_sub_pages
fireEvent.click(screen.getByRole('checkbox', { name: /crawlSubPage/ }))
expect(mockOnCrawlOptionsChange).toHaveBeenCalledWith(
expect.objectContaining({ crawl_sub_pages: false }),

View File

@ -24,11 +24,6 @@ describe('Options', () => {
vi.clearAllMocks()
})
// Helper to get checkboxes by test id pattern
const getCheckboxes = (container: HTMLElement) => {
return container.querySelectorAll('[data-testid^="checkbox-"]')
}
describe('Rendering', () => {
it('should render without crashing', () => {
const payload = createMockCrawlOptions()
@ -95,10 +90,9 @@ describe('Options', () => {
it('should render two checkboxes', () => {
const payload = createMockCrawlOptions()
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
expect(checkboxes.length).toBe(2)
expect(screen.getAllByRole('checkbox')).toHaveLength(2)
})
})
@ -108,27 +102,25 @@ describe('Options', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: true })
render(<Options payload={payload} onChange={mockOnChange} />)
// First checkbox should have check icon when checked
// First checkbox should have check icon when checked
expect(screen.queryByTestId('check-icon-crawl-sub-page'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /crawlSubPage/i })).toHaveAttribute('aria-checked', 'true')
})
it('should display crawl_sub_pages checkbox without check icon when false', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: false })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.queryByTestId('check-icon-crawl-sub-page')).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /crawlSubPage/i })).toHaveAttribute('aria-checked', 'false')
})
it('should display only_main_content checkbox with check icon when true', () => {
const payload = createMockCrawlOptions({ only_main_content: true })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByTestId('check-icon-only-main-content'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /extractOnlyMainContent/i })).toHaveAttribute('aria-checked', 'true')
})
it('should display only_main_content checkbox without check icon when false', () => {
const payload = createMockCrawlOptions({ only_main_content: false })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.queryByTestId('check-icon-only-main-content')).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /extractOnlyMainContent/i })).toHaveAttribute('aria-checked', 'false')
})
it('should display limit value in input', () => {
@ -167,10 +159,9 @@ describe('Options', () => {
describe('User Interactions', () => {
it('should call onChange with updated crawl_sub_pages when checkbox is clicked', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: true })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
fireEvent.click(checkboxes[0]!)
fireEvent.click(screen.getByRole('checkbox', { name: /crawlSubPage/i }))
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
@ -180,10 +171,9 @@ describe('Options', () => {
it('should call onChange with updated only_main_content when checkbox is clicked', () => {
const payload = createMockCrawlOptions({ only_main_content: false })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
fireEvent.click(checkboxes[1]!)
fireEvent.click(screen.getByRole('checkbox', { name: /extractOnlyMainContent/i }))
expect(mockOnChange).toHaveBeenCalledWith({
...payload,

View File

@ -35,7 +35,6 @@ const Options: FC<Props> = ({
<div className={cn(className, 'space-y-2')}>
<CheckboxWithLabel
label={t(`${I18N_PREFIX}.crawlSubPage`, { ns: 'datasetCreation' })}
testId="crawl-sub-page"
isChecked={payload.crawl_sub_pages}
onChange={handleChange('crawl_sub_pages')}
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
@ -77,7 +76,6 @@ const Options: FC<Props> = ({
</div>
<CheckboxWithLabel
label={t(`${I18N_PREFIX}.extractOnlyMainContent`, { ns: 'datasetCreation' })}
testId="only-main-content"
isChecked={payload.only_main_content}
onChange={handleChange('only_main_content')}
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"

View File

@ -499,11 +499,9 @@ describe('JinaReader', () => {
render(<JinaReader {...props} />)
// Find and click the checkbox by data-testid
const checkbox = screen.getByTestId('checkbox-crawl-sub-pages')
const checkbox = screen.getByRole('checkbox', { name: /crawlSubPage/ })
fireEvent.click(checkbox)
// Assert - onCrawlOptionsChange should be called
expect(onCrawlOptionsChange).toHaveBeenCalled()
})

View File

@ -25,10 +25,6 @@ describe('Options (jina-reader)', () => {
vi.clearAllMocks()
})
const getCheckboxes = (container: HTMLElement) => {
return container.querySelectorAll('[data-testid^="checkbox-"]')
}
describe('Rendering', () => {
it('should render crawlSubPage and useSitemap checkboxes and limit field', () => {
const payload = createMockCrawlOptions()
@ -41,10 +37,9 @@ describe('Options (jina-reader)', () => {
it('should render two checkboxes', () => {
const payload = createMockCrawlOptions()
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
expect(checkboxes.length).toBe(2)
expect(screen.getAllByRole('checkbox')).toHaveLength(2)
})
it('should render limit field with required indicator', () => {
@ -71,25 +66,25 @@ describe('Options (jina-reader)', () => {
it('should display crawl_sub_pages checkbox with check icon when true', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: true })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByTestId('check-icon-crawl-sub-pages')).toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /crawlSubPage/i })).toHaveAttribute('aria-checked', 'true')
})
it('should display crawl_sub_pages checkbox without check icon when false', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: false })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.queryByTestId('check-icon-crawl-sub-pages')).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /crawlSubPage/i })).toHaveAttribute('aria-checked', 'false')
})
it('should display use_sitemap checkbox with check icon when true', () => {
const payload = createMockCrawlOptions({ use_sitemap: true })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByTestId('check-icon-use-sitemap')).toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /useSitemap/i })).toHaveAttribute('aria-checked', 'true')
})
it('should display use_sitemap checkbox without check icon when false', () => {
const payload = createMockCrawlOptions({ use_sitemap: false })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.queryByTestId('check-icon-use-sitemap')).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /useSitemap/i })).toHaveAttribute('aria-checked', 'false')
})
it('should display limit value in input', () => {
@ -105,7 +100,7 @@ describe('Options (jina-reader)', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: true })
render(<Options payload={payload} onChange={mockOnChange} />)
fireEvent.click(screen.getByTestId('checkbox-crawl-sub-pages'))
fireEvent.click(screen.getByRole('checkbox', { name: /crawlSubPage/i }))
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
@ -117,7 +112,7 @@ describe('Options (jina-reader)', () => {
const payload = createMockCrawlOptions({ use_sitemap: false })
render(<Options payload={payload} onChange={mockOnChange} />)
fireEvent.click(screen.getByTestId('checkbox-use-sitemap'))
fireEvent.click(screen.getByRole('checkbox', { name: /useSitemap/i }))
expect(mockOnChange).toHaveBeenCalledWith({
...payload,

View File

@ -38,7 +38,6 @@ const Options: FC<Props> = ({
isChecked={payload.crawl_sub_pages}
onChange={handleChange('crawl_sub_pages')}
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
testId="crawl-sub-pages"
/>
<CheckboxWithLabel
label={t(`${I18N_PREFIX}.useSitemap`, { ns: 'datasetCreation' })}
@ -46,7 +45,6 @@ const Options: FC<Props> = ({
onChange={handleChange('use_sitemap')}
tooltip={t(`${I18N_PREFIX}.useSitemapTooltip`, { ns: 'datasetCreation' }) as string}
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
testId="use-sitemap"
/>
<div className="flex justify-between space-x-4">
<Field

View File

@ -502,11 +502,9 @@ describe('WaterCrawl', () => {
render(<WaterCrawl {...props} />)
// Find and click the checkbox by data-testid
const checkbox = screen.getByTestId('checkbox-crawl-sub-pages')
const checkbox = screen.getByRole('checkbox', { name: /crawlSubPage/ })
fireEvent.click(checkbox)
// Assert - onCrawlOptionsChange should be called
expect(onCrawlOptionsChange).toHaveBeenCalled()
})

View File

@ -25,10 +25,6 @@ describe('Options (watercrawl)', () => {
vi.clearAllMocks()
})
const getCheckboxes = (container: HTMLElement) => {
return container.querySelectorAll('[data-testid^="checkbox-"]')
}
describe('Rendering', () => {
it('should render all form fields', () => {
const payload = createMockCrawlOptions()
@ -44,10 +40,9 @@ describe('Options (watercrawl)', () => {
it('should render two checkboxes', () => {
const payload = createMockCrawlOptions()
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
expect(checkboxes.length).toBe(2)
expect(screen.getAllByRole('checkbox')).toHaveLength(2)
})
it('should render limit field with required indicator', () => {
@ -89,29 +84,27 @@ describe('Options (watercrawl)', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: true })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByTestId('check-icon-crawl-sub-pages'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /crawlSubPage/i })).toHaveAttribute('aria-checked', 'true')
})
it('should display crawl_sub_pages checkbox without check icon when false', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: false })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
expect(checkboxes[0]!.querySelector('svg')).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /crawlSubPage/i })).toHaveAttribute('aria-checked', 'false')
})
it('should display only_main_content checkbox with check icon when true', () => {
const payload = createMockCrawlOptions({ only_main_content: true })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByTestId('check-icon-only-main-content'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /extractOnlyMainContent/i })).toHaveAttribute('aria-checked', 'true')
})
it('should display only_main_content checkbox without check icon when false', () => {
const payload = createMockCrawlOptions({ only_main_content: false })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
expect(checkboxes[1]!.querySelector('svg')).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /extractOnlyMainContent/i })).toHaveAttribute('aria-checked', 'false')
})
it('should display limit value in input', () => {
@ -146,10 +139,9 @@ describe('Options (watercrawl)', () => {
describe('User Interactions', () => {
it('should call onChange with updated crawl_sub_pages when checkbox is clicked', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: true })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
fireEvent.click(checkboxes[0]!)
fireEvent.click(screen.getByRole('checkbox', { name: /crawlSubPage/i }))
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
@ -159,10 +151,9 @@ describe('Options (watercrawl)', () => {
it('should call onChange with updated only_main_content when checkbox is clicked', () => {
const payload = createMockCrawlOptions({ only_main_content: false })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
fireEvent.click(checkboxes[1]!)
fireEvent.click(screen.getByRole('checkbox', { name: /extractOnlyMainContent/i }))
expect(mockOnChange).toHaveBeenCalledWith({
...payload,

View File

@ -38,7 +38,6 @@ const Options: FC<Props> = ({
isChecked={payload.crawl_sub_pages}
onChange={handleChange('crawl_sub_pages')}
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
testId="crawl-sub-pages"
/>
<div className="flex justify-between space-x-4">
<Field
@ -80,7 +79,6 @@ const Options: FC<Props> = ({
isChecked={payload.only_main_content}
onChange={handleChange('only_main_content')}
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
testId="only-main-content"
/>
</div>
)

View File

@ -7,8 +7,6 @@ import DocumentList from '../list'
// Mock hooks used by DocumentList
const mockHandleSort = vi.fn()
const mockOnSelectAll = vi.fn()
const mockOnSelectOne = vi.fn()
const mockClearSelection = vi.fn()
const mockHandleAction = vi.fn(() => vi.fn())
const mockHandleBatchReIndex = vi.fn()
@ -24,10 +22,6 @@ vi.mock('../document-list/hooks', () => ({
handleSort: mockHandleSort,
})),
useDocumentSelection: vi.fn(() => ({
isAllSelected: false,
isSomeSelected: false,
onSelectAll: mockOnSelectAll,
onSelectOne: mockOnSelectOne,
hasErrorDocumentsSelected: false,
downloadableSelectedIds: [],
clearSelection: mockClearSelection,
@ -144,11 +138,9 @@ describe('DocumentList', () => {
})
it('should render select-all area when embeddingAvailable is true', () => {
const { container } = render(<DocumentList {...defaultProps} embeddingAvailable={true} />)
render(<DocumentList {...defaultProps} embeddingAvailable={true} />)
// Checkbox component renders inside the first td
const firstTd = container.querySelector('thead td')
expect(firstTd?.textContent).toContain('#')
expect(screen.getByRole('checkbox', { name: 'common.operation.selectAll' })).toBeInTheDocument()
})
it('should still render # column when embeddingAvailable is false', () => {
@ -171,6 +163,17 @@ describe('DocumentList', () => {
expect(screen.getByTestId('doc-row-a')).toBeInTheDocument()
expect(screen.getByTestId('doc-row-b')).toBeInTheDocument()
})
it('should call onSelectedIdChange when select-all is clicked', () => {
const docs = [createDoc({ id: 'a', name: 'Doc A' }), createDoc({ id: 'b', name: 'Doc B' })]
const onSelectedIdChange = vi.fn()
render(<DocumentList {...defaultProps} documents={docs} onSelectedIdChange={onSelectedIdChange} />)
fireEvent.click(screen.getByRole('checkbox', { name: 'common.operation.selectAll' }))
expect(onSelectedIdChange).toHaveBeenCalledWith(['a', 'b'])
})
})
// Verify sort headers trigger sort handler

View File

@ -189,11 +189,9 @@ describe('DocumentList', () => {
}
render(<DocumentList {...props} />, { wrapper: createWrapper() })
// When checked, checkbox should have a check icon (svg) inside
props.selectedIds.forEach((id) => {
const checkIcon = screen.getByTestId(`check-icon-doc-row-${id}`)
expect(checkIcon)!.toBeInTheDocument()
})
expect(screen.getByRole('checkbox', { name: 'Document 1.txt' })).toHaveAttribute('aria-checked', 'true')
expect(screen.getByRole('checkbox', { name: 'Document 2.txt' })).toHaveAttribute('aria-checked', 'true')
expect(screen.getByRole('checkbox', { name: 'Document 3.txt' })).toHaveAttribute('aria-checked', 'true')
})
it('should show indeterminate state when some are selected', () => {

View File

@ -1,5 +1,6 @@
import type { ReactNode } from 'react'
import type { SimpleDocumentDetail } from '@/models/datasets'
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@ -23,15 +24,21 @@ const createTestQueryClient = () => new QueryClient({
},
})
const createWrapper = () => {
const createWrapper = (value: string[] = [], onValueChange = vi.fn()) => {
const queryClient = createTestQueryClient()
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
<table>
<tbody>
{children}
</tbody>
</table>
<CheckboxGroup
value={value}
onValueChange={nextValue => onValueChange(nextValue)}
allValues={['doc-1']}
>
<table>
<tbody>
{children}
</tbody>
</table>
</CheckboxGroup>
</QueryClientProvider>
)
}
@ -74,22 +81,17 @@ const createMockDoc = (overrides: Record<string, unknown> = {}): LocalDoc => ({
...overrides,
}) as unknown as LocalDoc
// Helper to find the custom checkbox div (Checkbox component renders as a div, not a native checkbox)
const findCheckbox = (container: HTMLElement): HTMLElement | null => {
return container.querySelector('[class*="shadow-xs"]')
}
const getRowCheckbox = () => screen.getByRole('checkbox', { name: 'test-document.txt' })
describe('DocumentTableRow', () => {
const defaultProps = {
doc: createMockDoc(),
index: 0,
datasetId: 'dataset-1',
isSelected: false,
isGeneralMode: true,
isQAMode: false,
embeddingAvailable: true,
selectedIds: [],
onSelectOne: vi.fn(),
onSelectedIdChange: vi.fn(),
onShowRenameModal: vi.fn(),
onUpdate: vi.fn(),
@ -117,36 +119,28 @@ describe('DocumentTableRow', () => {
})
it('should render checkbox element', () => {
const { container } = render(<DocumentTableRow {...defaultProps} />, { wrapper: createWrapper() })
const checkbox = findCheckbox(container)
expect(checkbox)!.toBeInTheDocument()
render(<DocumentTableRow {...defaultProps} />, { wrapper: createWrapper() })
expect(getRowCheckbox())!.toBeInTheDocument()
})
})
describe('Selection', () => {
it('should show check icon when isSelected is true', () => {
const { container } = render(<DocumentTableRow {...defaultProps} isSelected />, { wrapper: createWrapper() })
const checkbox = findCheckbox(container)
expect(checkbox)!.toBeInTheDocument()
expect(screen.getByTestId('check-icon-doc-row-doc-1'))!.toBeInTheDocument()
it('should show check icon when document id is selected by CheckboxGroup', () => {
render(<DocumentTableRow {...defaultProps} />, { wrapper: createWrapper(['doc-1']) })
expect(getRowCheckbox()).toHaveAttribute('aria-checked', 'true')
})
it('should not show check icon when isSelected is false', () => {
const { container } = render(<DocumentTableRow {...defaultProps} isSelected={false} />, { wrapper: createWrapper() })
const checkbox = findCheckbox(container)
expect(checkbox)!.toBeInTheDocument()
expect(screen.queryByTestId('check-icon-doc-row-doc-1')).not.toBeInTheDocument()
it('should not show check icon when document id is not selected by CheckboxGroup', () => {
render(<DocumentTableRow {...defaultProps} />, { wrapper: createWrapper() })
expect(getRowCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should call onSelectOne when checkbox is clicked', () => {
const onSelectOne = vi.fn()
const { container } = render(<DocumentTableRow {...defaultProps} onSelectOne={onSelectOne} />, { wrapper: createWrapper() })
it('should call CheckboxGroup onValueChange when checkbox is clicked', () => {
const onValueChange = vi.fn()
render(<DocumentTableRow {...defaultProps} />, { wrapper: createWrapper([], onValueChange) })
const checkbox = findCheckbox(container)
if (checkbox) {
fireEvent.click(checkbox)
expect(onSelectOne).toHaveBeenCalledWith('doc-1')
}
fireEvent.click(getRowCheckbox())
expect(onValueChange).toHaveBeenCalledWith(['doc-1'])
})
it('should stop propagation when checkbox container is clicked', () => {

View File

@ -1,11 +1,10 @@
import type { FC } from 'react'
import type { SimpleDocumentDetail } from '@/models/datasets'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { pick } from 'es-toolkit/object'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import ChunkingModeLabel from '@/app/components/datasets/common/chunking-mode-label'
import Operations from '@/app/components/datasets/documents/components/operations'
import SummaryStatus from '@/app/components/datasets/documents/detail/completed/common/summary-status'
@ -23,12 +22,10 @@ type DocumentTableRowProps = {
doc: LocalDoc
index: number
datasetId: string
isSelected: boolean
isGeneralMode: boolean
isQAMode: boolean
embeddingAvailable: boolean
selectedIds: string[]
onSelectOne: (docId: string) => void
onSelectedIdChange: (ids: string[]) => void
onShowRenameModal: (doc: LocalDoc) => void
onUpdate: () => void
@ -44,24 +41,23 @@ const renderCount = (count: number | undefined) => {
return `${formatNumber((count / 1000).toFixed(1))}k`
}
const DocumentTableRow: FC<DocumentTableRowProps> = React.memo(({
const DocumentTableRow = React.memo(({
doc,
index,
datasetId,
isSelected,
isGeneralMode,
isQAMode,
embeddingAvailable,
selectedIds,
onSelectOne,
onSelectedIdChange,
onShowRenameModal,
onUpdate,
}) => {
}: DocumentTableRowProps) => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
const router = useRouter()
const searchParams = useSearchParams()
const documentNameId = React.useId()
const isFile = doc.data_source_type === DataSourceType.FILE
const fileType = isFile ? doc.data_source_detail_dict?.upload_file?.extension : ''
@ -89,9 +85,8 @@ const DocumentTableRow: FC<DocumentTableRowProps> = React.memo(({
<div className="flex items-center" onClick={handleCheckboxClick}>
<Checkbox
className="mr-2 shrink-0"
checked={isSelected}
onCheck={() => onSelectOne(doc.id)}
id={`doc-row-${doc.id}`}
value={doc.id}
aria-labelledby={documentNameId}
/>
{index + 1}
</div>
@ -104,7 +99,7 @@ const DocumentTableRow: FC<DocumentTableRowProps> = React.memo(({
<Tooltip>
<TooltipTrigger
render={(
<span className="grow truncate text-sm">{doc.name}</span>
<span id={documentNameId} className="grow truncate text-sm">{doc.name}</span>
)}
/>
<TooltipContent>

View File

@ -25,199 +25,6 @@ const createMockDocument = (overrides: Partial<LocalDoc> = {}): LocalDoc => ({
} as LocalDoc)
describe('useDocumentSelection', () => {
describe('isAllSelected', () => {
it('should return false when documents is empty', () => {
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() =>
useDocumentSelection({
documents: [],
selectedIds: [],
onSelectedIdChange,
}),
)
expect(result.current.isAllSelected).toBe(false)
})
it('should return true when all documents are selected', () => {
const docs = [
createMockDocument({ id: 'doc1' }),
createMockDocument({ id: 'doc2' }),
]
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() =>
useDocumentSelection({
documents: docs,
selectedIds: ['doc1', 'doc2'],
onSelectedIdChange,
}),
)
expect(result.current.isAllSelected).toBe(true)
})
it('should return false when not all documents are selected', () => {
const docs = [
createMockDocument({ id: 'doc1' }),
createMockDocument({ id: 'doc2' }),
]
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() =>
useDocumentSelection({
documents: docs,
selectedIds: ['doc1'],
onSelectedIdChange,
}),
)
expect(result.current.isAllSelected).toBe(false)
})
})
describe('isSomeSelected', () => {
it('should return false when no documents are selected', () => {
const docs = [createMockDocument({ id: 'doc1' })]
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() =>
useDocumentSelection({
documents: docs,
selectedIds: [],
onSelectedIdChange,
}),
)
expect(result.current.isSomeSelected).toBe(false)
})
it('should return true when some documents are selected', () => {
const docs = [
createMockDocument({ id: 'doc1' }),
createMockDocument({ id: 'doc2' }),
]
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() =>
useDocumentSelection({
documents: docs,
selectedIds: ['doc1'],
onSelectedIdChange,
}),
)
expect(result.current.isSomeSelected).toBe(true)
})
})
describe('onSelectAll', () => {
it('should select all documents when none are selected', () => {
const docs = [
createMockDocument({ id: 'doc1' }),
createMockDocument({ id: 'doc2' }),
]
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() =>
useDocumentSelection({
documents: docs,
selectedIds: [],
onSelectedIdChange,
}),
)
act(() => {
result.current.onSelectAll()
})
expect(onSelectedIdChange).toHaveBeenCalledWith(['doc1', 'doc2'])
})
it('should deselect all when all are selected', () => {
const docs = [
createMockDocument({ id: 'doc1' }),
createMockDocument({ id: 'doc2' }),
]
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() =>
useDocumentSelection({
documents: docs,
selectedIds: ['doc1', 'doc2'],
onSelectedIdChange,
}),
)
act(() => {
result.current.onSelectAll()
})
expect(onSelectedIdChange).toHaveBeenCalledWith([])
})
it('should add to existing selection when some are selected', () => {
const docs = [
createMockDocument({ id: 'doc1' }),
createMockDocument({ id: 'doc2' }),
createMockDocument({ id: 'doc3' }),
]
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() =>
useDocumentSelection({
documents: docs,
selectedIds: ['doc1'],
onSelectedIdChange,
}),
)
act(() => {
result.current.onSelectAll()
})
expect(onSelectedIdChange).toHaveBeenCalledWith(['doc1', 'doc2', 'doc3'])
})
})
describe('onSelectOne', () => {
it('should add document to selection when not selected', () => {
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() =>
useDocumentSelection({
documents: [],
selectedIds: [],
onSelectedIdChange,
}),
)
act(() => {
result.current.onSelectOne('doc1')
})
expect(onSelectedIdChange).toHaveBeenCalledWith(['doc1'])
})
it('should remove document from selection when already selected', () => {
const onSelectedIdChange = vi.fn()
const { result } = renderHook(() =>
useDocumentSelection({
documents: [],
selectedIds: ['doc1', 'doc2'],
onSelectedIdChange,
}),
)
act(() => {
result.current.onSelectOne('doc1')
})
expect(onSelectedIdChange).toHaveBeenCalledWith(['doc2'])
})
})
describe('hasErrorDocumentsSelected', () => {
it('should return false when no error documents are selected', () => {
const docs = [

View File

@ -1,5 +1,4 @@
import type { SimpleDocumentDetail } from '@/models/datasets'
import { uniq } from 'es-toolkit/array'
import { useCallback, useMemo } from 'react'
import { DataSourceType } from '@/models/datasets'
@ -16,29 +15,6 @@ export const useDocumentSelection = ({
selectedIds,
onSelectedIdChange,
}: UseDocumentSelectionOptions) => {
const isAllSelected = useMemo(() => {
return documents.length > 0 && documents.every(doc => selectedIds.includes(doc.id))
}, [documents, selectedIds])
const isSomeSelected = useMemo(() => {
return documents.some(doc => selectedIds.includes(doc.id))
}, [documents, selectedIds])
const onSelectAll = useCallback(() => {
if (isAllSelected)
onSelectedIdChange([])
else
onSelectedIdChange(uniq([...selectedIds, ...documents.map(doc => doc.id)]))
}, [isAllSelected, documents, onSelectedIdChange, selectedIds])
const onSelectOne = useCallback((docId: string) => {
onSelectedIdChange(
selectedIds.includes(docId)
? selectedIds.filter(id => id !== docId)
: [...selectedIds, docId],
)
}, [selectedIds, onSelectedIdChange])
const hasErrorDocumentsSelected = useMemo(() => {
return documents.some(doc => selectedIds.includes(doc.id) && doc.display_status === 'error')
}, [documents, selectedIds])
@ -55,10 +31,6 @@ export const useDocumentSelection = ({
}, [onSelectedIdChange])
return {
isAllSelected,
isSomeSelected,
onSelectAll,
onSelectOne,
hasErrorDocumentsSelected,
downloadableSelectedIds,
clearSelection,

View File

@ -1,12 +1,11 @@
'use client'
import type { FC } from 'react'
import type { Props as PaginationProps } from '@/app/components/base/pagination'
import type { SimpleDocumentDetail } from '@/models/datasets'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Pagination from '@/app/components/base/pagination'
import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
import useBatchEditDocumentMetadata from '@/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata'
@ -36,7 +35,7 @@ type DocumentListProps = {
/**
* Document list component including basic information
*/
const DocumentList: FC<DocumentListProps> = ({
const DocumentList = ({
embeddingAvailable,
documents = [],
selectedIds,
@ -47,7 +46,7 @@ const DocumentList: FC<DocumentListProps> = ({
onManageMetadata,
remoteSortValue,
onSortChange,
}) => {
}: DocumentListProps) => {
const { t } = useTranslation()
const datasetConfig = useDatasetDetailContext(s => s.dataset)
const chunkingMode = datasetConfig?.doc_form
@ -62,10 +61,6 @@ const DocumentList: FC<DocumentListProps> = ({
// Selection
const {
isAllSelected,
isSomeSelected,
onSelectAll,
onSelectOne,
hasErrorDocumentsSelected,
downloadableSelectedIds,
clearSelection,
@ -74,6 +69,7 @@ const DocumentList: FC<DocumentListProps> = ({
selectedIds,
onSelectedIdChange,
})
const documentIds = useMemo(() => documents.map(doc => doc.id), [documents])
// Actions
const { handleAction, handleBatchReIndex, handleBatchDownload } = useDocumentActions({
@ -116,7 +112,12 @@ const DocumentList: FC<DocumentListProps> = ({
return (
<div className="relative mt-3 flex h-full w-full flex-col">
<div className="relative h-0 grow overflow-x-auto">
<CheckboxGroup
value={selectedIds}
onValueChange={nextSelectedIds => onSelectedIdChange(nextSelectedIds)}
allValues={documentIds}
className="relative h-0 grow overflow-x-auto"
>
<table className={`w-full max-w-full min-w-[700px] border-collapse border-0 text-sm ${s.documentTable}`}>
<thead className="h-8 border-b border-divider-subtle text-xs leading-8 font-medium text-text-tertiary uppercase">
<tr>
@ -125,9 +126,8 @@ const DocumentList: FC<DocumentListProps> = ({
{embeddingAvailable && (
<Checkbox
className="mr-2 shrink-0"
checked={isAllSelected}
indeterminate={!isAllSelected && isSomeSelected}
onCheck={onSelectAll}
parent
aria-label={t('operation.selectAll', { ns: 'common' })}
/>
)}
#
@ -167,12 +167,10 @@ const DocumentList: FC<DocumentListProps> = ({
doc={doc}
index={index}
datasetId={datasetId}
isSelected={selectedIds.includes(doc.id)}
isGeneralMode={isGeneralMode}
isQAMode={isQAMode}
embeddingAvailable={embeddingAvailable}
selectedIds={selectedIds}
onSelectOne={onSelectOne}
onSelectedIdChange={onSelectedIdChange}
onShowRenameModal={handleShowRenameModal}
onUpdate={onUpdate}
@ -180,7 +178,7 @@ const DocumentList: FC<DocumentListProps> = ({
))}
</tbody>
</table>
</div>
</CheckboxGroup>
{selectedIds.length > 0 && (
<BatchAction

View File

@ -1327,7 +1327,7 @@ describe('useDatasourceActions', () => {
const { result } = renderHook(() => useDatasourceActions(params))
act(() => {
result.current.handleSelectAll()
result.current.handleSelectAll(true)
})
// Verify the callback was executed (no error thrown)
@ -1351,7 +1351,7 @@ describe('useDatasourceActions', () => {
const { result } = renderHook(() => useDatasourceActions(params))
act(() => {
result.current.handleSelectAll()
result.current.handleSelectAll(true)
})
expect(true).toBe(true)
@ -1961,7 +1961,7 @@ describe('useDatasourceActions - Async Functions', () => {
const { result } = renderHook(() => useDatasourceActions(params))
act(() => {
result.current.handleSelectAll()
result.current.handleSelectAll(false)
})
// Should deselect all since documents.length >= allIds.length
@ -2002,7 +2002,7 @@ describe('useDatasourceActions - Async Functions', () => {
const { result } = renderHook(() => useDatasourceActions(params))
act(() => {
result.current.handleSelectAll()
result.current.handleSelectAll(false)
})
// Should deselect all since selectedFileIds.length >= allKeys.length
@ -2531,7 +2531,7 @@ describe('useDatasourceActions - Edge Case Branches', () => {
const { result } = renderHook(() => useDatasourceActions(params))
act(() => {
result.current.handleSelectAll()
result.current.handleSelectAll(false)
})
// Should use empty array when currentWorkspacePages is undefined

View File

@ -1,4 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import Actions from '../index'
@ -17,6 +18,8 @@ vi.mock('@/next/link', () => ({
),
}))
const getSelectAllCheckbox = () => screen.getByRole('checkbox', { name: 'common.operation.selectAll' })
describe('Actions', () => {
// Default mock for required props
const defaultProps = {
@ -166,8 +169,9 @@ describe('Actions', () => {
expect(handleNextStep).not.toHaveBeenCalled()
})
it('should call onSelectAll when checkbox is clicked', () => {
it('should call onSelectAll when checkbox is clicked', async () => {
const onSelectAll = vi.fn()
const user = userEvent.setup()
render(
<Actions
{...defaultProps}
@ -178,14 +182,10 @@ describe('Actions', () => {
/>,
)
// Act - find the checkbox container and click it
const selectAllLabel = screen.getByText('common.operation.selectAll')
const checkboxContainer = selectAllLabel.closest('.flex.shrink-0.items-center')
const checkbox = checkboxContainer?.querySelector('[class*="cursor-pointer"]')
if (checkbox)
fireEvent.click(checkbox)
await user.click(screen.getByText('common.operation.selectAll'))
expect(onSelectAll).toHaveBeenCalledTimes(1)
expect(onSelectAll).toHaveBeenCalledWith(true)
})
})
@ -209,7 +209,7 @@ describe('Actions', () => {
})
it('should return false when selectedOptions is undefined', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -220,12 +220,11 @@ describe('Actions', () => {
)
// Assert - checkbox should not be indeterminate
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should return false when totalOptions is undefined', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -236,12 +235,11 @@ describe('Actions', () => {
)
// Assert - checkbox should exist
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should return true when some options are selected (0 < selectedOptions < totalOptions)', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -252,13 +250,11 @@ describe('Actions', () => {
)
// Assert - checkbox should render in indeterminate state
// The checkbox component renders IndeterminateIcon when indeterminate and not checked
const selectAllContainer = container.querySelector('.flex.shrink-0.items-center')
expect(selectAllContainer).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'mixed')
})
it('should return false when no options are selected (selectedOptions === 0)', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -269,12 +265,11 @@ describe('Actions', () => {
)
// Assert - checkbox should be unchecked and not indeterminate
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should return false when all options are selected (selectedOptions === totalOptions)', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -285,8 +280,7 @@ describe('Actions', () => {
)
// Assert - checkbox should be checked, not indeterminate
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'true')
})
})
@ -307,7 +301,7 @@ describe('Actions', () => {
})
it('should return false when selectedOptions is undefined', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -317,12 +311,11 @@ describe('Actions', () => {
/>,
)
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should return false when totalOptions is undefined', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -332,12 +325,11 @@ describe('Actions', () => {
/>,
)
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should return true when all options are selected (selectedOptions === totalOptions)', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -348,12 +340,11 @@ describe('Actions', () => {
)
// Assert - checkbox should show checked state (RiCheckLine icon)
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'true')
})
it('should return false when selectedOptions is 0', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -364,12 +355,11 @@ describe('Actions', () => {
)
// Assert - checkbox should be unchecked
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should return false when not all options are selected', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -380,8 +370,7 @@ describe('Actions', () => {
)
// Assert - checkbox should be indeterminate, not checked
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'mixed')
})
})
})
@ -443,7 +432,7 @@ describe('Actions', () => {
describe('Edge Cases', () => {
// Tests for boundary conditions and unusual inputs
it('should handle totalOptions of 0', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -454,12 +443,11 @@ describe('Actions', () => {
)
// Assert - should render checkbox
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should handle very large totalOptions', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -469,8 +457,7 @@ describe('Actions', () => {
/>,
)
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'mixed')
})
it('should handle very long tip text', () => {
@ -523,7 +510,7 @@ describe('Actions', () => {
it('should handle selectedOptions greater than totalOptions', () => {
// This is an edge case that shouldn't happen but should be handled gracefully
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -534,12 +521,11 @@ describe('Actions', () => {
)
// Assert - should still render
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should handle negative selectedOptions', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -550,12 +536,12 @@ describe('Actions', () => {
)
// Assert - should still render (though this is an invalid state)
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should handle onSelectAll being undefined when showSelect is true', () => {
const { container } = render(
it('should handle onSelectAll being undefined when showSelect is true', async () => {
const user = userEvent.setup()
render(
<Actions
{...defaultProps}
showSelect={true}
@ -566,11 +552,8 @@ describe('Actions', () => {
)
// Assert - should render checkbox
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
if (checkbox)
expect(() => fireEvent.click(checkbox)).not.toThrow()
expect(getSelectAllCheckbox()).toBeInTheDocument()
await expect(user.click(screen.getByText('common.operation.selectAll'))).resolves.toBeUndefined()
})
it('should handle empty datasetId from params', () => {
@ -654,8 +637,8 @@ describe('Actions', () => {
it.each(selectionStates)(
'should render with $expectedState state when totalOptions=$totalOptions and selectedOptions=$selectedOptions',
({ totalOptions, selectedOptions }) => {
const { container } = render(
({ totalOptions, selectedOptions, expectedState }) => {
render(
<Actions
{...defaultProps}
showSelect={true}
@ -666,8 +649,10 @@ describe('Actions', () => {
)
// Assert - component should render without errors
const checkbox = container.querySelector('[class*="cursor-pointer"]')
expect(checkbox).toBeInTheDocument()
const expectedAriaChecked = expectedState === 'indeterminate'
? 'mixed'
: expectedState === 'checked' ? 'true' : 'false'
expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', expectedAriaChecked)
expect(screen.getByText('common.operation.selectAll')).toBeInTheDocument()
},
)
@ -692,7 +677,7 @@ describe('Actions', () => {
})
it('should position select all section before buttons when showSelect is true', () => {
const { container } = render(
render(
<Actions
{...defaultProps}
showSelect={true}
@ -703,8 +688,7 @@ describe('Actions', () => {
)
// Assert - select all section should exist
const selectAllSection = container.querySelector('.flex.shrink-0.items-center')
expect(selectAllSection).toBeInTheDocument()
expect(getSelectAllCheckbox()).toBeInTheDocument()
})
})
})

View File

@ -1,9 +1,9 @@
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { RiArrowRightLine } from '@remixicon/react'
import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Link from '@/next/link'
import { useParams } from '@/next/navigation'
@ -13,7 +13,7 @@ type ActionsProps = {
showSelect?: boolean
totalOptions?: number
selectedOptions?: number
onSelectAll?: () => void
onSelectAll?: (checked: boolean) => void
tip?: string
}
@ -49,16 +49,16 @@ const Actions = ({
<div className="flex items-center gap-x-2 overflow-hidden">
{showSelect && (
<>
<div className="flex shrink-0 items-center gap-x-2 py-[3px] pr-2 pl-4">
<label className="flex shrink-0 cursor-pointer items-center gap-x-2 py-[3px] pr-2 pl-4">
<Checkbox
onCheck={onSelectAll}
onCheckedChange={checked => onSelectAll?.(checked)}
indeterminate={indeterminate}
checked={checked}
/>
<span className="system-sm-medium text-text-accent">
{t('operation.selectAll', { ns: 'common' })}
</span>
</div>
</label>
{tip && (
<div title={tip} className="max-w-full truncate system-xs-regular text-text-tertiary">
{tip}

View File

@ -9,20 +9,17 @@ vi.mock('@tanstack/react-virtual')
// Note: NotionIcon from @/app/components/base/ is NOT mocked - using real component per testing guidelines
// Helper Functions for Base Components
// Get checkbox element (uses data-testid pattern from base Checkbox component)
const getCheckbox = () => document.querySelector('[data-testid^="checkbox-"]') as HTMLElement
const getAllCheckboxes = () => document.querySelectorAll('[data-testid^="checkbox-"]')
const getCheckbox = (name = 'Test Page') => screen.getByRole('checkbox', { name })
const queryCheckbox = (name = 'Test Page') => screen.queryByRole('checkbox', { name })
const getAllCheckboxes = () => screen.getAllByRole('checkbox')
// Get radio element (uses size-4 rounded-full class pattern from base Radio component)
const getRadio = () => document.querySelector('.size-4.rounded-full') as HTMLElement
const getAllRadios = () => document.querySelectorAll('.size-4.rounded-full')
// Check if checkbox is checked by looking for check icon
const isCheckboxChecked = (checkbox: Element) => checkbox.querySelector('[data-testid^="check-icon-"]') !== null
const isCheckboxChecked = (checkbox: Element) => checkbox.getAttribute('aria-checked') === 'true'
// Check if checkbox is disabled by looking for disabled class
const isCheckboxDisabled = (checkbox: Element) => checkbox.classList.contains('cursor-not-allowed')
const isCheckboxDisabled = (checkbox: Element) => checkbox.hasAttribute('data-disabled') || checkbox.getAttribute('aria-disabled') === 'true'
const createMockPage = (overrides?: Partial<DataSourceNotionPage>): DataSourceNotionPage => ({
page_id: 'page-1',
@ -443,7 +440,7 @@ describe('PageSelector', () => {
render(<PageSelector {...props} />)
expect(getRadio())!.toBeInTheDocument()
expect(getCheckbox()).not.toBeInTheDocument()
expect(queryCheckbox()).not.toBeInTheDocument()
})
it('should use default value true when isMultipleChoice is not provided', () => {
@ -795,7 +792,7 @@ describe('PageSelector', () => {
render(<PageSelector {...props} />)
// Check the root page
fireEvent.click(getCheckbox())
fireEvent.click(getCheckbox('Root Page'))
// Assert - onSelect should be called with the page and its descendants
expect(mockOnSelect).toHaveBeenCalled()
@ -817,7 +814,7 @@ describe('PageSelector', () => {
render(<PageSelector {...props} />)
// Uncheck the root page
fireEvent.click(getCheckbox())
fireEvent.click(getCheckbox('Root Page'))
// Assert - onSelect should be called with empty/reduced set
expect(mockOnSelect).toHaveBeenCalled()
@ -1042,7 +1039,7 @@ describe('PageSelector', () => {
})
render(<PageSelector {...props} />)
fireEvent.click(getCheckbox())
fireEvent.click(getCheckbox('Root Page'))
// Assert - Only the clicked page should be selected (no descendants)
expect(mockOnSelect).toHaveBeenCalled()

View File

@ -189,11 +189,8 @@ describe('FileList', () => {
render(<FileList {...props} />)
// Assert - The checkbox for file-1 should be checked (check icon present)
expect(screen.getByTestId('checkbox-file-1')).toBeInTheDocument()
expect(screen.getByTestId('check-icon-file-1')).toBeInTheDocument()
expect(screen.getByTestId('checkbox-file-2')).toBeInTheDocument()
expect(screen.queryByTestId('check-icon-file-2')).not.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'file1.txt' })).toHaveAttribute('aria-checked', 'true')
expect(screen.getByRole('checkbox', { name: 'file2.txt' })).toHaveAttribute('aria-checked', 'false')
})
})
@ -236,8 +233,7 @@ describe('FileList', () => {
render(<FileList {...props} />)
// Assert - Checkbox component has data-testid="checkbox-{id}"
expect(screen.getByTestId('checkbox-file-1')).toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'file1.txt' })).toBeInTheDocument()
})
it('should render radio buttons when supportBatchUpload is false', () => {
@ -249,7 +245,7 @@ describe('FileList', () => {
// Assert - Radio is rendered as a div with rounded-full class
expect(container.querySelector('.rounded-full')).toBeInTheDocument()
// And checkbox should not be present
expect(screen.queryByTestId('checkbox-file-1')).not.toBeInTheDocument()
expect(screen.queryByRole('checkbox', { name: 'file1.txt' })).not.toBeInTheDocument()
})
})
})

View File

@ -1286,8 +1286,8 @@ describe('Item', () => {
...overrides,
})
// Helper to find custom checkbox element (div-based implementation)
const findCheckbox = (container: HTMLElement) => container.querySelector('[data-testid^="checkbox-"]')
const queryCheckbox = () => screen.queryByRole('checkbox')
const getCheckbox = () => screen.getByRole('checkbox')
const getRadio = () => screen.getByRole('radio')
beforeEach(() => {
@ -1330,8 +1330,8 @@ describe('Item', () => {
isMultipleChoice: true,
file: createMockOnlineDriveFile({ type: OnlineDriveFileType.file }),
})
const { container } = render(<ActualItem {...props} />)
expect(findCheckbox(container)).toBeInTheDocument()
render(<ActualItem {...props} />)
expect(getCheckbox()).toBeInTheDocument()
})
it('should render radio in single choice mode for file', () => {
@ -1348,8 +1348,8 @@ describe('Item', () => {
file: createMockOnlineDriveFile({ type: OnlineDriveFileType.bucket, name: 'my-bucket' }),
isMultipleChoice: true,
})
const { container } = render(<ActualItem {...props} />)
expect(findCheckbox(container)).not.toBeInTheDocument()
render(<ActualItem {...props} />)
expect(queryCheckbox()).not.toBeInTheDocument()
expect(screen.queryByRole('radio')).not.toBeInTheDocument()
})
@ -1366,18 +1366,14 @@ describe('Item', () => {
describe('isSelected prop', () => {
it('should show checkbox as checked when isSelected is true', () => {
const props = createItemProps({ isSelected: true, isMultipleChoice: true })
const { container } = render(<ActualItem {...props} />)
const checkbox = findCheckbox(container)
// Checked checkbox shows check icon
expect(checkbox?.querySelector('[data-testid^="check-icon-"]')).toBeInTheDocument()
render(<ActualItem {...props} />)
expect(getCheckbox()).toHaveAttribute('aria-checked', 'true')
})
it('should show checkbox as unchecked when isSelected is false', () => {
const props = createItemProps({ isSelected: false, isMultipleChoice: true })
const { container } = render(<ActualItem {...props} />)
const checkbox = findCheckbox(container)
// Unchecked checkbox has no check icon
expect(checkbox?.querySelector('[data-testid^="check-icon-"]')).not.toBeInTheDocument()
render(<ActualItem {...props} />)
expect(getCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should show radio as checked when isSelected is true', () => {
@ -1392,9 +1388,8 @@ describe('Item', () => {
it('should not call onSelect when clicking disabled checkbox', () => {
const onSelect = vi.fn()
const props = createItemProps({ disabled: true, isMultipleChoice: true, onSelect })
const { container } = render(<ActualItem {...props} />)
const checkbox = findCheckbox(container)
fireEvent.click(checkbox!)
render(<ActualItem {...props} />)
fireEvent.click(getCheckbox())
expect(onSelect).not.toHaveBeenCalled()
})
@ -1412,22 +1407,22 @@ describe('Item', () => {
it('should default to true', () => {
const props = createItemProps()
delete (props as Partial<ItemProps>).isMultipleChoice
const { container } = render(<ActualItem {...props} />)
expect(findCheckbox(container)).toBeInTheDocument()
render(<ActualItem {...props} />)
expect(getCheckbox()).toBeInTheDocument()
})
it('should render checkbox when true', () => {
const props = createItemProps({ isMultipleChoice: true })
const { container } = render(<ActualItem {...props} />)
expect(findCheckbox(container)).toBeInTheDocument()
render(<ActualItem {...props} />)
expect(getCheckbox()).toBeInTheDocument()
expect(screen.queryByRole('radio')).not.toBeInTheDocument()
})
it('should render radio when false', () => {
const props = createItemProps({ isMultipleChoice: false })
const { container } = render(<ActualItem {...props} />)
render(<ActualItem {...props} />)
expect(getRadio()).toBeInTheDocument()
expect(findCheckbox(container)).not.toBeInTheDocument()
expect(queryCheckbox()).not.toBeInTheDocument()
})
})
})
@ -1477,9 +1472,8 @@ describe('Item', () => {
const onSelect = vi.fn()
const file = createMockOnlineDriveFile()
const props = createItemProps({ file, onSelect, isMultipleChoice: true })
const { container } = render(<ActualItem {...props} />)
const checkbox = findCheckbox(container)
fireEvent.click(checkbox!)
render(<ActualItem {...props} />)
fireEvent.click(getCheckbox())
expect(onSelect).toHaveBeenCalledWith(file)
})
@ -1497,9 +1491,8 @@ describe('Item', () => {
const onSelect = vi.fn()
const file = createMockOnlineDriveFile()
const props = createItemProps({ file, onSelect, isMultipleChoice: true })
const { container } = render(<ActualItem {...props} />)
const checkbox = findCheckbox(container)
fireEvent.click(checkbox!)
render(<ActualItem {...props} />)
fireEvent.click(getCheckbox())
expect(onSelect).toHaveBeenCalledTimes(1)
})
})

View File

@ -3,12 +3,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Item from '../item'
vi.mock('@/app/components/base/checkbox', () => ({
default: ({ checked, onCheck, disabled }: { checked: boolean, onCheck: () => void, disabled?: boolean }) => (
<input type="checkbox" data-testid="checkbox" checked={checked} onChange={onCheck} disabled={disabled} />
),
}))
vi.mock('@/app/components/base/radio/ui', () => ({
default: ({ isChecked, onCheck }: { isChecked: boolean, onCheck: () => void }) => (
<input type="radio" data-testid="radio" checked={isChecked} onChange={onCheck} />
@ -45,7 +39,7 @@ describe('Item', () => {
it('should render checkbox for file type in multiple choice mode', () => {
render(<Item {...defaultProps} />)
expect(screen.getByTestId('checkbox')).toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'test.pdf' })).toBeInTheDocument()
})
it('should render radio for file type in single choice mode', () => {
@ -55,7 +49,7 @@ describe('Item', () => {
it('should not render checkbox for bucket type', () => {
render(<Item {...defaultProps} file={makeFile('bucket', 'my-bucket')} />)
expect(screen.queryByTestId('checkbox')).not.toBeInTheDocument()
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
})
it('should call onOpen for folder click', () => {
@ -71,6 +65,16 @@ describe('Item', () => {
expect(defaultProps.onSelect).toHaveBeenCalledWith(defaultProps.file)
})
it('should call onSelect once without bubbling to row click when checkbox is clicked', () => {
render(<Item {...defaultProps} />)
fireEvent.click(screen.getByRole('checkbox', { name: 'test.pdf' }))
expect(defaultProps.onSelect).toHaveBeenCalledTimes(1)
expect(defaultProps.onSelect).toHaveBeenCalledWith(defaultProps.file)
expect(defaultProps.onOpen).not.toHaveBeenCalled()
})
it('should not call handlers when disabled', () => {
render(<Item {...defaultProps} disabled={true} />)
fireEvent.click(screen.getByText('test.pdf'))

View File

@ -1,9 +1,9 @@
import type { OnlineDriveFile } from '@/models/pipeline'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Radio from '@/app/components/base/radio/ui'
import { formatFileSize } from '@/utils/format'
import FileIcon from './file-icon'
@ -26,7 +26,7 @@ const Item = ({
onOpen,
}: ItemProps) => {
const { t } = useTranslation()
const { id, name, type, size } = file
const { name, type, size } = file
const isBucket = type === 'bucket'
const isFolder = type === 'folder'
@ -38,6 +38,10 @@ const Item = ({
onSelect(file)
}, [file, onSelect])
const handleCheckboxSelect = useCallback(() => {
onSelect(file)
}, [file, onSelect])
const handleClickItem = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (disabled)
@ -55,13 +59,15 @@ const Item = ({
onClick={handleClickItem}
>
{!isBucket && isMultipleChoice && (
<Checkbox
className="shrink-0"
disabled={disabled}
id={id}
checked={isSelected}
onCheck={handleSelect}
/>
<span onClick={event => event.stopPropagation()}>
<Checkbox
className="shrink-0"
disabled={disabled}
checked={isSelected}
aria-label={name}
onCheckedChange={() => handleCheckboxSelect()}
/>
</span>
)}
{!isBucket && !isMultipleChoice && (
<Radio

View File

@ -2,12 +2,6 @@ import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CheckboxWithLabel from '../checkbox-with-label'
vi.mock('@/app/components/base/checkbox', () => ({
default: ({ checked, onCheck }: { checked: boolean, onCheck: () => void }) => (
<input type="checkbox" data-testid="checkbox" checked={checked} onChange={onCheck} />
),
}))
describe('CheckboxWithLabel', () => {
const defaultProps = {
isChecked: false,
@ -26,7 +20,7 @@ describe('CheckboxWithLabel', () => {
it('should render checkbox', () => {
render(<CheckboxWithLabel {...defaultProps} />)
expect(screen.getByTestId('checkbox')).toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'Test Label' })).toBeInTheDocument()
})
it('should render tooltip when provided', () => {

View File

@ -9,12 +9,6 @@ vi.mock('@langgenius/dify-ui/button', () => ({
),
}))
vi.mock('@/app/components/base/checkbox', () => ({
default: ({ checked, onCheck }: { checked: boolean, onCheck: () => void }) => (
<input type="checkbox" data-testid="checkbox" checked={checked} onChange={onCheck} />
),
}))
vi.mock('@/app/components/base/radio/ui', () => ({
default: ({ isChecked, onCheck }: { isChecked: boolean, onCheck: () => void }) => (
<input type="radio" data-testid="radio" checked={isChecked} onChange={onCheck} />
@ -49,7 +43,7 @@ describe('CrawledResultItem', () => {
it('should render checkbox in multiple choice mode', () => {
render(<CrawledResultItem {...defaultProps} />)
expect(screen.getByTestId('checkbox')).toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /Test Page/ })).toBeInTheDocument()
})
it('should render radio in single choice mode', () => {

View File

@ -1,5 +1,6 @@
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import CheckboxWithLabel from '../checkbox-with-label'
import CrawledResult from '../crawled-result'
@ -43,20 +44,15 @@ describe('CheckboxWithLabel', () => {
})
it('should render checkbox in unchecked state', () => {
const { container } = render(<CheckboxWithLabel {...defaultProps} isChecked={false} />)
render(<CheckboxWithLabel {...defaultProps} isChecked={false} />)
// Assert - Custom checkbox component uses div with data-testid
const checkbox = container.querySelector('[data-testid^="checkbox"]')
expect(checkbox)!.toBeInTheDocument()
expect(checkbox).not.toHaveClass('bg-components-checkbox-bg')
expect(screen.getByRole('checkbox', { name: 'Test Label' })).toHaveAttribute('aria-checked', 'false')
})
it('should render checkbox in checked state', () => {
const { container } = render(<CheckboxWithLabel {...defaultProps} isChecked={true} />)
render(<CheckboxWithLabel {...defaultProps} isChecked={true} />)
// Assert - Checked state has check icon
const checkIcon = container.querySelector('[data-testid^="check-icon"]')
expect(checkIcon)!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'Test Label' })).toHaveAttribute('aria-checked', 'true')
})
it('should render tooltip when provided', () => {
@ -90,32 +86,32 @@ describe('CheckboxWithLabel', () => {
})
describe('User Interactions', () => {
it('should call onChange with true when clicking unchecked checkbox', () => {
it('should call onChange with true when clicking unchecked checkbox', async () => {
const mockOnChange = vi.fn()
const { container } = render(<CheckboxWithLabel {...defaultProps} isChecked={false} onChange={mockOnChange} />)
const user = userEvent.setup()
render(<CheckboxWithLabel {...defaultProps} isChecked={false} onChange={mockOnChange} />)
const checkbox = container.querySelector('[data-testid^="checkbox"]')!
fireEvent.click(checkbox)
await user.click(screen.getByText('Test Label'))
expect(mockOnChange).toHaveBeenCalledWith(true)
})
it('should call onChange with false when clicking checked checkbox', () => {
it('should call onChange with false when clicking checked checkbox', async () => {
const mockOnChange = vi.fn()
const { container } = render(<CheckboxWithLabel {...defaultProps} isChecked={true} onChange={mockOnChange} />)
const user = userEvent.setup()
render(<CheckboxWithLabel {...defaultProps} isChecked={true} onChange={mockOnChange} />)
const checkbox = container.querySelector('[data-testid^="checkbox"]')!
fireEvent.click(checkbox)
await user.click(screen.getByText('Test Label'))
expect(mockOnChange).toHaveBeenCalledWith(false)
})
it('should trigger onChange when clicking label text', () => {
it('should trigger onChange when clicking label text', async () => {
const mockOnChange = vi.fn()
const user = userEvent.setup()
render(<CheckboxWithLabel {...defaultProps} onChange={mockOnChange} />)
const labelText = screen.getByText('Test Label')
fireEvent.click(labelText)
await user.click(screen.getByText('Test Label'))
expect(mockOnChange).toHaveBeenCalledWith(true)
})
@ -146,11 +142,9 @@ describe('CrawledResultItem', () => {
})
it('should render checkbox when isMultipleChoice is true', () => {
const { container } = render(<CrawledResultItem {...defaultProps} isMultipleChoice={true} />)
render(<CrawledResultItem {...defaultProps} isMultipleChoice={true} />)
// Assert - Custom checkbox uses data-testid
const checkbox = container.querySelector('[data-testid^="checkbox"]')
expect(checkbox)!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /Test Page Title/ })).toBeInTheDocument()
})
it('should render radio when isMultipleChoice is false', () => {
@ -162,11 +156,9 @@ describe('CrawledResultItem', () => {
})
it('should render checkbox as checked when isChecked is true', () => {
const { container } = render(<CrawledResultItem {...defaultProps} isChecked={true} />)
render(<CrawledResultItem {...defaultProps} isChecked={true} />)
// Assert - Checked state shows check icon
const checkIcon = container.querySelector('[data-testid^="check-icon"]')
expect(checkIcon)!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: /Test Page Title/ })).toHaveAttribute('aria-checked', 'true')
})
it('should render preview button when showPreview is true', () => {
@ -225,9 +217,10 @@ describe('CrawledResultItem', () => {
})
describe('User Interactions', () => {
it('should call onCheckChange with true when clicking unchecked checkbox', () => {
it('should call onCheckChange with true when clicking unchecked checkbox', async () => {
const mockOnCheckChange = vi.fn()
const { container } = render(
const user = userEvent.setup()
render(
<CrawledResultItem
{...defaultProps}
isChecked={false}
@ -235,15 +228,15 @@ describe('CrawledResultItem', () => {
/>,
)
const checkbox = container.querySelector('[data-testid^="checkbox"]')!
fireEvent.click(checkbox)
await user.click(screen.getByText('Test Page Title'))
expect(mockOnCheckChange).toHaveBeenCalledWith(true)
})
it('should call onCheckChange with false when clicking checked checkbox', () => {
it('should call onCheckChange with false when clicking checked checkbox', async () => {
const mockOnCheckChange = vi.fn()
const { container } = render(
const user = userEvent.setup()
render(
<CrawledResultItem
{...defaultProps}
isChecked={true}
@ -251,8 +244,7 @@ describe('CrawledResultItem', () => {
/>,
)
const checkbox = container.querySelector('[data-testid^="checkbox"]')!
fireEvent.click(checkbox)
await user.click(screen.getByText('Test Page Title'))
expect(mockOnCheckChange).toHaveBeenCalledWith(false)
})
@ -325,19 +317,16 @@ describe('CrawledResult', () => {
})
it('should render select all checkbox when isMultipleChoice is true', () => {
const { container } = render(<CrawledResult {...defaultProps} isMultipleChoice={true} />)
render(<CrawledResult {...defaultProps} isMultipleChoice={true} />)
// Assert - Multiple custom checkboxes (select all + items)
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
expect(checkboxes.length).toBe(4) // 1 select all + 3 items
expect(screen.getAllByRole('checkbox')).toHaveLength(4)
})
it('should not render select all checkbox when isMultipleChoice is false', () => {
const { container } = render(<CrawledResult {...defaultProps} isMultipleChoice={false} />)
// Assert - No select all checkbox, only radio buttons for items
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
expect(checkboxes.length).toBe(0)
expect(screen.queryAllByRole('checkbox')).toHaveLength(0)
// Radio buttons have size-4 and rounded-full classes
const radios = container.querySelectorAll('.size-4.rounded-full')
expect(radios.length).toBe(3)
@ -368,13 +357,12 @@ describe('CrawledResult', () => {
})
it('should highlight item at previewIndex', () => {
const { container } = render(
render(
<CrawledResult {...defaultProps} previewIndex={1} />,
)
// Assert - Second item should have active state
const items = container.querySelectorAll('[class*="rounded-lg"][class*="cursor-pointer"]')
expect(items[1])!.toHaveClass('bg-state-base-active')
expect(screen.getByText('Page 2').closest('.relative')).toHaveClass('bg-state-base-active')
})
it('should pass showPreview to items', () => {
@ -392,10 +380,11 @@ describe('CrawledResult', () => {
})
describe('User Interactions', () => {
it('should call onSelectedChange with all items when clicking select all', () => {
it('should call onSelectedChange with all items when clicking select all', async () => {
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
const user = userEvent.setup()
render(
<CrawledResult
{...defaultProps}
list={list}
@ -404,17 +393,16 @@ describe('CrawledResult', () => {
/>,
)
// Act - Click select all checkbox (first checkbox)
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
fireEvent.click(checkboxes[0]!)
await user.click(screen.getByText(/selectAll/i))
expect(mockOnSelectedChange).toHaveBeenCalledWith(list)
})
it('should call onSelectedChange with empty array when clicking reset all', () => {
it('should call onSelectedChange with empty array when clicking reset all', async () => {
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
const user = userEvent.setup()
render(
<CrawledResult
{...defaultProps}
list={list}
@ -423,16 +411,16 @@ describe('CrawledResult', () => {
/>,
)
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
fireEvent.click(checkboxes[0]!)
await user.click(screen.getByText(/resetAll/i))
expect(mockOnSelectedChange).toHaveBeenCalledWith([])
})
it('should add item to checkedList when checking unchecked item', () => {
it('should add item to checkedList when checking unchecked item', async () => {
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
const user = userEvent.setup()
render(
<CrawledResult
{...defaultProps}
list={list}
@ -441,17 +429,16 @@ describe('CrawledResult', () => {
/>,
)
// Act - Click second item checkbox (index 2, accounting for select all)
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
fireEvent.click(checkboxes[2]!)
await user.click(screen.getByText('Page 2'))
expect(mockOnSelectedChange).toHaveBeenCalledWith([list[0], list[1]])
})
it('should remove item from checkedList when unchecking checked item', () => {
it('should remove item from checkedList when unchecking checked item', async () => {
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
const user = userEvent.setup()
render(
<CrawledResult
{...defaultProps}
list={list}
@ -460,9 +447,7 @@ describe('CrawledResult', () => {
/>,
)
// Act - Uncheck first item (index 1, after select all)
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
fireEvent.click(checkboxes[1]!)
await user.click(screen.getByText('Page 1'))
expect(mockOnSelectedChange).toHaveBeenCalledWith([list[1]])
})
@ -751,7 +736,7 @@ describe('Base Components Integration', () => {
it('should render CrawledResult with CheckboxWithLabel for select all', () => {
const list = createMockCrawlResultItems(2)
const { container } = render(
render(
<CrawledResult
list={list}
checkedList={[]}
@ -762,16 +747,16 @@ describe('Base Components Integration', () => {
)
// Assert - Should have select all checkbox + item checkboxes
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
expect(checkboxes.length).toBe(3) // select all + 2 items
expect(screen.getAllByRole('checkbox')).toHaveLength(3)
})
it('should allow selecting and previewing items', () => {
it('should allow selecting and previewing items', async () => {
const list = createMockCrawlResultItems(3)
const mockOnSelectedChange = vi.fn()
const mockOnPreview = vi.fn()
const user = userEvent.setup()
const { container } = render(
render(
<CrawledResult
list={list}
checkedList={[]}
@ -782,9 +767,7 @@ describe('Base Components Integration', () => {
/>,
)
// Act - Select first item (index 1, after select all)
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
fireEvent.click(checkboxes[1]!)
await user.click(screen.getByText('Page 1'))
expect(mockOnSelectedChange).toHaveBeenCalledWith([list[0]])

View File

@ -1,8 +1,6 @@
'use client'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react'
import { useId } from 'react'
import Checkbox from '@/app/components/base/checkbox'
import { Infotip } from '@/app/components/base/infotip'
type CheckboxWithLabelProps = {
@ -14,29 +12,23 @@ type CheckboxWithLabelProps = {
tooltip?: string
}
const CheckboxWithLabel = ({
export default function CheckboxWithLabel({
className = '',
isChecked,
onChange,
label,
labelClassName,
tooltip,
}: CheckboxWithLabelProps) => {
const labelId = useId()
const handleToggle = () => onChange(!isChecked)
}: CheckboxWithLabelProps) {
return (
<div className={cn('flex items-center', className)}>
<Checkbox checked={isChecked} onCheck={handleToggle} ariaLabelledBy={labelId} />
<div className="ml-2 flex min-w-0 items-center gap-1">
<button
type="button"
id={labelId}
className={cn('min-w-0 cursor-pointer border-0 bg-transparent p-0 text-left system-sm-medium text-text-secondary', labelClassName)}
onClick={handleToggle}
>
<label className="flex min-w-0 cursor-pointer items-center">
<Checkbox checked={isChecked} onCheckedChange={checked => onChange(checked)} />
<span className={cn('ml-2 min-w-0 text-left system-sm-medium text-text-secondary', labelClassName)}>
{label}
</button>
</span>
</label>
<div className="ml-1 flex min-w-0 items-center">
{tooltip && (
<Infotip aria-label={tooltip} popupClassName="w-[200px]">
{tooltip}
@ -46,4 +38,3 @@ const CheckboxWithLabel = ({
</div>
)
}
export default React.memo(CheckboxWithLabel)

View File

@ -1,11 +1,11 @@
'use client'
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Radio from '@/app/components/base/radio/ui'
type CrawledResultItemProps = {
@ -35,41 +35,59 @@ const CrawledResultItem = ({
return (
<div className={cn(
'relative flex cursor-pointer gap-x-2 rounded-lg p-2',
'relative flex gap-x-2 rounded-lg p-2',
isPreview ? 'bg-state-base-active' : 'group hover:bg-state-base-hover',
)}
>
{
isMultipleChoice
? (
<Checkbox
className="shrink-0"
checked={isChecked}
onCheck={handleCheckChange}
/>
<label className="flex min-w-0 grow cursor-pointer gap-x-2">
<Checkbox
className="shrink-0"
checked={isChecked}
onCheckedChange={checked => onCheckChange(checked)}
/>
<div className="flex min-w-0 grow flex-col gap-y-0.5">
<div
className="truncate system-sm-medium text-text-secondary"
title={payload.title}
>
{payload.title}
</div>
<div
className="truncate system-xs-regular text-text-tertiary"
title={payload.source_url}
>
{payload.source_url}
</div>
</div>
</label>
)
: (
<Radio
className="shrink-0"
isChecked={isChecked}
onCheck={handleCheckChange}
/>
<>
<Radio
className="shrink-0"
isChecked={isChecked}
onCheck={handleCheckChange}
/>
<div className="flex min-w-0 grow flex-col gap-y-0.5">
<div
className="truncate system-sm-medium text-text-secondary"
title={payload.title}
>
{payload.title}
</div>
<div
className="truncate system-xs-regular text-text-tertiary"
title={payload.source_url}
>
{payload.source_url}
</div>
</div>
</>
)
}
<div className="flex min-w-0 grow flex-col gap-y-0.5">
<div
className="truncate system-sm-medium text-text-secondary"
title={payload.title}
>
{payload.title}
</div>
<div
className="truncate system-xs-regular text-text-tertiary"
title={payload.source_url}
>
{payload.source_url}
</div>
</div>
{showPreview && (
<Button
size="small"

View File

@ -146,13 +146,13 @@ describe('useDatasourceActions', () => {
// First call: select all
act(() => {
result.current.handleSelectAll()
result.current.handleSelectAll(true)
})
expect(store.getState().onlineDocuments).toHaveLength(2)
// Second call: deselect all
act(() => {
result.current.handleSelectAll()
result.current.handleSelectAll(false)
})
expect(store.getState().onlineDocuments).toEqual([])
})
@ -170,7 +170,7 @@ describe('useDatasourceActions', () => {
const { result } = renderHook(() => useDatasourceActions(params))
act(() => {
result.current.handleSelectAll()
result.current.handleSelectAll(true)
})
// Should select f1, f2 but not b1 (bucket)
expect(store.getState().selectedFileIds).toEqual(['f1', 'f2'])

View File

@ -238,11 +238,9 @@ export const useDatasourceActions = ({
}, [dataSourceStore, onClickPreview])
// Select all handler
const handleSelectAll = useCallback(() => {
const handleSelectAll = useCallback((checked: boolean) => {
const {
onlineDocuments,
onlineDriveFileList,
selectedFileIds,
setOnlineDocuments,
setSelectedFileIds,
setSelectedPagesId,
@ -250,7 +248,7 @@ export const useDatasourceActions = ({
if (datasourceType === DatasourceType.onlineDocument) {
const allIds = currentWorkspacePages?.map(page => page.page_id) || []
if (onlineDocuments.length < allIds.length) {
if (checked) {
const selectedPages = Array.from(allIds).map(pageId => PagesMapAndSelectedPagesId[pageId]!)
setOnlineDocuments(selectedPages)
setSelectedPagesId(new Set(allIds))
@ -263,7 +261,7 @@ export const useDatasourceActions = ({
if (datasourceType === DatasourceType.onlineDrive) {
const allKeys = onlineDriveFileList.filter(item => item.type !== 'bucket').map(file => file.id)
if (selectedFileIds.length < allKeys.length)
if (checked)
setSelectedFileIds(allKeys)
else
setSelectedFileIds([])

View File

@ -28,7 +28,7 @@ type StepOneContentProps = {
nextBtnDisabled: boolean
onSelectDataSource: (dataSource: Datasource) => void
onCredentialChange: (credentialId: string) => void
onSelectAll: () => void
onSelectAll: (checked: boolean) => void
onNextStep: () => void
}

View File

@ -58,15 +58,15 @@ vi.mock('../completed/common/action-buttons', () => ({
}))
vi.mock('../completed/common/add-another', () => ({
default: ({ isChecked, onCheck, className }: { isChecked: boolean, onCheck: () => void, className?: string }) => (
<div data-testid="add-another" className={className}>
default: ({ checked, onCheckedChange, className }: { checked: boolean, onCheckedChange: (checked: boolean) => void, className?: string }) => (
<label className={className}>
<input
type="checkbox"
checked={isChecked}
onChange={onCheck}
data-testid="add-another-checkbox"
checked={checked}
onChange={event => onCheckedChange(event.currentTarget.checked)}
/>
</div>
datasetDocuments.segment.addAnother
</label>
),
}))
@ -228,12 +228,10 @@ describe('NewSegmentModal', () => {
it('should toggle add another checkbox', () => {
render(<NewSegmentModal {...defaultProps} />)
const checkbox = screen.getByTestId('add-another-checkbox')
const checkbox = screen.getByRole('checkbox', { name: 'datasetDocuments.segment.addAnother' })
fireEvent.click(checkbox)
// Assert - checkbox state should toggle
// Assert - checkbox state should toggle
expect(checkbox)!.toBeInTheDocument()
})
})
@ -342,7 +340,7 @@ describe('NewSegmentModal', () => {
render(<NewSegmentModal {...defaultProps} />)
expect(screen.getByTestId('add-another'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'datasetDocuments.segment.addAnother' }))!.toBeInTheDocument()
})
it('should call toggleFullScreen when expand button is clicked', () => {
@ -541,8 +539,7 @@ describe('NewSegmentModal', () => {
render(<NewSegmentModal {...defaultProps} onCancel={mockOnCancel} docForm={ChunkingMode.text} />)
// Uncheck "add another"
const checkbox = screen.getByTestId('add-another-checkbox')
const checkbox = screen.getByRole('checkbox', { name: 'datasetDocuments.segment.addAnother' })
fireEvent.click(checkbox)
// Enter content and save
@ -598,9 +595,7 @@ describe('NewSegmentModal', () => {
render(<NewSegmentModal {...defaultProps} />)
// Assert - footer should have both AddAnother and ActionButtons
// Assert - footer should have both AddAnother and ActionButtons
expect(screen.getByTestId('add-another'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'datasetDocuments.segment.addAnother' }))!.toBeInTheDocument()
expect(screen.getByTestId('action-buttons'))!.toBeInTheDocument()
})
})

View File

@ -57,15 +57,15 @@ vi.mock('../common/action-buttons', () => ({
}))
vi.mock('../common/add-another', () => ({
default: ({ isChecked, onCheck, className }: { isChecked: boolean, onCheck: () => void, className?: string }) => (
<div data-testid="add-another" className={className}>
default: ({ checked, onCheckedChange, className }: { checked: boolean, onCheckedChange: (checked: boolean) => void, className?: string }) => (
<label className={className}>
<input
type="checkbox"
checked={isChecked}
onChange={onCheck}
data-testid="add-another-checkbox"
checked={checked}
onChange={event => onCheckedChange(event.currentTarget.checked)}
/>
</div>
datasetDocuments.segment.addAnother
</label>
),
}))
@ -134,7 +134,7 @@ describe('NewChildSegmentModal', () => {
it('should render add another checkbox', () => {
render(<NewChildSegmentModal {...defaultProps} />)
expect(screen.getByTestId('add-another'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'datasetDocuments.segment.addAnother' }))!.toBeInTheDocument()
})
})
@ -170,7 +170,7 @@ describe('NewChildSegmentModal', () => {
it('should toggle add another checkbox', () => {
render(<NewChildSegmentModal {...defaultProps} />)
const checkbox = screen.getByTestId('add-another-checkbox')
const checkbox = screen.getByRole('checkbox', { name: 'datasetDocuments.segment.addAnother' })
fireEvent.click(checkbox)
@ -257,7 +257,7 @@ describe('NewChildSegmentModal', () => {
render(<NewChildSegmentModal {...defaultProps} />)
expect(screen.getByTestId('add-another'))!.toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'datasetDocuments.segment.addAnother' }))!.toBeInTheDocument()
})
})
@ -312,8 +312,7 @@ describe('NewChildSegmentModal', () => {
render(<NewChildSegmentModal {...defaultProps} onCancel={mockOnCancel} />)
// Uncheck add another
fireEvent.click(screen.getByTestId('add-another-checkbox'))
fireEvent.click(screen.getByRole('checkbox', { name: 'datasetDocuments.segment.addAnother' }))
// Enter valid content
fireEvent.change(screen.getByTestId('content-input'), {

View File

@ -284,11 +284,9 @@ describe('SegmentList', () => {
// Checkbox Selection
describe('Checkbox Selection', () => {
it('should render checkbox for each segment', () => {
const { container } = render(<SegmentList {...defaultProps} />)
render(<SegmentList {...defaultProps} />)
// Assert - Checkbox component should exist
const checkboxes = container.querySelectorAll('[class*="checkbox"]')
expect(checkboxes.length).toBeGreaterThan(0)
expect(screen.getAllByRole('checkbox')).toHaveLength(defaultProps.items.length)
})
it('should pass selectedSegmentIds to check state', () => {

View File

@ -1,4 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AddAnother from '../add-another'
@ -7,27 +8,27 @@ describe('AddAnother', () => {
vi.clearAllMocks()
})
const getCheckbox = () => screen.getByRole('checkbox', { name: /segment\.addAnother/i })
describe('Rendering', () => {
it('should render without crashing', () => {
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
<AddAnother checked={false} onCheckedChange={vi.fn()} />,
)
expect(container.firstChild).toBeInTheDocument()
})
it('should render the checkbox', () => {
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
render(
<AddAnother checked={false} onCheckedChange={vi.fn()} />,
)
// Assert - Checkbox component renders with shrink-0 class
const checkbox = container.querySelector('.shrink-0')
expect(checkbox).toBeInTheDocument()
expect(getCheckbox()).toBeInTheDocument()
})
it('should render the add another text', () => {
render(<AddAnother isChecked={false} onCheck={vi.fn()} />)
render(<AddAnother checked={false} onCheckedChange={vi.fn()} />)
// Assert - i18n key format
expect(screen.getByText(/segment\.addAnother/i)).toBeInTheDocument()
@ -35,7 +36,7 @@ describe('AddAnother', () => {
it('should render with correct base styling classes', () => {
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
<AddAnother checked={false} onCheckedChange={vi.fn()} />,
)
const wrapper = container.firstChild as HTMLElement
@ -47,31 +48,27 @@ describe('AddAnother', () => {
})
describe('Props', () => {
it('should render unchecked state when isChecked is false', () => {
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
it('should render unchecked state when checked is false', () => {
render(
<AddAnother checked={false} onCheckedChange={vi.fn()} />,
)
// Assert - unchecked checkbox has border class
const checkbox = container.querySelector('.border-components-checkbox-border')
expect(checkbox).toBeInTheDocument()
expect(getCheckbox()).toHaveAttribute('aria-checked', 'false')
})
it('should render checked state when isChecked is true', () => {
const { container } = render(
<AddAnother isChecked={true} onCheck={vi.fn()} />,
it('should render checked state when checked is true', () => {
render(
<AddAnother checked={true} onCheckedChange={vi.fn()} />,
)
// Assert - checked checkbox has bg-components-checkbox-bg class
const checkbox = container.querySelector('.bg-components-checkbox-bg')
expect(checkbox).toBeInTheDocument()
expect(getCheckbox()).toHaveAttribute('aria-checked', 'true')
})
it('should apply custom className', () => {
const { container } = render(
<AddAnother
isChecked={false}
onCheck={vi.fn()}
checked={false}
onCheckedChange={vi.fn()}
className="custom-class"
/>,
)
@ -82,42 +79,37 @@ describe('AddAnother', () => {
})
describe('User Interactions', () => {
it('should call onCheck when checkbox is clicked', () => {
const mockOnCheck = vi.fn()
const { container } = render(
<AddAnother isChecked={false} onCheck={mockOnCheck} />,
it('should call mockOnCheckedChange when checkbox is clicked', async () => {
const mockOnCheckedChange = vi.fn()
const user = userEvent.setup()
render(
<AddAnother checked={false} onCheckedChange={mockOnCheckedChange} />,
)
// Act - click on the checkbox element
const checkbox = container.querySelector('.shrink-0')
if (checkbox)
fireEvent.click(checkbox)
await user.click(screen.getByText(/segment\.addAnother/i))
expect(mockOnCheck).toHaveBeenCalledTimes(1)
expect(mockOnCheckedChange).toHaveBeenCalledTimes(1)
})
it('should toggle checked state on multiple clicks', () => {
const mockOnCheck = vi.fn()
const { container, rerender } = render(
<AddAnother isChecked={false} onCheck={mockOnCheck} />,
it('should toggle checked state on multiple clicks', async () => {
const mockOnCheckedChange = vi.fn()
const user = userEvent.setup()
const { rerender } = render(
<AddAnother checked={false} onCheckedChange={mockOnCheckedChange} />,
)
// Act - first click
const checkbox = container.querySelector('.shrink-0')
if (checkbox) {
fireEvent.click(checkbox)
rerender(<AddAnother isChecked={true} onCheck={mockOnCheck} />)
fireEvent.click(checkbox)
}
await user.click(screen.getByText(/segment\.addAnother/i))
rerender(<AddAnother checked={true} onCheckedChange={mockOnCheckedChange} />)
await user.click(screen.getByText(/segment\.addAnother/i))
expect(mockOnCheck).toHaveBeenCalledTimes(2)
expect(mockOnCheckedChange).toHaveBeenCalledTimes(2)
})
})
describe('Structure', () => {
it('should render text with tertiary text color', () => {
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
<AddAnother checked={false} onCheckedChange={vi.fn()} />,
)
const textElement = container.querySelector('.text-text-tertiary')
@ -126,7 +118,7 @@ describe('AddAnother', () => {
it('should render text with xs medium font styling', () => {
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
<AddAnother checked={false} onCheckedChange={vi.fn()} />,
)
const textElement = container.querySelector('.system-xs-medium')
@ -136,30 +128,27 @@ describe('AddAnother', () => {
describe('Edge Cases', () => {
it('should maintain structure when rerendered', () => {
const mockOnCheck = vi.fn()
const { rerender, container } = render(
<AddAnother isChecked={false} onCheck={mockOnCheck} />,
const mockOnCheckedChange = vi.fn()
const { rerender } = render(
<AddAnother checked={false} onCheckedChange={mockOnCheckedChange} />,
)
rerender(<AddAnother isChecked={true} onCheck={mockOnCheck} />)
rerender(<AddAnother checked={true} onCheckedChange={mockOnCheckedChange} />)
const checkbox = container.querySelector('.shrink-0')
expect(checkbox).toBeInTheDocument()
expect(getCheckbox()).toBeInTheDocument()
})
it('should handle rapid state changes', () => {
const mockOnCheck = vi.fn()
const { container } = render(
<AddAnother isChecked={false} onCheck={mockOnCheck} />,
it('should handle rapid state changes', async () => {
const mockOnCheckedChange = vi.fn()
const user = userEvent.setup()
render(
<AddAnother checked={false} onCheckedChange={mockOnCheckedChange} />,
)
const checkbox = container.querySelector('.shrink-0')
if (checkbox) {
for (let i = 0; i < 5; i++)
fireEvent.click(checkbox)
}
for (let i = 0; i < 5; i++)
await user.click(screen.getByText(/segment\.addAnother/i))
expect(mockOnCheck).toHaveBeenCalledTimes(5)
expect(mockOnCheckedChange).toHaveBeenCalledTimes(5)
})
})
})

View File

@ -1,32 +1,32 @@
import type { FC } from 'react'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
type AddAnotherProps = {
className?: string
isChecked: boolean
onCheck: () => void
checked: boolean
onCheckedChange: (checked: boolean) => void
}
const AddAnother: FC<AddAnotherProps> = ({
className,
isChecked,
onCheck,
checked,
onCheckedChange,
}) => {
const { t } = useTranslation()
return (
<div className={cn('flex items-center gap-x-1 pl-1', className)}>
<label className={cn('flex cursor-pointer items-center gap-x-1 pl-1', className)}>
<Checkbox
key="add-another-checkbox"
className="shrink-0"
checked={isChecked}
onCheck={onCheck}
checked={checked}
onCheckedChange={onCheckedChange}
/>
<span className="system-xs-medium text-text-tertiary">{t('segment.addAnother', { ns: 'datasetDocuments' })}</span>
</div>
</label>
)
}

View File

@ -44,9 +44,9 @@ describe('MenuBar', () => {
})
it('should render checkbox', () => {
const { container } = render(<MenuBar {...defaultProps} />)
const checkbox = container.querySelector('[class*="shrink-0"]')
expect(checkbox).toBeInTheDocument()
render(<MenuBar {...defaultProps} />)
expect(screen.getByRole('checkbox', { name: 'common.operation.selectAll' })).toBeInTheDocument()
})
it('should call onInputChange when input changes', () => {

View File

@ -1,8 +1,9 @@
'use client'
import type { FC } from 'react'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import Checkbox from '@/app/components/base/checkbox'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import Input from '@/app/components/base/input'
import DisplayToggle from '../display-toggle'
@ -43,6 +44,7 @@ const MenuBar: FC<MenuBarProps> = ({
isCollapsed,
toggleCollapsed,
}) => {
const { t } = useTranslation()
const selectedStatus = statusList.find(item => item.value === selectDefaultValue) ?? null
return (
@ -51,7 +53,8 @@ const MenuBar: FC<MenuBarProps> = ({
className="shrink-0"
checked={isAllSelected}
indeterminate={!isAllSelected && isSomeSelected}
onCheck={onSelectedAll}
aria-label={t('operation.selectAll', { ns: 'common' })}
onCheckedChange={() => onSelectedAll()}
disabled={isLoading}
/>
<div className="flex-1 pl-5 system-sm-semibold-uppercase text-text-secondary">{totalText}</div>

View File

@ -100,7 +100,7 @@ const NewChildSegmentModal: FC<NewChildSegmentModalProps> = ({
<div className="flex items-center">
{fullScreen && (
<>
<AddAnother className="mr-3" isChecked={addAnother} onCheck={() => setAddAnother(!addAnother)} />
<AddAnother className="mr-3" checked={addAnother} onCheckedChange={setAddAnother} />
<ActionButtons
handleCancel={handleCancel.bind(null, 'esc')}
handleSave={handleSave}
@ -141,7 +141,7 @@ const NewChildSegmentModal: FC<NewChildSegmentModalProps> = ({
</div>
{!fullScreen && (
<div className="flex items-center justify-between border-t border-t-divider-subtle p-4 pt-3">
<AddAnother isChecked={addAnother} onCheck={() => setAddAnother(!addAnother)} />
<AddAnother checked={addAnother} onCheckedChange={setAddAnother} />
<ActionButtons
handleCancel={handleCancel.bind(null, 'esc')}
handleSave={handleSave}

View File

@ -1,7 +1,8 @@
import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import * as React from 'react'
import { useMemo } from 'react'
import Checkbox from '@/app/components/base/checkbox'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { ChunkingMode } from '@/models/datasets'
import { useDocumentContext } from '../context'
@ -47,6 +48,7 @@ const SegmentList = (
ref: React.LegacyRef<HTMLDivElement>
},
) => {
const { t } = useTranslation()
const docForm = useDocumentContext(s => s.docForm)
const parentMode = useDocumentContext(s => s.parentMode)
const currSegment = useSegmentListContext(s => s.currSegment)
@ -83,7 +85,8 @@ const SegmentList = (
key={`${segItem.id}-checkbox`}
className="mt-3.5 shrink-0"
checked={selectedSegmentIds.includes(segItem.id)}
onCheck={() => onSelected(segItem.id)}
aria-label={`${t('segment.chunk', { ns: 'datasetDocuments' })} ${segItem.position}`}
onCheckedChange={() => onSelected(segItem.id)}
/>
<div className="min-w-0 grow">
<SegmentCard

View File

@ -84,12 +84,11 @@ describe('GeneralListSkeleton', () => {
// Checkbox tests
describe('Checkboxes', () => {
it('should render disabled checkboxes', () => {
it('should render checkbox skeleton placeholders', () => {
const { container } = render(<GeneralListSkeleton />)
// Assert - Checkbox component uses cursor-not-allowed class when disabled
const disabledCheckboxes = container.querySelectorAll('.cursor-not-allowed')
expect(disabledCheckboxes.length).toBeGreaterThan(0)
const checkboxSkeletons = container.querySelectorAll('[class*="size-4"][class*="rounded-sm"]')
expect(checkboxSkeletons).toHaveLength(10)
})
it('should render checkboxes with shrink-0 class for consistent sizing', () => {

View File

@ -40,12 +40,11 @@ describe('ParagraphListSkeleton', () => {
// Checkbox tests
describe('Checkboxes', () => {
it('should render disabled checkboxes', () => {
it('should render checkbox skeleton placeholders', () => {
const { container } = render(<ParagraphListSkeleton />)
// Assert - Checkbox component uses cursor-not-allowed class when disabled
const disabledCheckboxes = container.querySelectorAll('.cursor-not-allowed')
expect(disabledCheckboxes.length).toBeGreaterThan(0)
const checkboxSkeletons = container.querySelectorAll('[class*="size-4"][class*="rounded-sm"]')
expect(checkboxSkeletons).toHaveLength(10)
})
it('should render checkboxes with shrink-0 class for consistent sizing', () => {

View File

@ -1,5 +1,5 @@
import { CheckboxSkeleton } from '@langgenius/dify-ui/checkbox'
import * as React from 'react'
import Checkbox from '@/app/components/base/checkbox'
import Divider from '@/app/components/base/divider'
import {
SkeletonContainer,
@ -53,10 +53,9 @@ const GeneralListSkeleton = () => {
{Array.from({ length: 10 }).map((_, index) => {
return (
<div key={index} className="flex items-start gap-x-2">
<Checkbox
<CheckboxSkeleton
key={`${index}-checkbox`}
className="mt-3.5 shrink-0"
disabled
/>
<div className="grow">
<CardSkelton />

View File

@ -1,6 +1,6 @@
import { CheckboxSkeleton } from '@langgenius/dify-ui/checkbox'
import { RiArrowRightSLine } from '@remixicon/react'
import * as React from 'react'
import Checkbox from '@/app/components/base/checkbox'
import Divider from '@/app/components/base/divider'
import {
SkeletonContainer,
@ -55,10 +55,9 @@ const ParagraphListSkeleton = () => {
{Array.from({ length: 10 }).map((_, index) => {
return (
<div key={index} className="flex items-start gap-x-2">
<Checkbox
<CheckboxSkeleton
key={`${index}-checkbox`}
className="mt-3.5 shrink-0"
disabled
/>
<div className="grow">
<CardSkelton />

View File

@ -132,7 +132,7 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
<div className="flex items-center">
{fullScreen && (
<>
<AddAnother className="mr-3" isChecked={addAnother} onCheck={() => setAddAnother(!addAnother)} />
<AddAnother className="mr-3" checked={addAnother} onCheckedChange={setAddAnother} />
<ActionButtons
handleCancel={handleCancel.bind(null, 'esc')}
handleSave={handleSave}
@ -190,7 +190,7 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
</div>
{!fullScreen && (
<div className="flex items-center justify-between border-t border-t-divider-subtle p-4 pt-3">
<AddAnother isChecked={addAnother} onCheck={() => setAddAnother(!addAnother)} />
<AddAnother checked={addAnother} onCheckedChange={setAddAnother} />
<ActionButtons
handleCancel={handleCancel.bind(null, 'esc')}
handleSave={handleSave}

View File

@ -26,10 +26,10 @@ describe('LoadingError', () => {
expect(screen.getByText('plugin.installModal.pluginLoadErrorDesc')).toBeInTheDocument()
})
it('should render disabled checkbox', () => {
it('should render non-interactive checkbox skeleton', () => {
render(<LoadingError />)
expect(screen.getByTestId('checkbox-undefined')).toBeInTheDocument()
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
})
it('should render error icon with close indicator', () => {

View File

@ -15,10 +15,10 @@ describe('Loading', () => {
Loading = mod.default
})
it('should render disabled unchecked checkbox', () => {
it('should render non-interactive checkbox skeleton', () => {
render(<Loading />)
expect(screen.getByTestId('checkbox-undefined')).toBeInTheDocument()
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
})
it('should render placeholder', () => {

View File

@ -1,9 +1,9 @@
'use client'
import type { FC } from 'react'
import { CheckboxSkeleton } from '@langgenius/dify-ui/checkbox'
import { RiCloseLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import { LoadingPlaceholder } from '@/app/components/plugins/card/base/placeholder'
import { Group } from '../../../base/icons/src/vender/other'
@ -11,10 +11,8 @@ const LoadingError: FC = () => {
const { t } = useTranslation()
return (
<div className="flex items-center space-x-2">
<Checkbox
<CheckboxSkeleton
className="shrink-0"
checked={false}
disabled
/>
<div className="hover-bg-components-panel-on-panel-item-bg relative grow rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs">
<div className="flex">

View File

@ -1,15 +1,13 @@
'use client'
import { CheckboxSkeleton } from '@langgenius/dify-ui/checkbox'
import * as React from 'react'
import Checkbox from '@/app/components/base/checkbox'
import Placeholder from '../../card/base/placeholder'
const Loading = () => {
return (
<div className="flex items-center space-x-2">
<Checkbox
<CheckboxSkeleton
className="shrink-0"
checked={false}
disabled
/>
<div className="hover-bg-components-panel-on-panel-item-bg relative grow rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs">
<Placeholder

View File

@ -1053,8 +1053,7 @@ describe('LoadedItem', () => {
vi.clearAllMocks()
})
// Helper to find checkbox element
const getCheckbox = () => screen.getByTestId(/^checkbox/)
const getCheckbox = () => screen.getByRole('checkbox', { name: defaultLoadedItemProps.payload.name })
// ================================
// Rendering Tests
@ -1070,16 +1069,14 @@ describe('LoadedItem', () => {
render(<LoadedItem {...defaultLoadedItemProps} checked={true} />)
expect(getCheckbox()).toBeInTheDocument()
// Check icon should be present when checked
expect(screen.getByTestId(/^check-icon/)).toBeInTheDocument()
expect(getCheckbox()).toHaveAttribute('aria-checked', 'true')
})
it('should render checkbox without check icon when checked prop is false', () => {
render(<LoadedItem {...defaultLoadedItemProps} checked={false} />)
expect(getCheckbox()).toBeInTheDocument()
// Check icon should not be present when unchecked
expect(screen.queryByTestId(/^check-icon/)).not.toBeInTheDocument()
expect(getCheckbox()).toHaveAttribute('aria-checked', 'false')
})
})
@ -1142,8 +1139,7 @@ describe('MarketplaceItem', () => {
vi.clearAllMocks()
})
// Helper to find checkbox element
const getCheckbox = () => screen.getByTestId(/^checkbox/)
const getCheckbox = () => screen.getByRole('checkbox', { name: defaultMarketplaceItemProps.payload.name })
// ================================
// Rendering Tests
@ -1158,9 +1154,7 @@ describe('MarketplaceItem', () => {
it('should render Loading when payload is undefined', () => {
render(<MarketplaceItem {...defaultMarketplaceItemProps} payload={undefined} />)
// Loading component renders a disabled checkbox
const checkbox = screen.getByTestId(/^checkbox/)
expect(checkbox).toHaveClass('cursor-not-allowed')
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
})
})
@ -1177,8 +1171,7 @@ describe('MarketplaceItem', () => {
it('should pass checked state to LoadedItem', () => {
render(<MarketplaceItem {...defaultMarketplaceItemProps} checked={true} />)
// When checked, the check icon should be present
expect(screen.getByTestId(/^check-icon/)).toBeInTheDocument()
expect(getCheckbox()).toHaveAttribute('aria-checked', 'true')
})
})
@ -1222,8 +1215,7 @@ describe('PackageItem', () => {
vi.clearAllMocks()
})
// Helper to find checkbox element
const getCheckbox = () => screen.getByTestId(/^checkbox/)
const getCheckbox = () => screen.getByRole('checkbox', { name: 'Package Plugin' })
// ================================
// Rendering Tests
@ -1243,9 +1235,7 @@ describe('PackageItem', () => {
render(<PackageItem {...defaultPackageItemProps} payload={invalidPayload} />)
// LoadingError renders a disabled checkbox and error text
const checkbox = screen.getByTestId(/^checkbox/)
expect(checkbox).toHaveClass('cursor-not-allowed')
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
expect(screen.getByText('plugin.installModal.pluginLoadError')).toBeInTheDocument()
})
})
@ -1263,8 +1253,7 @@ describe('PackageItem', () => {
it('should pass checked state to LoadedItem', () => {
render(<PackageItem {...defaultPackageItemProps} checked={true} />)
// When checked, the check icon should be present
expect(screen.getByTestId(/^check-icon/)).toBeInTheDocument()
expect(getCheckbox()).toHaveAttribute('aria-checked', 'true')
})
})
@ -1319,9 +1308,7 @@ describe('GithubItem', () => {
mockUseUploadGitHub.mockReturnValue({ data: null, error: null })
render(<GithubItem {...defaultGithubItemProps} />)
// Loading component renders a disabled checkbox
const checkbox = screen.getByTestId(/^checkbox/)
expect(checkbox).toHaveClass('cursor-not-allowed')
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
})
it('should render LoadedItem when data is fetched', async () => {
@ -1352,9 +1339,8 @@ describe('GithubItem', () => {
render(<GithubItem {...defaultGithubItemProps} />)
// When data is loaded, LoadedItem should be rendered with checkbox
await waitFor(() => {
expect(screen.getByTestId(/^checkbox/)).toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'Test Plugin' })).toBeInTheDocument()
})
})
})

View File

@ -4,7 +4,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import LoadedItem from '../loaded-item'
const mockCheckbox = vi.fn()
const mockCard = vi.fn()
const mockVersion = vi.fn()
const mockUsePluginInstallLimit = vi.fn()
@ -14,21 +13,6 @@ vi.mock('@/config', () => ({
MARKETPLACE_API_PREFIX: 'https://marketplace.example.com',
}))
vi.mock('@/app/components/base/checkbox', () => ({
default: (props: { checked: boolean, disabled: boolean, onCheck: () => void }) => {
mockCheckbox(props)
return (
<button
data-testid="checkbox"
disabled={props.disabled}
onClick={props.onCheck}
>
{String(props.checked)}
</button>
)
},
}))
vi.mock('../../../../card', () => ({
default: (props: { titleLeft?: React.ReactNode }) => {
mockCard(props)
@ -117,7 +101,7 @@ describe('LoadedItem', () => {
/>,
)
expect(screen.getByTestId('checkbox')).toBeDisabled()
expect(screen.getByRole('checkbox', { name: 'Loaded Plugin' })).toHaveAttribute('aria-disabled', 'true')
expect(mockCard).toHaveBeenCalledWith(expect.objectContaining({
limitedInstall: true,
payload: expect.objectContaining({
@ -138,7 +122,7 @@ describe('LoadedItem', () => {
/>,
)
fireEvent.click(screen.getByTestId('checkbox'))
fireEvent.click(screen.getByRole('checkbox', { name: 'Loaded Plugin' }))
expect(onCheckedChange).toHaveBeenCalledWith(payload)
})

View File

@ -1,8 +1,8 @@
'use client'
import type { FC } from 'react'
import type { Plugin, VersionProps } from '../../../types'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import * as React from 'react'
import Checkbox from '@/app/components/base/checkbox'
import { MARKETPLACE_API_PREFIX } from '@/config'
import Card from '../../../card'
import useGetIcon from '../../base/use-get-icon'
@ -36,7 +36,8 @@ const LoadedItem: FC<Props> = ({
disabled={!canInstall}
className="shrink-0"
checked={checked}
onCheck={() => onCheckedChange(payload)}
aria-label={payload.name}
onCheckedChange={() => onCheckedChange(payload)}
/>
<Card
className="grow"

View File

@ -3,11 +3,11 @@ import type { FC } from 'react'
import type { Dependency, InstallStatus, InstallStatusResponse, Plugin, VersionInfo } from '../../../types'
import type { ExposeRefs } from './install-multi'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { RiLoader2Line } from '@remixicon/react'
import * as React from 'react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-reference-setting'
import { useMittContextSelector } from '@/context/mitt-context'
import { useInstallOrUpdate, usePluginTaskList } from '@/service/use-plugins'
@ -193,10 +193,10 @@ const Install: FC<Props> = ({
<div className="flex items-center justify-between gap-2 self-stretch p-6 pt-5">
<div className="px-2">
{canInstall && (
<div className="flex items-center gap-x-2" onClick={handleClickSelectAll}>
<Checkbox checked={isSelectAll} indeterminate={isIndeterminate} />
<p className="cursor-pointer system-sm-medium text-text-secondary">{isSelectAll ? t('operation.deSelectAll', { ns: 'common' }) : t('operation.selectAll', { ns: 'common' })}</p>
</div>
<label className="flex cursor-pointer items-center gap-x-2">
<Checkbox checked={isSelectAll} indeterminate={isIndeterminate} onCheckedChange={() => handleClickSelectAll()} />
<span className="system-sm-medium text-text-secondary">{isSelectAll ? t('operation.deSelectAll', { ns: 'common' }) : t('operation.selectAll', { ns: 'common' })}</span>
</label>
)}
</div>
<div className="flex items-center justify-end gap-2 self-stretch">

View File

@ -32,10 +32,6 @@ vi.mock('@/app/components/plugins/hooks', () => ({
}),
}))
vi.mock('@/app/components/base/checkbox', () => ({
default: ({ checked }: { checked: boolean }) => <span data-testid="checkbox">{String(checked)}</span>,
}))
vi.mock('@/app/components/base/input', () => ({
default: ({
value,

View File

@ -1,5 +1,6 @@
'use client'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import {
Popover,
PopoverContent,
@ -7,7 +8,6 @@ import {
} from '@langgenius/dify-ui/popover'
import { useState } from 'react'
import { useTranslation } from '#i18n'
import Checkbox from '@/app/components/base/checkbox'
import Input from '@/app/components/base/input'
import { useTags } from '@/app/components/plugins/hooks'
import MarketplaceTrigger from './trigger/marketplace'
@ -88,19 +88,19 @@ const TagsFilter = ({
<div className="max-h-[448px] overflow-y-auto p-1">
{
filteredOptions.map(option => (
<div
<label
key={option.name}
className="flex h-7 cursor-pointer items-center rounded-lg px-2 py-1.5 select-none hover:bg-state-base-hover"
onClick={() => handleCheck(option.name)}
>
<Checkbox
className="mr-1"
checked={tags.includes(option.name)}
onCheckedChange={() => handleCheck(option.name)}
/>
<div className="px-1 system-sm-medium text-text-secondary">
{option.label}
</div>
</div>
</label>
))
}
</div>

View File

@ -695,10 +695,8 @@ describe('CategoriesFilter Component', () => {
// Act
fireEvent.click(screen.getByTestId('portal-trigger'))
// Assert - Check icon appears for checked state
await waitFor(() => {
const checkIcons = screen.getAllByTestId(/check-icon/)
expect(checkIcons.length).toBeGreaterThan(0)
expect(screen.getByRole('checkbox', { name: 'Models' })).toHaveAttribute('aria-checked', 'true')
})
})
@ -709,10 +707,8 @@ describe('CategoriesFilter Component', () => {
// Act
fireEvent.click(screen.getByTestId('portal-trigger'))
// Assert - No check icon for unchecked state
await waitFor(() => {
const checkIcons = screen.queryAllByTestId(/check-icon/)
expect(checkIcons.length).toBe(0)
expect(screen.getByRole('checkbox', { name: 'Models' })).toHaveAttribute('aria-checked', 'false')
})
})
})

View File

@ -1,5 +1,6 @@
'use client'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import {
Popover,
@ -12,7 +13,6 @@ import {
} from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Input from '@/app/components/base/input'
import { useCategories } from '../../hooks'
@ -108,19 +108,19 @@ const CategoriesFilter = ({
<div className="max-h-[448px] overflow-y-auto p-1">
{
filteredOptions.map(option => (
<div
<label
key={option.name}
className="flex h-7 cursor-pointer items-center rounded-lg px-2 py-1.5 hover:bg-state-base-hover"
onClick={() => handleCheck(option.name)}
>
<Checkbox
className="mr-1"
checked={value.includes(option.name)}
onCheckedChange={() => handleCheck(option.name)}
/>
<div className="px-1 system-sm-medium text-text-secondary">
{option.label}
</div>
</div>
</label>
))
}
</div>

View File

@ -1,5 +1,6 @@
'use client'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import {
Popover,
@ -12,7 +13,6 @@ import {
} from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Input from '@/app/components/base/input'
import { useTags } from '../../hooks'
@ -106,19 +106,19 @@ const TagsFilter = ({
<div className="max-h-[448px] overflow-y-auto p-1">
{
filteredOptions.map(option => (
<div
<label
key={option.name}
className="flex h-7 cursor-pointer items-center rounded-lg px-2 py-1.5 select-none hover:bg-state-base-hover"
onClick={() => handleCheck(option.name)}
>
<Checkbox
className="mr-1"
checked={value.includes(option.name)}
onCheckedChange={() => handleCheck(option.name)}
/>
<div className="px-1 system-sm-medium text-text-secondary">
{option.label}
</div>
</div>
</label>
))
}
</div>

View File

@ -179,23 +179,6 @@ vi.mock('@/app/components/plugins/marketplace/search-box', () => ({
),
}))
// Mock Checkbox component
vi.mock('@/app/components/base/checkbox', () => ({
default: ({ checked, onCheck, className }: {
checked?: boolean
onCheck: () => void
className?: string
}) => (
<input
type="checkbox"
checked={checked}
onChange={onCheck}
className={className}
data-testid="checkbox"
/>
),
}))
// Mock Icon component
vi.mock('@/app/components/plugins/card/base/card-icon', () => ({
default: ({ size, src }: { size: string, src: string }) => (
@ -739,7 +722,7 @@ describe('auto-update-setting', () => {
render(<ToolItem {...defaultProps} isChecked={false} />)
// Assert
expect(screen.getByTestId('checkbox')).not.toBeChecked()
expect(screen.getByRole('checkbox')).not.toBeChecked()
})
it('should render checkbox checked when isChecked is true', () => {
@ -747,7 +730,7 @@ describe('auto-update-setting', () => {
render(<ToolItem {...defaultProps} isChecked={true} />)
// Assert
expect(screen.getByTestId('checkbox')).toBeChecked()
expect(screen.getByRole('checkbox')).toBeChecked()
})
})
@ -758,7 +741,7 @@ describe('auto-update-setting', () => {
// Act
render(<ToolItem {...defaultProps} onCheckChange={onCheckChange} />)
fireEvent.click(screen.getByTestId('checkbox'))
fireEvent.click(screen.getByRole('checkbox'))
// Assert
expect(onCheckChange).toHaveBeenCalledTimes(1)
@ -1008,7 +991,7 @@ describe('auto-update-setting', () => {
// Act
renderWithQueryClient(<ToolPicker {...defaultProps} isShow={true} onChange={onChange} />)
fireEvent.click(screen.getByTestId('checkbox'))
fireEvent.click(screen.getByRole('checkbox'))
// Assert
expect(onChange).toHaveBeenCalledWith(['test-plugin'])
@ -1029,7 +1012,7 @@ describe('auto-update-setting', () => {
renderWithQueryClient(
<ToolPicker {...defaultProps} isShow={true} value={['test-plugin']} onChange={onChange} />,
)
fireEvent.click(screen.getByTestId('checkbox'))
fireEvent.click(screen.getByRole('checkbox'))
// Assert
expect(onChange).toHaveBeenCalledWith([])
@ -1054,7 +1037,7 @@ describe('auto-update-setting', () => {
)
// Click to select
fireEvent.click(screen.getByTestId('checkbox'))
fireEvent.click(screen.getByRole('checkbox'))
expect(onChange).toHaveBeenCalledWith(['plugin-1'])
// Rerender with new value
@ -1066,7 +1049,7 @@ describe('auto-update-setting', () => {
)
// Click to unselect
fireEvent.click(screen.getByTestId('checkbox'))
fireEvent.click(screen.getByRole('checkbox'))
expect(onChange).toHaveBeenCalledWith([])
})
})

View File

@ -19,20 +19,6 @@ vi.mock('@/app/components/plugins/card/base/card-icon', () => ({
default: ({ src }: { src: string }) => <div data-testid="plugin-icon">{src}</div>,
}))
vi.mock('@/app/components/base/checkbox', () => ({
default: ({
checked,
onCheck,
}: {
checked?: boolean
onCheck: () => void
}) => (
<button data-testid="checkbox" onClick={onCheck}>
{String(checked)}
</button>
),
}))
const payload = {
plugin_id: 'dify/plugin-1',
declaration: {
@ -51,14 +37,14 @@ describe('ToolItem', () => {
expect(screen.getByText('Plugin One')).toBeInTheDocument()
expect(screen.getByText('Dify')).toBeInTheDocument()
expect(screen.getByText('https://marketplace.example.com/plugins/dify/plugin-1/icon')).toBeInTheDocument()
expect(screen.getByText('true')).toBeInTheDocument()
expect(screen.getByRole('checkbox', { name: 'Plugin One' })).toHaveAttribute('aria-checked', 'true')
})
it('calls onCheckChange when checkbox is clicked', () => {
const onCheckChange = vi.fn()
render(<ToolItem payload={payload} onCheckChange={onCheckChange} />)
fireEvent.click(screen.getByTestId('checkbox'))
fireEvent.click(screen.getByRole('checkbox', { name: 'Plugin One' }))
expect(onCheckChange).toHaveBeenCalledTimes(1)
})

View File

@ -1,8 +1,8 @@
'use client'
import type { FC } from 'react'
import type { PluginDetail } from '@/app/components/plugins/types'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import * as React from 'react'
import Checkbox from '@/app/components/base/checkbox'
import Icon from '@/app/components/plugins/card/base/card-icon'
import { MARKETPLACE_API_PREFIX } from '@/config'
import { useGetLanguage } from '@/context/i18n'
@ -35,8 +35,9 @@ const ToolItem: FC<Props> = ({
</div>
<Checkbox
checked={isChecked}
onCheck={onCheckChange}
onCheckedChange={() => onCheckChange()}
className="shrink-0"
aria-label={renderI18nObject(label, language)}
/>
</div>
</div>

View File

@ -265,9 +265,7 @@ describe('LabelSelector', () => {
vi.advanceTimersByTime(10)
})
// Checkboxes should be visible in the dropdown
const checkboxes = document.querySelectorAll('[data-testid^="checkbox"]')
expect(checkboxes.length).toBeGreaterThan(0)
expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0)
})
})

View File

@ -1,5 +1,6 @@
import type { FC } from 'react'
import type { Label } from '@/app/components/tools/labels/constant'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import {
Popover,
@ -7,10 +8,8 @@ import {
PopoverTrigger,
} from '@langgenius/dify-ui/popover'
import { useDebounceFn } from 'ahooks'
import { noop } from 'es-toolkit/function'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import { Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import Input from '@/app/components/base/input'
import { useTags } from '@/app/components/plugins/hooks'
@ -89,18 +88,17 @@ const LabelSelector: FC<LabelSelectorProps> = ({
</div>
<div className="max-h-[264px] overflow-y-auto p-1">
{filteredLabelList.map(label => (
<div
<label
key={label.name}
className="flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pr-2 pl-3 hover:bg-components-panel-on-panel-item-bg-hover"
onClick={() => selectLabel(label)}
>
<Checkbox
className="shrink-0"
checked={value.includes(label.name)}
onCheck={noop}
onCheckedChange={() => selectLabel(label)}
/>
<div title={label.label} className="grow truncate text-sm leading-5 text-text-secondary">{label.label}</div>
</div>
</label>
))}
{!filteredLabelList.length && (
<div className="flex flex-col items-center gap-1 p-3">

View File

@ -8,11 +8,11 @@ import {
AlertDialogContent,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
export type DSLExportConfirmModalProps = {
envList: EnvironmentVariable[]
@ -84,24 +84,22 @@ export const DSLExportConfirmContent = ({
</tbody>
</table>
</div>
<div className="mt-4 flex gap-2">
<label className={cn('mt-4 flex gap-2', !isExporting && 'cursor-pointer')}>
<Checkbox
className="shrink-0"
checked={exportSecrets}
disabled={isExporting}
onCheck={() => setExportSecrets(!exportSecrets)}
ariaLabelledBy="dsl-export-secrets-checkbox-label"
onCheckedChange={setExportSecrets}
/>
<button
id="dsl-export-secrets-checkbox-label"
type="button"
disabled={isExporting}
className="cursor-pointer rounded-sm text-left system-sm-medium text-text-primary outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-hover disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => setExportSecrets(!exportSecrets)}
<span
className={cn(
'rounded-sm text-left system-sm-medium text-text-primary outline-hidden',
isExporting && 'cursor-not-allowed opacity-50',
)}
>
{t('env.export.checkbox', { ns: 'workflow' })}
</button>
</div>
</span>
</label>
</div>
<AlertDialogActions>
<AlertDialogCancelButton disabled={isExporting}>

View File

@ -1,9 +1,9 @@
'use client'
import type { FC } from 'react'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
type Props = {
name: string
@ -21,22 +21,22 @@ const BoolInput: FC<Props> = ({
readonly,
}) => {
const { t } = useTranslation()
const handleChange = useCallback(() => {
onChange(!value)
}, [value, onChange])
const handleChange = useCallback((checked: boolean) => {
onChange(checked)
}, [onChange])
return (
<div className="flex h-6 items-center gap-2">
<label className="flex h-6 items-center gap-2">
<Checkbox
className="h-4! w-4!"
checked={!!value}
onCheck={handleChange}
onCheckedChange={handleChange}
disabled={readonly}
/>
<div className="flex items-center gap-1 system-sm-medium text-text-secondary">
{name}
{!required && <span className="system-xs-regular text-text-tertiary">{t('panel.optional', { ns: 'workflow' })}</span>}
</div>
</div>
</label>
)
}
export default React.memo(BoolInput)

Some files were not shown because too many files have changed in this diff Show More