mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
refactor(web): migrate annotation selection to checkbox group (#36370)
This commit is contained in:
parent
04d62867af
commit
2031d31ee8
@ -6,14 +6,14 @@ describe('BatchAction', () => {
|
|||||||
const baseProps = {
|
const baseProps = {
|
||||||
selectedIds: ['1', '2', '3'],
|
selectedIds: ['1', '2', '3'],
|
||||||
onBatchDelete: vi.fn(),
|
onBatchDelete: vi.fn(),
|
||||||
onCancel: vi.fn(),
|
onSelectedIdsChange: vi.fn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
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(<BatchAction {...baseProps} className="custom-class" />)
|
render(<BatchAction {...baseProps} className="custom-class" />)
|
||||||
|
|
||||||
expect(screen.getByText('3')).toBeInTheDocument()
|
expect(screen.getByText('3')).toBeInTheDocument()
|
||||||
@ -21,7 +21,7 @@ describe('BatchAction', () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
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 () => {
|
it('should confirm before running batch delete', async () => {
|
||||||
|
|||||||
@ -67,7 +67,7 @@ vi.mock('../header-opts', () => ({
|
|||||||
let latestListProps: any
|
let latestListProps: any
|
||||||
|
|
||||||
vi.mock('../list', () => ({
|
vi.mock('../list', () => ({
|
||||||
default: (props: any) => {
|
List: (props: any) => {
|
||||||
latestListProps = props
|
latestListProps = props
|
||||||
if (!props.list.length)
|
if (!props.list.length)
|
||||||
return <div data-testid="list-empty" />
|
return <div data-testid="list-empty" />
|
||||||
@ -440,7 +440,7 @@ describe('Annotation', () => {
|
|||||||
latestListProps.onSelectedIdsChange([annotation.id])
|
latestListProps.onSelectedIdsChange([annotation.id])
|
||||||
})
|
})
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
latestListProps.onCancel()
|
latestListProps.onSelectedIdsChange([])
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(latestListProps.selectedIds).toEqual([])
|
expect(latestListProps.selectedIds).toEqual([])
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { AnnotationItem } from '../type'
|
import type { AnnotationItem } from '../type'
|
||||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import List from '../list'
|
import { List } from '../list'
|
||||||
|
|
||||||
const mockFormatTime = vi.fn(() => 'formatted-time')
|
const mockFormatTime = vi.fn(() => 'formatted-time')
|
||||||
|
|
||||||
@ -36,7 +36,6 @@ describe('List', () => {
|
|||||||
selectedIds={[]}
|
selectedIds={[]}
|
||||||
onSelectedIdsChange={vi.fn()}
|
onSelectedIdsChange={vi.fn()}
|
||||||
onBatchDelete={vi.fn()}
|
onBatchDelete={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,12 +56,11 @@ describe('List', () => {
|
|||||||
selectedIds={[]}
|
selectedIds={[]}
|
||||||
onSelectedIdsChange={onSelectedIdsChange}
|
onSelectedIdsChange={onSelectedIdsChange}
|
||||||
onBatchDelete={vi.fn()}
|
onBatchDelete={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('checkbox', { name: 'A' }))
|
fireEvent.click(screen.getByRole('checkbox', { name: 'A' }))
|
||||||
expect(onSelectedIdsChange).toHaveBeenCalledWith(['a'])
|
expect(onSelectedIdsChange.mock.calls.at(-1)?.[0]).toEqual(['a'])
|
||||||
|
|
||||||
rerender(
|
rerender(
|
||||||
<List
|
<List
|
||||||
@ -72,14 +70,13 @@ describe('List', () => {
|
|||||||
selectedIds={['a']}
|
selectedIds={['a']}
|
||||||
onSelectedIdsChange={onSelectedIdsChange}
|
onSelectedIdsChange={onSelectedIdsChange}
|
||||||
onBatchDelete={vi.fn()}
|
onBatchDelete={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
fireEvent.click(screen.getByRole('checkbox', { name: 'A' }))
|
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' }))
|
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 () => {
|
it('should confirm before removing an annotation and expose batch actions', async () => {
|
||||||
@ -93,7 +90,6 @@ describe('List', () => {
|
|||||||
selectedIds={[item.id]}
|
selectedIds={[item.id]}
|
||||||
onSelectedIdsChange={vi.fn()}
|
onSelectedIdsChange={vi.fn()}
|
||||||
onBatchDelete={vi.fn()}
|
onBatchDelete={vi.fn()}
|
||||||
onCancel={vi.fn()}
|
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import {
|
|||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@langgenius/dify-ui/alert-dialog'
|
} from '@langgenius/dify-ui/alert-dialog'
|
||||||
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { RiDeleteBinLine } from '@remixicon/react'
|
|
||||||
import { useBoolean } from 'ahooks'
|
import { useBoolean } from 'ahooks'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -20,14 +20,14 @@ type IBatchActionProps = {
|
|||||||
className?: string
|
className?: string
|
||||||
selectedIds: string[]
|
selectedIds: string[]
|
||||||
onBatchDelete: () => Promise<void>
|
onBatchDelete: () => Promise<void>
|
||||||
onCancel: () => void
|
onSelectedIdsChange: (selectedIds: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const BatchAction: FC<IBatchActionProps> = ({
|
const BatchAction: FC<IBatchActionProps> = ({
|
||||||
className,
|
className,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
onBatchDelete,
|
onBatchDelete,
|
||||||
onCancel,
|
onSelectedIdsChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isShowDeleteConfirm, {
|
const [isShowDeleteConfirm, {
|
||||||
@ -46,34 +46,35 @@ const BatchAction: FC<IBatchActionProps> = ({
|
|||||||
setIsNotDeleting()
|
setIsNotDeleting()
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={cn('pointer-events-none flex w-full justify-center', className)}>
|
<div className={cn('pointer-events-none flex w-full justify-center gap-x-2', className)}>
|
||||||
<div className="pointer-events-auto flex items-center gap-x-1 rounded-[10px] border border-components-actionbar-border-accent bg-components-actionbar-bg-accent p-1 shadow-xl shadow-shadow-shadow-5 backdrop-blur-[5px]">
|
<div className="pointer-events-auto flex items-center gap-x-1 rounded-[10px] border border-components-actionbar-border-accent bg-components-actionbar-bg-accent p-1 shadow-xl shadow-shadow-shadow-5">
|
||||||
<div className="inline-flex items-center gap-x-2 py-1 pr-3 pl-2">
|
<div className="inline-flex items-center gap-x-2 py-1 pr-3 pl-2">
|
||||||
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-text-accent px-1 py-0.5 text-xs font-medium text-text-primary-on-surface">
|
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-text-accent system-xs-medium text-text-primary-on-surface">
|
||||||
{selectedIds.length}
|
{selectedIds.length}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[13px] leading-[16px] font-semibold text-text-accent">{t(`${i18nPrefix}.selected`, { ns: 'appAnnotation' })}</span>
|
<span className="system-sm-semibold text-text-accent">{t(`${i18nPrefix}.selected`, { ns: 'appAnnotation' })}</span>
|
||||||
</div>
|
</div>
|
||||||
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
|
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
className="flex cursor-pointer items-center gap-x-0.5 border-none bg-transparent px-3 py-2 text-left text-components-button-destructive-ghost-text focus-visible:ring-1 focus-visible:ring-state-destructive-border focus-visible:outline-hidden"
|
tone="destructive"
|
||||||
|
className="gap-x-0.5 px-3"
|
||||||
onClick={showDeleteConfirm}
|
onClick={showDeleteConfirm}
|
||||||
>
|
>
|
||||||
<RiDeleteBinLine className="h-4 w-4" aria-hidden="true" />
|
<span aria-hidden className="i-ri-delete-bin-line size-4" />
|
||||||
<span className="px-0.5 text-[13px] leading-[16px] font-medium">
|
<span className="px-0.5">
|
||||||
{t('operation.delete', { ns: 'common' })}
|
{t('operation.delete', { ns: 'common' })}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
|
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
className="border-none bg-transparent px-3.5 py-2 text-left text-[13px] leading-[16px] font-medium text-components-button-ghost-text focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
className="px-3"
|
||||||
onClick={onCancel}
|
onClick={() => onSelectedIdsChange([])}
|
||||||
>
|
>
|
||||||
{t('operation.cancel', { ns: 'common' })}
|
<span className="px-0.5">{t('operation.cancel', { ns: 'common' })}</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<AlertDialog open={isShowDeleteConfirm} onOpenChange={open => !open && hideDeleteConfirm()}>
|
<AlertDialog open={isShowDeleteConfirm} onOpenChange={open => !open && hideDeleteConfirm()}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { sleep } from '@/utils'
|
|||||||
import EmptyElement from './empty-element'
|
import EmptyElement from './empty-element'
|
||||||
import Filter from './filter'
|
import Filter from './filter'
|
||||||
import HeaderOpts from './header-opts'
|
import HeaderOpts from './header-opts'
|
||||||
import List from './list'
|
import { List } from './list'
|
||||||
import { AnnotationEnableStatus, JobStatus } from './type'
|
import { AnnotationEnableStatus, JobStatus } from './type'
|
||||||
import ViewAnnotationModal from './view-annotation-modal'
|
import ViewAnnotationModal from './view-annotation-modal'
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ const Annotation: FC<Props> = (props) => {
|
|||||||
>
|
>
|
||||||
</Switch>
|
</Switch>
|
||||||
{annotationConfig?.enabled && (
|
{annotationConfig?.enabled && (
|
||||||
<div className="flex items-center pl-1.5">
|
<div className="flex items-center pr-1 pl-1.5">
|
||||||
<div className="mr-1 h-3.5 w-px shrink-0 bg-divider-subtle"></div>
|
<div className="mr-1 h-3.5 w-px shrink-0 bg-divider-subtle"></div>
|
||||||
<ActionButton onClick={() => setIsShowEdit(true)}>
|
<ActionButton onClick={() => setIsShowEdit(true)}>
|
||||||
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
|
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
|
||||||
@ -210,7 +210,6 @@ const Annotation: FC<Props> = (props) => {
|
|||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
onSelectedIdsChange={setSelectedIds}
|
onSelectedIdsChange={setSelectedIds}
|
||||||
onBatchDelete={handleBatchDelete}
|
onBatchDelete={handleBatchDelete}
|
||||||
onCancel={() => setSelectedIds([])}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: <div className="flex h-full grow items-center justify-center"><EmptyElement /></div>}
|
: <div className="flex h-full grow items-center justify-center"><EmptyElement /></div>}
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
|
||||||
import type { AnnotationItem } from './type'
|
import type { AnnotationItem } from './type'
|
||||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
|
||||||
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useMemo } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
import useTimestamp from '@/hooks/use-timestamp'
|
import useTimestamp from '@/hooks/use-timestamp'
|
||||||
@ -19,121 +16,126 @@ type Props = {
|
|||||||
selectedIds: string[]
|
selectedIds: string[]
|
||||||
onSelectedIdsChange: (selectedIds: string[]) => void
|
onSelectedIdsChange: (selectedIds: string[]) => void
|
||||||
onBatchDelete: () => Promise<void>
|
onBatchDelete: () => Promise<void>
|
||||||
onCancel: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const List: FC<Props> = ({
|
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 (
|
||||||
|
<tr
|
||||||
|
className="cursor-pointer border-b border-divider-subtle hover:bg-background-default-hover"
|
||||||
|
onClick={() => onView(item)}
|
||||||
|
>
|
||||||
|
<td className="w-12 px-2 align-middle" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
className="shrink-0"
|
||||||
|
value={item.id}
|
||||||
|
aria-labelledby={questionId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="max-w-62.5 overflow-hidden p-3 pr-2 text-ellipsis whitespace-nowrap"
|
||||||
|
title={item.question}
|
||||||
|
>
|
||||||
|
<span id={questionId}>{item.question}</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="max-w-62.5 overflow-hidden p-3 pr-2 text-ellipsis whitespace-nowrap"
|
||||||
|
title={item.answer}
|
||||||
|
>
|
||||||
|
{item.answer}
|
||||||
|
</td>
|
||||||
|
<td className="p-3 pr-2">{formattedCreatedAt}</td>
|
||||||
|
<td className="p-3 pr-2">{item.hit_count}</td>
|
||||||
|
<td className="w-24 p-3 pr-2" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="flex space-x-1 text-text-tertiary">
|
||||||
|
<ActionButton aria-label={t('feature.annotation.edit', { ns: 'appDebug' })} onClick={() => onView(item)}>
|
||||||
|
<span aria-hidden className="i-ri-edit-line h-4 w-4" />
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
aria-label={t('feature.annotation.remove', { ns: 'appDebug' })}
|
||||||
|
onClick={() => onRemoveClick(item.id)}
|
||||||
|
>
|
||||||
|
<span aria-hidden className="i-ri-delete-bin-line h-4 w-4" />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function List({
|
||||||
list,
|
list,
|
||||||
onView,
|
onView,
|
||||||
onRemove,
|
onRemove,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
onSelectedIdsChange,
|
onSelectedIdsChange,
|
||||||
onBatchDelete,
|
onBatchDelete,
|
||||||
onCancel,
|
}: Props) {
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { formatTime } = useTimestamp()
|
const { formatTime } = useTimestamp()
|
||||||
const [currId, setCurrId] = React.useState<string | null>(null)
|
const [currId, setCurrId] = React.useState<string | null>(null)
|
||||||
const [showConfirmDelete, setShowConfirmDelete] = React.useState(false)
|
const [showConfirmDelete, setShowConfirmDelete] = React.useState(false)
|
||||||
|
const annotationIds = list.map(item => item.id)
|
||||||
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])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative mt-2 grow overflow-x-auto">
|
<div className="relative mt-2 grow overflow-x-auto">
|
||||||
<table className={cn('w-full min-w-[440px] border-collapse border-0')}>
|
<CheckboxGroup
|
||||||
<thead className="system-xs-medium-uppercase text-text-tertiary">
|
value={selectedIds}
|
||||||
<tr>
|
onValueChange={onSelectedIdsChange}
|
||||||
<td className="w-12 rounded-l-lg bg-background-section-burn px-2 whitespace-nowrap">
|
allValues={annotationIds}
|
||||||
<Checkbox
|
>
|
||||||
className="mr-2"
|
<table className="w-full min-w-110 border-collapse border-0">
|
||||||
checked={isAllSelected}
|
<thead className="system-xs-medium-uppercase text-text-tertiary">
|
||||||
indeterminate={!isAllSelected && isSomeSelected}
|
<tr>
|
||||||
aria-label={t('operation.selectAll', { ns: 'common' })}
|
<td className="w-12 rounded-l-lg bg-background-section-burn px-2 align-middle whitespace-nowrap">
|
||||||
onCheckedChange={handleSelectAll}
|
<div className="flex items-center">
|
||||||
/>
|
<Checkbox
|
||||||
</td>
|
className="shrink-0"
|
||||||
<td className="w-5 bg-background-section-burn pr-1 pl-2 whitespace-nowrap">{t('table.header.question', { ns: 'appAnnotation' })}</td>
|
parent
|
||||||
<td className="bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">{t('table.header.answer', { ns: 'appAnnotation' })}</td>
|
aria-label={t('operation.selectAll', { ns: 'common' })}
|
||||||
<td className="bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">{t('table.header.createdAt', { ns: 'appAnnotation' })}</td>
|
/>
|
||||||
<td className="bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">{t('table.header.hits', { ns: 'appAnnotation' })}</td>
|
|
||||||
<td className="w-[96px] rounded-r-lg bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">{t('table.header.actions', { ns: 'appAnnotation' })}</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="system-sm-regular text-text-secondary">
|
|
||||||
{list.map(item => (
|
|
||||||
<tr
|
|
||||||
key={item.id}
|
|
||||||
className="cursor-pointer border-b border-divider-subtle hover:bg-background-default-hover"
|
|
||||||
onClick={
|
|
||||||
() => {
|
|
||||||
onView(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<td className="w-12 px-2" onClick={e => e.stopPropagation()}>
|
|
||||||
<Checkbox
|
|
||||||
className="mr-2"
|
|
||||||
checked={selectedIds.includes(item.id)}
|
|
||||||
aria-label={item.question}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
if (checked)
|
|
||||||
onSelectedIdsChange([...selectedIds, item.id])
|
|
||||||
else
|
|
||||||
onSelectedIdsChange(selectedIds.filter(id => id !== item.id))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="max-w-[250px] overflow-hidden p-3 pr-2 text-ellipsis whitespace-nowrap"
|
|
||||||
title={item.question}
|
|
||||||
>
|
|
||||||
{item.question}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="max-w-[250px] overflow-hidden p-3 pr-2 text-ellipsis whitespace-nowrap"
|
|
||||||
title={item.answer}
|
|
||||||
>
|
|
||||||
{item.answer}
|
|
||||||
</td>
|
|
||||||
<td className="p-3 pr-2">{formatTime(item.created_at, t('dateTimeFormat', { ns: 'appLog' }) as string)}</td>
|
|
||||||
<td className="p-3 pr-2">{item.hit_count}</td>
|
|
||||||
<td className="w-[96px] p-3 pr-2" onClick={e => e.stopPropagation()}>
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex space-x-1 text-text-tertiary">
|
|
||||||
<ActionButton onClick={() => onView(item)}>
|
|
||||||
<RiEditLine className="h-4 w-4" />
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
onClick={() => {
|
|
||||||
setCurrId(item.id)
|
|
||||||
setShowConfirmDelete(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiDeleteBinLine className="h-4 w-4" />
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="w-5 bg-background-section-burn pr-1 pl-2 whitespace-nowrap">{t('table.header.question', { ns: 'appAnnotation' })}</td>
|
||||||
|
<td className="bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">{t('table.header.answer', { ns: 'appAnnotation' })}</td>
|
||||||
|
<td className="bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">{t('table.header.createdAt', { ns: 'appAnnotation' })}</td>
|
||||||
|
<td className="bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">{t('table.header.hits', { ns: 'appAnnotation' })}</td>
|
||||||
|
<td className="w-24 rounded-r-lg bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">{t('table.header.actions', { ns: 'appAnnotation' })}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody className="system-sm-regular text-text-secondary">
|
||||||
</table>
|
{list.map(item => (
|
||||||
|
<AnnotationTableRow
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
formattedCreatedAt={formatTime(item.created_at, t('dateTimeFormat', { ns: 'appLog' }) as string)}
|
||||||
|
onView={onView}
|
||||||
|
onRemoveClick={(id) => {
|
||||||
|
setCurrId(id)
|
||||||
|
setShowConfirmDelete(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</CheckboxGroup>
|
||||||
<RemoveAnnotationConfirmModal
|
<RemoveAnnotationConfirmModal
|
||||||
isShow={showConfirmDelete}
|
isShow={showConfirmDelete}
|
||||||
onHide={() => setShowConfirmDelete(false)}
|
onHide={() => setShowConfirmDelete(false)}
|
||||||
@ -148,10 +150,9 @@ const List: FC<Props> = ({
|
|||||||
className="absolute bottom-20 left-0 z-20"
|
className="absolute bottom-20 left-0 z-20"
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
onBatchDelete={onBatchDelete}
|
onBatchDelete={onBatchDelete}
|
||||||
onCancel={onCancel}
|
onSelectedIdsChange={onSelectedIdsChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default React.memo(List)
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user