From 2031d31ee8a9a05a6a492bfc4b6ec8be87edd6bb Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 19 May 2026 13:40:24 +0800 Subject: [PATCH] refactor(web): migrate annotation selection to checkbox group (#36370) --- .../__tests__/batch-action.spec.tsx | 6 +- .../app/annotation/__tests__/index.spec.tsx | 4 +- .../app/annotation/__tests__/list.spec.tsx | 12 +- .../app/annotation/batch-action.tsx | 39 ++-- web/app/components/app/annotation/index.tsx | 5 +- web/app/components/app/annotation/list.tsx | 205 +++++++++--------- 6 files changed, 134 insertions(+), 137 deletions(-) diff --git a/web/app/components/app/annotation/__tests__/batch-action.spec.tsx b/web/app/components/app/annotation/__tests__/batch-action.spec.tsx index 102465e116..95342615d1 100644 --- a/web/app/components/app/annotation/__tests__/batch-action.spec.tsx +++ b/web/app/components/app/annotation/__tests__/batch-action.spec.tsx @@ -6,14 +6,14 @@ describe('BatchAction', () => { const baseProps = { selectedIds: ['1', '2', '3'], onBatchDelete: vi.fn(), - onCancel: vi.fn(), + onSelectedIdsChange: vi.fn(), } beforeEach(() => { vi.clearAllMocks() }) - it('should show the selected count and trigger cancel action', () => { + it('should show the selected count and clear selection through selection change', () => { render() expect(screen.getByText('3')).toBeInTheDocument() @@ -21,7 +21,7 @@ describe('BatchAction', () => { fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) - expect(baseProps.onCancel).toHaveBeenCalledTimes(1) + expect(baseProps.onSelectedIdsChange).toHaveBeenCalledWith([]) }) it('should confirm before running batch delete', async () => { diff --git a/web/app/components/app/annotation/__tests__/index.spec.tsx b/web/app/components/app/annotation/__tests__/index.spec.tsx index 2bd94d03b0..f0928ba2d3 100644 --- a/web/app/components/app/annotation/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/__tests__/index.spec.tsx @@ -67,7 +67,7 @@ vi.mock('../header-opts', () => ({ let latestListProps: any vi.mock('../list', () => ({ - default: (props: any) => { + List: (props: any) => { latestListProps = props if (!props.list.length) return
@@ -440,7 +440,7 @@ describe('Annotation', () => { latestListProps.onSelectedIdsChange([annotation.id]) }) await act(async () => { - latestListProps.onCancel() + latestListProps.onSelectedIdsChange([]) }) expect(latestListProps.selectedIds).toEqual([]) diff --git a/web/app/components/app/annotation/__tests__/list.spec.tsx b/web/app/components/app/annotation/__tests__/list.spec.tsx index 883c1adfe0..1116725fb2 100644 --- a/web/app/components/app/annotation/__tests__/list.spec.tsx +++ b/web/app/components/app/annotation/__tests__/list.spec.tsx @@ -1,7 +1,7 @@ import type { AnnotationItem } from '../type' import { fireEvent, render, screen, within } from '@testing-library/react' import * as React from 'react' -import List from '../list' +import { List } from '../list' const mockFormatTime = vi.fn(() => 'formatted-time') @@ -36,7 +36,6 @@ describe('List', () => { selectedIds={[]} onSelectedIdsChange={vi.fn()} onBatchDelete={vi.fn()} - onCancel={vi.fn()} />, ) @@ -57,12 +56,11 @@ describe('List', () => { selectedIds={[]} onSelectedIdsChange={onSelectedIdsChange} onBatchDelete={vi.fn()} - onCancel={vi.fn()} />, ) fireEvent.click(screen.getByRole('checkbox', { name: 'A' })) - expect(onSelectedIdsChange).toHaveBeenCalledWith(['a']) + expect(onSelectedIdsChange.mock.calls.at(-1)?.[0]).toEqual(['a']) rerender( { selectedIds={['a']} onSelectedIdsChange={onSelectedIdsChange} onBatchDelete={vi.fn()} - onCancel={vi.fn()} />, ) fireEvent.click(screen.getByRole('checkbox', { name: 'A' })) - expect(onSelectedIdsChange).toHaveBeenCalledWith([]) + expect(onSelectedIdsChange.mock.calls.at(-1)?.[0]).toEqual([]) fireEvent.click(screen.getByRole('checkbox', { name: 'common.operation.selectAll' })) - expect(onSelectedIdsChange).toHaveBeenCalledWith(['a', 'b']) + expect(onSelectedIdsChange.mock.calls.at(-1)?.[0]).toEqual(['a', 'b']) }) it('should confirm before removing an annotation and expose batch actions', async () => { @@ -93,7 +90,6 @@ describe('List', () => { selectedIds={[item.id]} onSelectedIdsChange={vi.fn()} onBatchDelete={vi.fn()} - onCancel={vi.fn()} />, ) diff --git a/web/app/components/app/annotation/batch-action.tsx b/web/app/components/app/annotation/batch-action.tsx index 961f313746..188dc46ce9 100644 --- a/web/app/components/app/annotation/batch-action.tsx +++ b/web/app/components/app/annotation/batch-action.tsx @@ -7,8 +7,8 @@ import { AlertDialogContent, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' +import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { RiDeleteBinLine } from '@remixicon/react' import { useBoolean } from 'ahooks' import * as React from 'react' import { useTranslation } from 'react-i18next' @@ -20,14 +20,14 @@ type IBatchActionProps = { className?: string selectedIds: string[] onBatchDelete: () => Promise - onCancel: () => void + onSelectedIdsChange: (selectedIds: string[]) => void } const BatchAction: FC = ({ className, selectedIds, onBatchDelete, - onCancel, + onSelectedIdsChange, }) => { const { t } = useTranslation() const [isShowDeleteConfirm, { @@ -46,34 +46,35 @@ const BatchAction: FC = ({ setIsNotDeleting() } return ( -
-
+
+
- + {selectedIds.length} - {t(`${i18nPrefix}.selected`, { ns: 'appAnnotation' })} + {t(`${i18nPrefix}.selected`, { ns: 'appAnnotation' })}
- + - + {t('operation.cancel', { ns: 'common' })} +
!open && hideDeleteConfirm()}> diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index cbad5b40ea..fd8f75fefd 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -26,7 +26,7 @@ import { sleep } from '@/utils' import EmptyElement from './empty-element' import Filter from './filter' import HeaderOpts from './header-opts' -import List from './list' +import { List } from './list' import { AnnotationEnableStatus, JobStatus } from './type' import ViewAnnotationModal from './view-annotation-modal' @@ -176,7 +176,7 @@ const Annotation: FC = (props) => { > {annotationConfig?.enabled && ( -
+
setIsShowEdit(true)}> @@ -210,7 +210,6 @@ const Annotation: FC = (props) => { selectedIds={selectedIds} onSelectedIdsChange={setSelectedIds} onBatchDelete={handleBatchDelete} - onCancel={() => setSelectedIds([])} /> ) :
} diff --git a/web/app/components/app/annotation/list.tsx b/web/app/components/app/annotation/list.tsx index fa12c95ae3..361d410698 100644 --- a/web/app/components/app/annotation/list.tsx +++ b/web/app/components/app/annotation/list.tsx @@ -1,11 +1,8 @@ '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 { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group' import * as React from 'react' -import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import useTimestamp from '@/hooks/use-timestamp' @@ -19,121 +16,126 @@ type Props = { selectedIds: string[] onSelectedIdsChange: (selectedIds: string[]) => void onBatchDelete: () => Promise - onCancel: () => void } -const List: FC = ({ +type AnnotationTableRowProps = { + item: AnnotationItem + formattedCreatedAt: string + onView: (item: AnnotationItem) => void + onRemoveClick: (id: string) => void +} + +function AnnotationTableRow({ + item, + formattedCreatedAt, + onView, + onRemoveClick, +}: AnnotationTableRowProps) { + const { t } = useTranslation() + const questionId = React.useId() + + return ( + onView(item)} + > + e.stopPropagation()}> +
+ +
+ + + {item.question} + + + {item.answer} + + {formattedCreatedAt} + {item.hit_count} + e.stopPropagation()}> +
+ onView(item)}> + + + onRemoveClick(item.id)} + > + + +
+ + + ) +} + +export function List({ list, onView, onRemove, selectedIds, onSelectedIdsChange, onBatchDelete, - onCancel, -}) => { +}: Props) { const { t } = useTranslation() const { formatTime } = useTimestamp() const [currId, setCurrId] = React.useState(null) const [showConfirmDelete, setShowConfirmDelete] = React.useState(false) - - const isAllSelected = useMemo(() => { - return list.length > 0 && list.every(item => selectedIds.includes(item.id)) - }, [list, selectedIds]) - - const isSomeSelected = useMemo(() => { - return list.some(item => selectedIds.includes(item.id)) - }, [list, selectedIds]) - - const handleSelectAll = useCallback((checked: boolean) => { - const currentPageIds = list.map(item => item.id) - const otherPageIds = selectedIds.filter(id => !currentPageIds.includes(id)) - - if (checked) - onSelectedIdsChange([...otherPageIds, ...currentPageIds]) - else - onSelectedIdsChange(otherPageIds) - }, [list, selectedIds, onSelectedIdsChange]) + const annotationIds = list.map(item => item.id) return ( <>
- - - - - - - - - - - - - {list.map(item => ( - { - onView(item) - } - } - > - - - - - - + {list.map(item => ( + { + setCurrId(id) + setShowConfirmDelete(true) + }} + /> + ))} + +
- - {t('table.header.question', { ns: 'appAnnotation' })}{t('table.header.answer', { ns: 'appAnnotation' })}{t('table.header.createdAt', { ns: 'appAnnotation' })}{t('table.header.hits', { ns: 'appAnnotation' })}{t('table.header.actions', { ns: 'appAnnotation' })}
e.stopPropagation()}> - { - if (checked) - onSelectedIdsChange([...selectedIds, item.id]) - else - onSelectedIdsChange(selectedIds.filter(id => id !== item.id)) - }} - /> - - {item.question} - - {item.answer} - {formatTime(item.created_at, t('dateTimeFormat', { ns: 'appLog' }) as string)}{item.hit_count} e.stopPropagation()}> - {/* Actions */} -
- onView(item)}> - - - { - setCurrId(item.id) - setShowConfirmDelete(true) - }} - > - - + + + + + + + + + + - ))} - -
+
+
{t('table.header.question', { ns: 'appAnnotation' })}{t('table.header.answer', { ns: 'appAnnotation' })}{t('table.header.createdAt', { ns: 'appAnnotation' })}{t('table.header.hits', { ns: 'appAnnotation' })}{t('table.header.actions', { ns: 'appAnnotation' })}
+ +
+ setShowConfirmDelete(false)} @@ -148,10 +150,9 @@ const List: FC = ({ className="absolute bottom-20 left-0 z-20" selectedIds={selectedIds} onBatchDelete={onBatchDelete} - onCancel={onCancel} + onSelectedIdsChange={onSelectedIdsChange} /> )} ) } -export default React.memo(List)