mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 07:52:50 +08:00
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:
parent
1925d58369
commit
e2c52c9b0f
@ -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
|
||||
|
||||
@ -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' }),
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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'])
|
||||
})
|
||||
|
||||
|
||||
@ -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' }))
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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]"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
@ -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',
|
||||
},
|
||||
}
|
||||
@ -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
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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]])
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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'])
|
||||
|
||||
@ -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([])
|
||||
|
||||
@ -28,7 +28,7 @@ type StepOneContentProps = {
|
||||
nextBtnDisabled: boolean
|
||||
onSelectDataSource: (dataSource: Datasource) => void
|
||||
onCredentialChange: (credentialId: string) => void
|
||||
onSelectAll: () => void
|
||||
onSelectAll: (checked: boolean) => void
|
||||
onNextStep: () => void
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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'), {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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([])
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user