diff --git a/eslint-suppressions.json b/eslint-suppressions.json index cd737f35cc..c1835f888f 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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 diff --git a/web/__tests__/base/notion-page-selector-flow.test.tsx b/web/__tests__/base/notion-page-selector-flow.test.tsx index ef813ee4bc..34f4c988e1 100644 --- a/web/__tests__/base/notion-page-selector-flow.test.tsx +++ b/web/__tests__/base/notion-page-selector-flow.test.tsx @@ -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' }), diff --git a/web/__tests__/datasets/document-management.test.tsx b/web/__tests__/datasets/document-management.test.tsx index 96937332a3..8ce661ff4f 100644 --- a/web/__tests__/datasets/document-management.test.tsx +++ b/web/__tests__/datasets/document-management.test.tsx @@ -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) }) }) }) diff --git a/web/app/components/app/annotation/__tests__/list.spec.tsx b/web/app/components/app/annotation/__tests__/list.spec.tsx index ad6a1e5e9c..883c1adfe0 100644 --- a/web/app/components/app/annotation/__tests__/list.spec.tsx +++ b/web/app/components/app/annotation/__tests__/list.spec.tsx @@ -19,8 +19,6 @@ const createAnnotation = (overrides: Partial = {}): 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( { />, ) - 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']) }) diff --git a/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx index 497c623f3e..00f91ab429 100644 --- a/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/__tests__/index.spec.tsx @@ -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' })) diff --git a/web/app/components/app/annotation/add-annotation-modal/index.tsx b/web/app/components/app/annotation/add-annotation-modal/index.tsx index 53f3cd5bdd..f94b072e39 100644 --- a/web/app/components/app/annotation/add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/index.tsx @@ -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 = ({ )}
-
- setIsCreateNext(!isCreateNext)} /> -
{t('addModal.createNext', { ns: 'appAnnotation' })}
-
+
diff --git a/web/app/components/app/annotation/list.tsx b/web/app/components/app/annotation/list.tsx index caad4dd523..fa12c95ae3 100644 --- a/web/app/components/app/annotation/list.tsx +++ b/web/app/components/app/annotation/list.tsx @@ -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 = ({ 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 = ({ className="mr-2" checked={isAllSelected} indeterminate={!isAllSelected && isSomeSelected} - onCheck={handleSelectAll} + aria-label={t('operation.selectAll', { ns: 'common' })} + onCheckedChange={handleSelectAll} /> {t('table.header.question', { ns: 'appAnnotation' })} @@ -90,11 +91,12 @@ const List: FC = ({ { - 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)) }} /> diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx index cdb1d17833..7a77821c18 100644 --- a/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx @@ -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 }) => ( - - ), -})) - vi.mock('@langgenius/dify-ui/select', async (importOriginal) => { const actual = await importOriginal() @@ -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() - 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() - 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') }) }) diff --git a/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx b/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx index 4bd938c3f6..c932c15896 100644 --- a/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx @@ -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 = ({ )} -
- onPayloadChange('required')(!tempPayload.required)} /> +
+ {!isFileInput && ( -
- onPayloadChange('hide')(!tempPayload.hide)} /> -
+
+ +
}
- setRemoveOriginal(!removeOriginal)} /> - +
diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index 0c6f1702d7..75442c1ebd 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -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) diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index e2e8e737fc..b1145fe5de 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -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 = ({ 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 = ({ />