diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index 800bbc746b..0a6df42d77 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -1986,9 +1986,6 @@
},
"react-refresh/only-export-components": {
"count": 1
- },
- "react/use-memo": {
- "count": 1
}
},
"web/app/components/datasets/documents/detail/completed/segment-list.tsx": {
diff --git a/web/__tests__/datasets/segment-crud.test.tsx b/web/__tests__/datasets/segment-crud.test.tsx
index fe21dd5079..1308b0c103 100644
--- a/web/__tests__/datasets/segment-crud.test.tsx
+++ b/web/__tests__/datasets/segment-crud.test.tsx
@@ -111,77 +111,27 @@ describe('Segment CRUD Flow', () => {
})
describe('Segment Selection → Batch Operations', () => {
- const segments = [
- createSegment('seg-1'),
- createSegment('seg-2'),
- createSegment('seg-3'),
- ]
-
it('should manage individual segment selection', () => {
- const { result } = renderHook(() => useSegmentSelection(segments))
+ const { result } = renderHook(() => useSegmentSelection())
act(() => {
- result.current.onSelected('seg-1')
+ result.current.onSelectedSegmentIdsChange(['seg-1'])
})
expect(result.current.selectedSegmentIds).toContain('seg-1')
act(() => {
- result.current.onSelected('seg-2')
+ result.current.onSelectedSegmentIdsChange(['seg-1', 'seg-2'])
})
expect(result.current.selectedSegmentIds).toContain('seg-1')
expect(result.current.selectedSegmentIds).toContain('seg-2')
expect(result.current.selectedSegmentIds).toHaveLength(2)
})
- it('should toggle selection on repeated click', () => {
- const { result } = renderHook(() => useSegmentSelection(segments))
-
- act(() => {
- result.current.onSelected('seg-1')
- })
- expect(result.current.selectedSegmentIds).toContain('seg-1')
-
- act(() => {
- result.current.onSelected('seg-1')
- })
- expect(result.current.selectedSegmentIds).not.toContain('seg-1')
- })
-
- it('should support select all toggle', () => {
- const { result } = renderHook(() => useSegmentSelection(segments))
-
- act(() => {
- result.current.onSelectedAll()
- })
- expect(result.current.selectedSegmentIds).toHaveLength(3)
- expect(result.current.isAllSelected).toBe(true)
-
- act(() => {
- result.current.onSelectedAll()
- })
- expect(result.current.selectedSegmentIds).toHaveLength(0)
- expect(result.current.isAllSelected).toBe(false)
- })
-
- it('should detect partial selection via isSomeSelected', () => {
- const { result } = renderHook(() => useSegmentSelection(segments))
-
- act(() => {
- result.current.onSelected('seg-1')
- })
-
- // After selecting one of three, isSomeSelected should be true
- expect(result.current.selectedSegmentIds).toEqual(['seg-1'])
- expect(result.current.isSomeSelected).toBe(true)
- expect(result.current.isAllSelected).toBe(false)
- })
-
it('should clear selection via onCancelBatchOperation', () => {
- const { result } = renderHook(() => useSegmentSelection(segments))
+ const { result } = renderHook(() => useSegmentSelection())
act(() => {
- result.current.onSelected('seg-1')
- result.current.onSelected('seg-2')
+ result.current.onSelectedSegmentIdsChange(['seg-1', 'seg-2'])
})
expect(result.current.selectedSegmentIds).toHaveLength(2)
@@ -271,7 +221,7 @@ describe('Segment CRUD Flow', () => {
useSearchFilter({ onPageChange: vi.fn() }),
)
const { result: selectionResult } = renderHook(() =>
- useSegmentSelection(segments),
+ useSegmentSelection(),
)
const { result: modalResult } = renderHook(() =>
useModalState({ onNewSegmentModalChange: vi.fn() }),
@@ -284,7 +234,7 @@ describe('Segment CRUD Flow', () => {
// Select a segment
act(() => {
- selectionResult.current.onSelected('seg-1')
+ selectionResult.current.onSelectedSegmentIdsChange(['seg-1'])
})
// Open detail modal
diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx
index 900c974252..0d6a37d64e 100644
--- a/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx
@@ -137,36 +137,40 @@ vi.mock('../hooks/use-child-segment-data', () => ({
},
}))
-vi.mock('../components/menu-bar', () => ({
- default: ({ totalText, onInputChange, inputValue, isLoading, onSelectedAll, onChangeStatus }: {
- totalText: string
- onInputChange: (value: string) => void
- inputValue: string
- isLoading: boolean
- onSelectedAll?: () => void
- onChangeStatus?: (item: { value: string | number, name: string }) => void
- }) => (
-
- {totalText}
- onInputChange(e.target.value)}
- disabled={isLoading}
- />
- {onSelectedAll && (
-
- )}
- {onChangeStatus && (
- <>
-
-
-
- >
- )}
-
- ),
-}))
+vi.mock('../components/menu-bar', async () => {
+ const { Checkbox } = await import('@langgenius/dify-ui/checkbox')
+
+ return {
+ default: ({ hasSelectableSegments, totalText, onInputChange, inputValue, isLoading, onChangeStatus }: {
+ hasSelectableSegments: boolean
+ totalText: string
+ onInputChange: (value: string) => void
+ inputValue: string
+ isLoading: boolean
+ onChangeStatus?: (item: { value: string | number, name: string }) => void
+ }) => (
+
+ {totalText}
+ onInputChange(e.target.value)}
+ disabled={isLoading}
+ />
+ {hasSelectableSegments
+ ?
+ : }
+ {onChangeStatus && (
+ <>
+
+
+
+ >
+ )}
+
+ ),
+ }
+})
vi.mock('../components/drawer-group', () => ({
DrawerGroup: () => ,
@@ -751,6 +755,17 @@ describe('Batch Action Callbacks', () => {
})
})
+ it('should not render select all when there are no current page segments', () => {
+ mockSegmentListData.data = []
+ mockSegmentListData.total = 0
+
+ render(, { wrapper: createWrapper() })
+
+ expect(screen.queryByTestId('select-all-button')).not.toBeInTheDocument()
+ expect(screen.getByTestId('select-all-spacer')).toBeInTheDocument()
+ expect(screen.queryByTestId('batch-action')).not.toBeInTheDocument()
+ })
+
it('should call onChangeSwitch with true when batch enable is clicked', async () => {
render(, { wrapper: createWrapper() })
diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/segment-list.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/segment-list.spec.tsx
index d76bac26e6..4e9f880536 100644
--- a/web/app/components/datasets/documents/detail/completed/__tests__/segment-list.spec.tsx
+++ b/web/app/components/datasets/documents/detail/completed/__tests__/segment-list.spec.tsx
@@ -123,8 +123,6 @@ describe('SegmentList', () => {
ref: null,
isLoading: false,
items: [createMockSegment('seg-1', 'Segment 1 content')],
- selectedSegmentIds: [],
- onSelected: vi.fn(),
onClick: vi.fn(),
onChangeSwitch: vi.fn(),
onDelete: vi.fn(),
@@ -289,18 +287,10 @@ describe('SegmentList', () => {
expect(screen.getAllByRole('checkbox')).toHaveLength(defaultProps.items.length)
})
- it('should pass selectedSegmentIds to check state', () => {
- const { container } = render()
+ it('should label each segment checkbox', () => {
+ render()
- // Assert - component should render with selected state
- expect(container.firstChild).toBeInTheDocument()
- })
-
- it('should handle empty selectedSegmentIds', () => {
- const { container } = render()
-
- // Assert - component should render
- expect(container.firstChild).toBeInTheDocument()
+ expect(screen.getByRole('checkbox', { name: 'datasetDocuments.segment.chunk 1' })).toBeInTheDocument()
})
})
diff --git a/web/app/components/datasets/documents/detail/completed/components/__tests__/menu-bar.spec.tsx b/web/app/components/datasets/documents/detail/completed/components/__tests__/menu-bar.spec.tsx
index f3bb01a804..9ca5601e6a 100644
--- a/web/app/components/datasets/documents/detail/completed/components/__tests__/menu-bar.spec.tsx
+++ b/web/app/components/datasets/documents/detail/completed/components/__tests__/menu-bar.spec.tsx
@@ -1,3 +1,4 @@
+import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import MenuBar from '../menu-bar'
@@ -16,9 +17,7 @@ vi.mock('../../status-item', () => ({
describe('MenuBar', () => {
const defaultProps = {
- isAllSelected: false,
- isSomeSelected: false,
- onSelectedAll: vi.fn(),
+ hasSelectableSegments: true,
isLoading: false,
totalText: '10 Chunks',
statusList: [
@@ -38,49 +37,63 @@ describe('MenuBar', () => {
vi.clearAllMocks()
})
+ const renderMenuBar = (props: Partial = {}) => {
+ return render(
+
+
+ ,
+ )
+ }
+
it('should render total text', () => {
- render()
+ renderMenuBar()
expect(screen.getByText('10 Chunks')).toBeInTheDocument()
})
it('should render checkbox', () => {
- render()
+ renderMenuBar()
expect(screen.getByRole('checkbox', { name: 'common.operation.selectAll' })).toBeInTheDocument()
})
+ it('should not render select all checkbox when there are no selectable segments', () => {
+ renderMenuBar({ hasSelectableSegments: false })
+
+ expect(screen.queryByRole('checkbox', { name: 'common.operation.selectAll' })).not.toBeInTheDocument()
+ })
+
it('should call onInputChange when input changes', () => {
- render()
+ renderMenuBar()
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'test search' } })
expect(defaultProps.onInputChange).toHaveBeenCalledWith('test search')
})
it('should render display toggle', () => {
- render()
+ renderMenuBar()
expect(screen.getByTestId('display-toggle')).toBeInTheDocument()
})
it('should call toggleCollapsed when display toggle clicked', () => {
- render()
+ renderMenuBar()
fireEvent.click(screen.getByTestId('display-toggle'))
expect(defaultProps.toggleCollapsed).toHaveBeenCalled()
})
it('should call onInputChange with empty string when input is cleared', () => {
- render()
+ renderMenuBar({ inputValue: 'some text' })
const clearButton = screen.getByRole('button', { name: 'common.operation.clear' })
fireEvent.click(clearButton)
expect(defaultProps.onInputChange).toHaveBeenCalledWith('')
})
it('should render select with status items via renderOption', () => {
- render()
+ renderMenuBar()
expect(screen.getByText('All')).toBeInTheDocument()
})
it('should call renderOption for each item when dropdown is opened', async () => {
- render()
+ renderMenuBar()
const selectButton = screen.getByRole('combobox')
fireEvent.click(selectButton)
diff --git a/web/app/components/datasets/documents/detail/completed/components/__tests__/segment-list-content.spec.tsx b/web/app/components/datasets/documents/detail/completed/components/__tests__/segment-list-content.spec.tsx
index eeeeca333d..9f35b1b2eb 100644
--- a/web/app/components/datasets/documents/detail/completed/components/__tests__/segment-list-content.spec.tsx
+++ b/web/app/components/datasets/documents/detail/completed/components/__tests__/segment-list-content.spec.tsx
@@ -84,8 +84,6 @@ describe('GeneralModeContent', () => {
embeddingAvailable: true,
isLoadingSegmentList: false,
segments: [{ id: 'seg-1' }, { id: 'seg-2' }] as SegmentDetailModel[],
- selectedSegmentIds: [],
- onSelected: vi.fn(),
onChangeSwitch: vi.fn(),
onDelete: vi.fn(),
onClickCard: vi.fn(),
diff --git a/web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx b/web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx
index 4d0f48f223..09a3db925c 100644
--- a/web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx
+++ b/web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx
@@ -1,5 +1,4 @@
'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'
@@ -16,9 +15,7 @@ type Item = {
} & Record
type MenuBarProps = {
- isAllSelected: boolean
- isSomeSelected: boolean
- onSelectedAll: () => void
+ hasSelectableSegments: boolean
isLoading: boolean
totalText: string
statusList: Item[]
@@ -30,10 +27,8 @@ type MenuBarProps = {
toggleCollapsed: () => void
}
-const MenuBar: FC = ({
- isAllSelected,
- isSomeSelected,
- onSelectedAll,
+function MenuBar({
+ hasSelectableSegments,
isLoading,
totalText,
statusList,
@@ -43,20 +38,24 @@ const MenuBar: FC = ({
onInputChange,
isCollapsed,
toggleCollapsed,
-}) => {
+}: MenuBarProps) {
const { t } = useTranslation()
const selectedStatus = statusList.find(item => item.value === selectDefaultValue) ?? null
return (
-
onSelectedAll()}
- disabled={isLoading}
- />
+ {hasSelectableSegments
+ ? (
+
+ )
+ : (
+
+ )}
{totalText}
-
+
onChange(nextValue)}
+ className="max-h-[448px] overflow-y-auto p-1"
+ >
{
filteredOptions.map(option => (
diff --git a/web/app/components/plugins/plugin-page/filter-management/tag-filter.tsx b/web/app/components/plugins/plugin-page/filter-management/tag-filter.tsx
index 226bd9514d..9a9cd1a65f 100644
--- a/web/app/components/plugins/plugin-page/filter-management/tag-filter.tsx
+++ b/web/app/components/plugins/plugin-page/filter-management/tag-filter.tsx
@@ -1,6 +1,7 @@
'use client'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
+import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { cn } from '@langgenius/dify-ui/cn'
import {
Popover,
@@ -29,12 +30,6 @@ const TagsFilter = ({
const [searchText, setSearchText] = useState('')
const { tags: options, getTagLabel } = useTags()
const filteredOptions = options.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase()))
- const handleCheck = (id: string) => {
- if (value.includes(id))
- onChange(value.filter(tag => tag !== id))
- else
- onChange([...value, id])
- }
const selectedTagsLength = value.length
return (
@@ -103,7 +98,12 @@ const TagsFilter = ({
placeholder={t('searchTags', { ns: 'pluginTags' })}
/>
-
+
onChange(nextValue)}
+ className="max-h-[448px] overflow-y-auto p-1"
+ >
{
filteredOptions.map(option => (
diff --git a/web/app/components/tools/labels/selector.tsx b/web/app/components/tools/labels/selector.tsx
index 07e60e08df..e8e068dc7f 100644
--- a/web/app/components/tools/labels/selector.tsx
+++ b/web/app/components/tools/labels/selector.tsx
@@ -1,6 +1,5 @@
-import type { FC } from 'react'
-import type { Label } from '@/app/components/tools/labels/constant'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
+import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { cn } from '@langgenius/dify-ui/cn'
import {
Popover,
@@ -8,7 +7,7 @@ import {
PopoverTrigger,
} from '@langgenius/dify-ui/popover'
import { useDebounceFn } from 'ahooks'
-import { useMemo, useState } from 'react'
+import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import Input from '@/app/components/base/input'
@@ -19,10 +18,10 @@ type LabelSelectorProps = {
onChange: (v: string[]) => void
}
-const LabelSelector: FC = ({
+function LabelSelector({
value,
onChange,
-}) => {
+}: LabelSelectorProps) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
@@ -39,20 +38,8 @@ const LabelSelector: FC = ({
handleSearch()
}
- const filteredLabelList = useMemo(() => {
- return labelList.filter(label => label.name.includes(searchKeywords))
- }, [labelList, searchKeywords])
-
- const selectedLabels = useMemo(() => {
- return value.map(v => labelList.find(l => l.name === v)?.label).join(', ')
- }, [value, labelList])
-
- const selectLabel = (label: Label) => {
- if (value.includes(label.name))
- onChange(value.filter(v => v !== label.name))
- else
- onChange([...value, label.name])
- }
+ const filteredLabelList = labelList.filter(label => label.name.includes(searchKeywords))
+ const selectedLabels = value.map(v => labelList.find(l => l.name === v)?.label).join(', ')
return (
@@ -86,7 +73,12 @@ const LabelSelector: FC = ({
onClear={() => handleKeywordsChange('')}
/>
-
+
onChange(nextValue)}
+ className="max-h-[264px] overflow-y-auto p-1"
+ >
{filteredLabelList.map(label => (
@@ -106,7 +97,7 @@ const LabelSelector: FC = ({
{t('tag.noTag', { ns: 'common' })}
)}
-
+