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 (
<>
-
-
-
- |
-
- |
- {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' })} |
-
-
-
- {list.map(item => (
- {
- onView(item)
- }
- }
- >
- | 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' })} |
- ))}
-
-
+
+
+ {list.map(item => (
+ {
+ setCurrId(id)
+ setShowConfirmDelete(true)
+ }}
+ />
+ ))}
+
+ |
+
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)