mirror of
https://github.com/langgenius/dify.git
synced 2026-05-21 09:17:27 +08:00
refactor(web): migrate multi-checkbox lists to CheckboxGroup (#36381)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -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": {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}) => (
|
||||
<div data-testid="menu-bar">
|
||||
<span data-testid="total-text">{totalText}</span>
|
||||
<input
|
||||
data-testid="search-input"
|
||||
value={inputValue}
|
||||
onChange={e => onInputChange(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{onSelectedAll && (
|
||||
<button data-testid="select-all-button" onClick={onSelectedAll}>Select All</button>
|
||||
)}
|
||||
{onChangeStatus && (
|
||||
<>
|
||||
<button data-testid="status-enabled" onClick={() => onChangeStatus({ value: 1, name: 'Enabled' })}>Enabled</button>
|
||||
<button data-testid="status-disabled" onClick={() => onChangeStatus({ value: 0, name: 'Disabled' })}>Disabled</button>
|
||||
<button data-testid="status-all" onClick={() => onChangeStatus({ value: 'all', name: 'All' })}>All</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
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
|
||||
}) => (
|
||||
<div data-testid="menu-bar">
|
||||
<span data-testid="total-text">{totalText}</span>
|
||||
<input
|
||||
data-testid="search-input"
|
||||
value={inputValue}
|
||||
onChange={e => onInputChange(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{hasSelectableSegments
|
||||
? <Checkbox parent data-testid="select-all-button" aria-label="Select All" disabled={isLoading} />
|
||||
: <span data-testid="select-all-spacer" aria-hidden />}
|
||||
{onChangeStatus && (
|
||||
<>
|
||||
<button data-testid="status-enabled" onClick={() => onChangeStatus({ value: 1, name: 'Enabled' })}>Enabled</button>
|
||||
<button data-testid="status-disabled" onClick={() => onChangeStatus({ value: 0, name: 'Disabled' })}>Disabled</button>
|
||||
<button data-testid="status-all" onClick={() => onChangeStatus({ value: 'all', name: 'All' })}>All</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../components/drawer-group', () => ({
|
||||
DrawerGroup: () => <div data-testid="drawer-group" />,
|
||||
@ -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(<Completed {...defaultProps} />, { 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(<Completed {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
|
||||
@ -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(<SegmentList {...defaultProps} selectedSegmentIds={['seg-1']} />)
|
||||
it('should label each segment checkbox', () => {
|
||||
render(<SegmentList {...defaultProps} />)
|
||||
|
||||
// Assert - component should render with selected state
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty selectedSegmentIds', () => {
|
||||
const { container } = render(<SegmentList {...defaultProps} selectedSegmentIds={[]} />)
|
||||
|
||||
// Assert - component should render
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
expect(screen.getByRole('checkbox', { name: 'datasetDocuments.segment.chunk 1' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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<typeof defaultProps> = {}) => {
|
||||
return render(
|
||||
<CheckboxGroup value={[]} onValueChange={vi.fn()} allValues={['seg-1']}>
|
||||
<MenuBar {...defaultProps} {...props} />
|
||||
</CheckboxGroup>,
|
||||
)
|
||||
}
|
||||
|
||||
it('should render total text', () => {
|
||||
render(<MenuBar {...defaultProps} />)
|
||||
renderMenuBar()
|
||||
expect(screen.getByText('10 Chunks')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render checkbox', () => {
|
||||
render(<MenuBar {...defaultProps} />)
|
||||
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(<MenuBar {...defaultProps} />)
|
||||
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(<MenuBar {...defaultProps} />)
|
||||
renderMenuBar()
|
||||
expect(screen.getByTestId('display-toggle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call toggleCollapsed when display toggle clicked', () => {
|
||||
render(<MenuBar {...defaultProps} />)
|
||||
renderMenuBar()
|
||||
fireEvent.click(screen.getByTestId('display-toggle'))
|
||||
expect(defaultProps.toggleCollapsed).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onInputChange with empty string when input is cleared', () => {
|
||||
render(<MenuBar {...defaultProps} inputValue="some text" />)
|
||||
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(<MenuBar {...defaultProps} />)
|
||||
renderMenuBar()
|
||||
expect(screen.getByText('All')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call renderOption for each item when dropdown is opened', async () => {
|
||||
render(<MenuBar {...defaultProps} />)
|
||||
renderMenuBar()
|
||||
|
||||
const selectButton = screen.getByRole('combobox')
|
||||
fireEvent.click(selectButton)
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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<string, unknown>
|
||||
|
||||
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<MenuBarProps> = ({
|
||||
isAllSelected,
|
||||
isSomeSelected,
|
||||
onSelectedAll,
|
||||
function MenuBar({
|
||||
hasSelectableSegments,
|
||||
isLoading,
|
||||
totalText,
|
||||
statusList,
|
||||
@ -43,20 +38,24 @@ const MenuBar: FC<MenuBarProps> = ({
|
||||
onInputChange,
|
||||
isCollapsed,
|
||||
toggleCollapsed,
|
||||
}) => {
|
||||
}: MenuBarProps) {
|
||||
const { t } = useTranslation()
|
||||
const selectedStatus = statusList.find(item => item.value === selectDefaultValue) ?? null
|
||||
|
||||
return (
|
||||
<div className={s.docSearchWrapper}>
|
||||
<Checkbox
|
||||
className="shrink-0"
|
||||
checked={isAllSelected}
|
||||
indeterminate={!isAllSelected && isSomeSelected}
|
||||
aria-label={t('operation.selectAll', { ns: 'common' })}
|
||||
onCheckedChange={() => onSelectedAll()}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{hasSelectableSegments
|
||||
? (
|
||||
<Checkbox
|
||||
className="shrink-0"
|
||||
parent
|
||||
aria-label={t('operation.selectAll', { ns: 'common' })}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<span className="size-4 shrink-0" aria-hidden />
|
||||
)}
|
||||
<div className="flex-1 pl-5 system-sm-semibold-uppercase text-text-secondary">{totalText}</div>
|
||||
<Select
|
||||
value={selectedStatus ? String(selectedStatus.value) : null}
|
||||
|
||||
@ -78,8 +78,6 @@ type GeneralModeContentProps = {
|
||||
embeddingAvailable: boolean
|
||||
isLoadingSegmentList: boolean
|
||||
segments: SegmentDetailModel[]
|
||||
selectedSegmentIds: string[]
|
||||
onSelected: (segId: string) => void
|
||||
onChangeSwitch: (enable: boolean, segId?: string) => Promise<void>
|
||||
onDelete: (segId?: string) => Promise<void>
|
||||
onClickCard: (detail: SegmentDetailModel, isEditMode?: boolean) => void
|
||||
@ -95,8 +93,6 @@ export const GeneralModeContent: FC<GeneralModeContentProps> = ({
|
||||
embeddingAvailable,
|
||||
isLoadingSegmentList,
|
||||
segments,
|
||||
selectedSegmentIds,
|
||||
onSelected,
|
||||
onChangeSwitch,
|
||||
onDelete,
|
||||
onClickCard,
|
||||
@ -112,8 +108,6 @@ export const GeneralModeContent: FC<GeneralModeContentProps> = ({
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
isLoading={isLoadingSegmentList}
|
||||
items={segments}
|
||||
selectedSegmentIds={selectedSegmentIds}
|
||||
onSelected={onSelected}
|
||||
onChangeSwitch={onChangeSwitch}
|
||||
onDelete={onDelete}
|
||||
onClick={onClickCard}
|
||||
|
||||
@ -1,83 +1,33 @@
|
||||
import type { SegmentDetailModel } from '@/models/datasets'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useSegmentSelection } from '../use-segment-selection'
|
||||
import { mergeCurrentPageSelectedSegmentIds, useSegmentSelection } from '../use-segment-selection'
|
||||
|
||||
describe('useSegmentSelection', () => {
|
||||
const segments = [
|
||||
{ id: 'seg-1', content: 'A' },
|
||||
{ id: 'seg-2', content: 'B' },
|
||||
{ id: 'seg-3', content: 'C' },
|
||||
] as unknown as SegmentDetailModel[]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should initialize with empty selection', () => {
|
||||
const { result } = renderHook(() => useSegmentSelection(segments))
|
||||
|
||||
expect(result.current.selectedSegmentIds).toEqual([])
|
||||
expect(result.current.isAllSelected).toBe(false)
|
||||
expect(result.current.isSomeSelected).toBe(false)
|
||||
})
|
||||
|
||||
it('should select a segment', () => {
|
||||
const { result } = renderHook(() => useSegmentSelection(segments))
|
||||
|
||||
act(() => {
|
||||
result.current.onSelected('seg-1')
|
||||
})
|
||||
|
||||
expect(result.current.selectedSegmentIds).toEqual(['seg-1'])
|
||||
expect(result.current.isSomeSelected).toBe(true)
|
||||
expect(result.current.isAllSelected).toBe(false)
|
||||
})
|
||||
|
||||
it('should deselect a selected segment', () => {
|
||||
const { result } = renderHook(() => useSegmentSelection(segments))
|
||||
|
||||
act(() => {
|
||||
result.current.onSelected('seg-1')
|
||||
})
|
||||
act(() => {
|
||||
result.current.onSelected('seg-1')
|
||||
})
|
||||
const { result } = renderHook(() => useSegmentSelection())
|
||||
|
||||
expect(result.current.selectedSegmentIds).toEqual([])
|
||||
})
|
||||
|
||||
it('should select all segments', () => {
|
||||
const { result } = renderHook(() => useSegmentSelection(segments))
|
||||
it('should update selected segment ids', () => {
|
||||
const { result } = renderHook(() => useSegmentSelection())
|
||||
|
||||
act(() => {
|
||||
result.current.onSelectedAll()
|
||||
result.current.onSelectedSegmentIdsChange(['seg-1', 'seg-2'])
|
||||
})
|
||||
|
||||
expect(result.current.selectedSegmentIds).toEqual(['seg-1', 'seg-2', 'seg-3'])
|
||||
expect(result.current.isAllSelected).toBe(true)
|
||||
})
|
||||
|
||||
it('should deselect all when all are selected', () => {
|
||||
const { result } = renderHook(() => useSegmentSelection(segments))
|
||||
|
||||
act(() => {
|
||||
result.current.onSelectedAll()
|
||||
})
|
||||
act(() => {
|
||||
result.current.onSelectedAll()
|
||||
})
|
||||
|
||||
expect(result.current.selectedSegmentIds).toEqual([])
|
||||
expect(result.current.isAllSelected).toBe(false)
|
||||
expect(result.current.selectedSegmentIds).toEqual(['seg-1', 'seg-2'])
|
||||
})
|
||||
|
||||
it('should cancel batch operation', () => {
|
||||
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'])
|
||||
})
|
||||
act(() => {
|
||||
result.current.onCancelBatchOperation()
|
||||
@ -87,10 +37,10 @@ describe('useSegmentSelection', () => {
|
||||
})
|
||||
|
||||
it('should clear selection', () => {
|
||||
const { result } = renderHook(() => useSegmentSelection(segments))
|
||||
const { result } = renderHook(() => useSegmentSelection())
|
||||
|
||||
act(() => {
|
||||
result.current.onSelected('seg-1')
|
||||
result.current.onSelectedSegmentIdsChange(['seg-1'])
|
||||
})
|
||||
act(() => {
|
||||
result.current.clearSelection()
|
||||
@ -99,61 +49,19 @@ describe('useSegmentSelection', () => {
|
||||
expect(result.current.selectedSegmentIds).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle empty segments array', () => {
|
||||
const { result } = renderHook(() => useSegmentSelection([]))
|
||||
|
||||
expect(result.current.isAllSelected).toBe(false)
|
||||
expect(result.current.isSomeSelected).toBe(false)
|
||||
it('should merge current page selection without dropping selected ids from other pages', () => {
|
||||
expect(mergeCurrentPageSelectedSegmentIds({
|
||||
selectedSegmentIds: ['page-1-a', 'page-1-b'],
|
||||
currentPageSegmentIds: ['page-2-a', 'page-2-b'],
|
||||
nextCurrentPageSelectedSegmentIds: ['page-2-a'],
|
||||
})).toEqual(['page-1-a', 'page-1-b', 'page-2-a'])
|
||||
})
|
||||
|
||||
it('should allow multiple selections', () => {
|
||||
const { result } = renderHook(() => useSegmentSelection(segments))
|
||||
|
||||
act(() => {
|
||||
result.current.onSelected('seg-1')
|
||||
})
|
||||
act(() => {
|
||||
result.current.onSelected('seg-2')
|
||||
})
|
||||
|
||||
expect(result.current.selectedSegmentIds).toEqual(['seg-1', 'seg-2'])
|
||||
expect(result.current.isSomeSelected).toBe(true)
|
||||
expect(result.current.isAllSelected).toBe(false)
|
||||
})
|
||||
|
||||
it('should preserve selection of segments not in current list', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ segs }) => useSegmentSelection(segs),
|
||||
{ initialProps: { segs: segments } },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.onSelected('seg-1')
|
||||
})
|
||||
|
||||
// Rerender with different segment list (simulating page change)
|
||||
const newSegments = [
|
||||
{ id: 'seg-4', content: 'D' },
|
||||
{ id: 'seg-5', content: 'E' },
|
||||
] as unknown as SegmentDetailModel[]
|
||||
|
||||
rerender({ segs: newSegments })
|
||||
|
||||
// Previously selected segment should still be in selectedSegmentIds
|
||||
expect(result.current.selectedSegmentIds).toContain('seg-1')
|
||||
})
|
||||
|
||||
it('should select remaining unselected segments when onSelectedAll is called with partial selection', () => {
|
||||
const { result } = renderHook(() => useSegmentSelection(segments))
|
||||
|
||||
act(() => {
|
||||
result.current.onSelected('seg-1')
|
||||
})
|
||||
act(() => {
|
||||
result.current.onSelectedAll()
|
||||
})
|
||||
|
||||
expect(result.current.selectedSegmentIds).toEqual(expect.arrayContaining(['seg-1', 'seg-2', 'seg-3']))
|
||||
expect(result.current.isAllSelected).toBe(true)
|
||||
it('should replace only current page selected ids when current page selection changes', () => {
|
||||
expect(mergeCurrentPageSelectedSegmentIds({
|
||||
selectedSegmentIds: ['page-1-a', 'page-2-a', 'page-2-b'],
|
||||
currentPageSegmentIds: ['page-2-a', 'page-2-b'],
|
||||
nextCurrentPageSelectedSegmentIds: ['page-2-b'],
|
||||
})).toEqual(['page-1-a', 'page-2-b'])
|
||||
})
|
||||
})
|
||||
|
||||
@ -6,4 +6,4 @@ export { useSearchFilter } from './use-search-filter'
|
||||
|
||||
export { useSegmentListData } from './use-segment-list-data'
|
||||
|
||||
export { useSegmentSelection } from './use-segment-selection'
|
||||
export { mergeCurrentPageSelectedSegmentIds, useSegmentSelection } from './use-segment-selection'
|
||||
|
||||
@ -1,43 +1,39 @@
|
||||
import type { SegmentDetailModel } from '@/models/datasets'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
type UseSegmentSelectionReturn = {
|
||||
selectedSegmentIds: string[]
|
||||
isAllSelected: boolean
|
||||
isSomeSelected: boolean
|
||||
onSelected: (segId: string) => void
|
||||
onSelectedAll: () => void
|
||||
onSelectedSegmentIdsChange: (segmentIds: string[]) => void
|
||||
onCancelBatchOperation: () => void
|
||||
clearSelection: () => void
|
||||
}
|
||||
|
||||
export const useSegmentSelection = (segments: SegmentDetailModel[]): UseSegmentSelectionReturn => {
|
||||
type MergeCurrentPageSelectedSegmentIdsOptions = {
|
||||
selectedSegmentIds: string[]
|
||||
currentPageSegmentIds: string[]
|
||||
nextCurrentPageSelectedSegmentIds: string[]
|
||||
}
|
||||
|
||||
export const mergeCurrentPageSelectedSegmentIds = ({
|
||||
selectedSegmentIds,
|
||||
currentPageSegmentIds,
|
||||
nextCurrentPageSelectedSegmentIds,
|
||||
}: MergeCurrentPageSelectedSegmentIdsOptions) => {
|
||||
const currentPageSegmentIdSet = new Set(currentPageSegmentIds)
|
||||
const selectedSegmentIdsOutsideCurrentPage = selectedSegmentIds.filter(segmentId => !currentPageSegmentIdSet.has(segmentId))
|
||||
|
||||
return [
|
||||
...selectedSegmentIdsOutsideCurrentPage,
|
||||
...nextCurrentPageSelectedSegmentIds,
|
||||
]
|
||||
}
|
||||
|
||||
export const useSegmentSelection = (): UseSegmentSelectionReturn => {
|
||||
const [selectedSegmentIds, setSelectedSegmentIds] = useState<string[]>([])
|
||||
|
||||
const onSelected = useCallback((segId: string) => {
|
||||
setSelectedSegmentIds(prev =>
|
||||
prev.includes(segId)
|
||||
? prev.filter(id => id !== segId)
|
||||
: [...prev, segId],
|
||||
)
|
||||
const onSelectedSegmentIdsChange = useCallback((segmentIds: string[]) => {
|
||||
setSelectedSegmentIds(segmentIds)
|
||||
}, [])
|
||||
|
||||
const isAllSelected = useMemo(() => {
|
||||
return segments.length > 0 && segments.every(seg => selectedSegmentIds.includes(seg.id))
|
||||
}, [segments, selectedSegmentIds])
|
||||
|
||||
const isSomeSelected = useMemo(() => {
|
||||
return segments.some(seg => selectedSegmentIds.includes(seg.id))
|
||||
}, [segments, selectedSegmentIds])
|
||||
|
||||
const onSelectedAll = useCallback(() => {
|
||||
setSelectedSegmentIds((prev) => {
|
||||
const currentAllSegIds = segments.map(seg => seg.id)
|
||||
const prevSelectedIds = prev.filter(item => !currentAllSegIds.includes(item))
|
||||
return [...prevSelectedIds, ...(isAllSelected ? [] : currentAllSegIds)]
|
||||
})
|
||||
}, [segments, isAllSelected])
|
||||
|
||||
const onCancelBatchOperation = useCallback(() => {
|
||||
setSelectedSegmentIds([])
|
||||
}, [])
|
||||
@ -48,10 +44,7 @@ export const useSegmentSelection = (segments: SegmentDetailModel[]): UseSegmentS
|
||||
|
||||
return {
|
||||
selectedSegmentIds,
|
||||
isAllSelected,
|
||||
isSomeSelected,
|
||||
onSelected,
|
||||
onSelectedAll,
|
||||
onSelectedSegmentIdsChange,
|
||||
onCancelBatchOperation,
|
||||
clearSelection,
|
||||
}
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
import type { FC } from 'react'
|
||||
import type { SegmentListContextValue } from './segment-list-context'
|
||||
import type { SegmentImportStatus } from '@/types/dataset'
|
||||
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import {
|
||||
@ -17,6 +19,7 @@ import { DrawerGroup } from './components/drawer-group'
|
||||
import MenuBar from './components/menu-bar'
|
||||
import { FullDocModeContent, GeneralModeContent } from './components/segment-list-content'
|
||||
import {
|
||||
mergeCurrentPageSelectedSegmentIds,
|
||||
useChildSegmentData,
|
||||
useModalState,
|
||||
useSearchFilter,
|
||||
@ -49,6 +52,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
importStatus,
|
||||
archived,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docForm = useDocumentContext(s => s.docForm)
|
||||
|
||||
// Pagination state
|
||||
@ -65,8 +69,8 @@ const Completed: FC<ICompletedProps> = ({
|
||||
onNewSegmentModalChange,
|
||||
})
|
||||
|
||||
// Selection state (need segments first, so we use a placeholder initially)
|
||||
const [segmentsForSelection, setSegmentsForSelection] = useState<string[]>([])
|
||||
// Selection state
|
||||
const selectionState = useSegmentSelection()
|
||||
|
||||
// Invalidation hooks for child segment data
|
||||
const invalidChunkListAll = useInvalid(useChunkListAllKey)
|
||||
@ -95,21 +99,29 @@ const Completed: FC<ICompletedProps> = ({
|
||||
const segmentListDataHook = useSegmentListData({
|
||||
searchValue: searchFilter.searchValue,
|
||||
selectedStatus: searchFilter.selectedStatus,
|
||||
selectedSegmentIds: segmentsForSelection,
|
||||
selectedSegmentIds: selectionState.selectedSegmentIds,
|
||||
importStatus,
|
||||
currentPage,
|
||||
limit,
|
||||
onCloseSegmentDetail: modalState.onCloseSegmentDetail,
|
||||
clearSelection: () => setSegmentsForSelection([]),
|
||||
clearSelection: selectionState.clearSelection,
|
||||
})
|
||||
|
||||
// Selection state (with actual segments)
|
||||
const selectionState = useSegmentSelection(segmentListDataHook.segments)
|
||||
|
||||
// Sync selection state for segment list data hook
|
||||
useMemo(() => {
|
||||
setSegmentsForSelection(selectionState.selectedSegmentIds)
|
||||
}, [selectionState.selectedSegmentIds])
|
||||
const segmentIds = useMemo(
|
||||
() => segmentListDataHook.segments.map(segment => segment.id),
|
||||
[segmentListDataHook.segments],
|
||||
)
|
||||
const currentPageSegmentIdSet = useMemo(() => new Set(segmentIds), [segmentIds])
|
||||
const currentPageSelectedSegmentIds = useMemo(() => {
|
||||
return selectionState.selectedSegmentIds.filter(segmentId => currentPageSegmentIdSet.has(segmentId))
|
||||
}, [currentPageSegmentIdSet, selectionState.selectedSegmentIds])
|
||||
const handleCurrentPageSelectedSegmentIdsChange = useCallback((nextCurrentPageSelectedSegmentIds: string[]) => {
|
||||
selectionState.onSelectedSegmentIdsChange(mergeCurrentPageSelectedSegmentIds({
|
||||
selectedSegmentIds: selectionState.selectedSegmentIds,
|
||||
currentPageSegmentIds: segmentIds,
|
||||
nextCurrentPageSelectedSegmentIds,
|
||||
}))
|
||||
}, [segmentIds, selectionState.selectedSegmentIds, selectionState.onSelectedSegmentIdsChange])
|
||||
|
||||
// Child segment data
|
||||
const childSegmentDataHook = useChildSegmentData({
|
||||
@ -153,24 +165,6 @@ const Completed: FC<ICompletedProps> = ({
|
||||
|
||||
return (
|
||||
<SegmentListContext.Provider value={contextValue}>
|
||||
{/* Menu Bar */}
|
||||
{!segmentListDataHook.isFullDocMode && (
|
||||
<MenuBar
|
||||
isAllSelected={selectionState.isAllSelected}
|
||||
isSomeSelected={selectionState.isSomeSelected}
|
||||
onSelectedAll={selectionState.onSelectedAll}
|
||||
isLoading={segmentListDataHook.isLoadingSegmentList}
|
||||
totalText={segmentListDataHook.totalText}
|
||||
statusList={searchFilter.statusList}
|
||||
selectDefaultValue={searchFilter.selectDefaultValue}
|
||||
onChangeStatus={searchFilter.onChangeStatus}
|
||||
inputValue={searchFilter.inputValue}
|
||||
onInputChange={searchFilter.handleInputChange}
|
||||
isCollapsed={modalState.isCollapsed}
|
||||
toggleCollapsed={modalState.toggleCollapsed}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Segment list */}
|
||||
{segmentListDataHook.isFullDocMode
|
||||
? (
|
||||
@ -192,22 +186,40 @@ const Completed: FC<ICompletedProps> = ({
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<GeneralModeContent
|
||||
segmentListRef={segmentListDataHook.segmentListRef}
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
isLoadingSegmentList={segmentListDataHook.isLoadingSegmentList}
|
||||
segments={segmentListDataHook.segments}
|
||||
selectedSegmentIds={selectionState.selectedSegmentIds}
|
||||
onSelected={selectionState.onSelected}
|
||||
onChangeSwitch={segmentListDataHook.onChangeSwitch}
|
||||
onDelete={segmentListDataHook.onDelete}
|
||||
onClickCard={modalState.onClickCard}
|
||||
archived={archived}
|
||||
onDeleteChildChunk={childSegmentDataHook.onDeleteChildChunk}
|
||||
handleAddNewChildChunk={modalState.handleAddNewChildChunk}
|
||||
onClickSlice={modalState.onClickSlice}
|
||||
onClearFilter={searchFilter.onClearFilter}
|
||||
/>
|
||||
<CheckboxGroup
|
||||
aria-label={t('segment.chunk', { ns: 'datasetDocuments' })}
|
||||
value={currentPageSelectedSegmentIds}
|
||||
onValueChange={nextSegmentIds => handleCurrentPageSelectedSegmentIdsChange(nextSegmentIds)}
|
||||
allValues={segmentIds}
|
||||
className="flex min-h-0 grow flex-col"
|
||||
>
|
||||
<MenuBar
|
||||
hasSelectableSegments={segmentIds.length > 0}
|
||||
isLoading={segmentListDataHook.isLoadingSegmentList}
|
||||
totalText={segmentListDataHook.totalText}
|
||||
statusList={searchFilter.statusList}
|
||||
selectDefaultValue={searchFilter.selectDefaultValue}
|
||||
onChangeStatus={searchFilter.onChangeStatus}
|
||||
inputValue={searchFilter.inputValue}
|
||||
onInputChange={searchFilter.handleInputChange}
|
||||
isCollapsed={modalState.isCollapsed}
|
||||
toggleCollapsed={modalState.toggleCollapsed}
|
||||
/>
|
||||
<GeneralModeContent
|
||||
segmentListRef={segmentListDataHook.segmentListRef}
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
isLoadingSegmentList={segmentListDataHook.isLoadingSegmentList}
|
||||
segments={segmentListDataHook.segments}
|
||||
onChangeSwitch={segmentListDataHook.onChangeSwitch}
|
||||
onDelete={segmentListDataHook.onDelete}
|
||||
onClickCard={modalState.onClickCard}
|
||||
archived={archived}
|
||||
onDeleteChildChunk={childSegmentDataHook.onDeleteChildChunk}
|
||||
handleAddNewChildChunk={modalState.handleAddNewChildChunk}
|
||||
onClickSlice={modalState.onClickSlice}
|
||||
onClearFilter={searchFilter.onClearFilter}
|
||||
/>
|
||||
</CheckboxGroup>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
|
||||
@ -15,8 +15,6 @@ import ParagraphListSkeleton from './skeleton/paragraph-list-skeleton'
|
||||
type ISegmentListProps = {
|
||||
isLoading: boolean
|
||||
items: SegmentDetailModel[]
|
||||
selectedSegmentIds: string[]
|
||||
onSelected: (segId: string) => void
|
||||
onClick: (detail: SegmentDetailModel, isEditMode?: boolean) => void
|
||||
onChangeSwitch: (enabled: boolean, segId?: string) => Promise<void>
|
||||
onDelete: (segId: string) => Promise<void>
|
||||
@ -33,8 +31,6 @@ const SegmentList = (
|
||||
ref,
|
||||
isLoading,
|
||||
items,
|
||||
selectedSegmentIds,
|
||||
onSelected,
|
||||
onClick: onClickCard,
|
||||
onChangeSwitch,
|
||||
onDelete,
|
||||
@ -84,9 +80,8 @@ const SegmentList = (
|
||||
<Checkbox
|
||||
key={`${segItem.id}-checkbox`}
|
||||
className="mt-3.5 shrink-0"
|
||||
checked={selectedSegmentIds.includes(segItem.id)}
|
||||
value={segItem.id}
|
||||
aria-label={`${t('segment.chunk', { ns: 'datasetDocuments' })} ${segItem.position}`}
|
||||
onCheckedChange={() => onSelected(segItem.id)}
|
||||
/>
|
||||
<div className="min-w-0 grow">
|
||||
<SegmentCard
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@ -28,12 +29,6 @@ const TagsFilter = ({
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const { tags: options, tagsMap } = useTags()
|
||||
const filteredOptions = options.filter(option => option.label.toLowerCase().includes(searchText.toLowerCase()))
|
||||
const handleCheck = (id: string) => {
|
||||
if (tags.includes(id))
|
||||
onTagsChange(tags.filter((tag: string) => tag !== id))
|
||||
else
|
||||
onTagsChange([...tags, id])
|
||||
}
|
||||
const selectedTagsLength = tags.length
|
||||
|
||||
return (
|
||||
@ -85,7 +80,12 @@ const TagsFilter = ({
|
||||
placeholder={t('searchTags', { ns: 'pluginTags' }) || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[448px] overflow-y-auto p-1">
|
||||
<CheckboxGroup
|
||||
aria-label={t('allTags', { ns: 'pluginTags' })}
|
||||
value={tags}
|
||||
onValueChange={nextTags => onTagsChange(nextTags)}
|
||||
className="max-h-[448px] overflow-y-auto p-1"
|
||||
>
|
||||
{
|
||||
filteredOptions.map(option => (
|
||||
<label
|
||||
@ -94,8 +94,7 @@ const TagsFilter = ({
|
||||
>
|
||||
<Checkbox
|
||||
className="mr-1"
|
||||
checked={tags.includes(option.name)}
|
||||
onCheckedChange={() => handleCheck(option.name)}
|
||||
value={option.name}
|
||||
/>
|
||||
<div className="px-1 system-sm-medium text-text-secondary">
|
||||
{option.label}
|
||||
@ -103,7 +102,7 @@ const TagsFilter = ({
|
||||
</label>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CheckboxGroup>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@ -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 CategoriesFilter = ({
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const { categories: options, categoriesMap } = useCategories()
|
||||
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 (
|
||||
@ -105,7 +100,12 @@ const CategoriesFilter = ({
|
||||
placeholder={t('searchCategories', { ns: 'plugin' })}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[448px] overflow-y-auto p-1">
|
||||
<CheckboxGroup
|
||||
aria-label={t('allCategories', { ns: 'plugin' })}
|
||||
value={value}
|
||||
onValueChange={nextValue => onChange(nextValue)}
|
||||
className="max-h-[448px] overflow-y-auto p-1"
|
||||
>
|
||||
{
|
||||
filteredOptions.map(option => (
|
||||
<label
|
||||
@ -114,8 +114,7 @@ const CategoriesFilter = ({
|
||||
>
|
||||
<Checkbox
|
||||
className="mr-1"
|
||||
checked={value.includes(option.name)}
|
||||
onCheckedChange={() => handleCheck(option.name)}
|
||||
value={option.name}
|
||||
/>
|
||||
<div className="px-1 system-sm-medium text-text-secondary">
|
||||
{option.label}
|
||||
@ -123,7 +122,7 @@ const CategoriesFilter = ({
|
||||
</label>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CheckboxGroup>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@ -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' })}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[448px] overflow-y-auto p-1">
|
||||
<CheckboxGroup
|
||||
aria-label={t('allTags', { ns: 'pluginTags' })}
|
||||
value={value}
|
||||
onValueChange={nextValue => onChange(nextValue)}
|
||||
className="max-h-[448px] overflow-y-auto p-1"
|
||||
>
|
||||
{
|
||||
filteredOptions.map(option => (
|
||||
<label
|
||||
@ -112,8 +112,7 @@ const TagsFilter = ({
|
||||
>
|
||||
<Checkbox
|
||||
className="mr-1"
|
||||
checked={value.includes(option.name)}
|
||||
onCheckedChange={() => handleCheck(option.name)}
|
||||
value={option.name}
|
||||
/>
|
||||
<div className="px-1 system-sm-medium text-text-secondary">
|
||||
{option.label}
|
||||
@ -121,7 +120,7 @@ const TagsFilter = ({
|
||||
</label>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CheckboxGroup>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@ -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<LabelSelectorProps> = ({
|
||||
function LabelSelector({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
}: LabelSelectorProps) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@ -39,20 +38,8 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
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 (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
@ -86,7 +73,12 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
onClear={() => handleKeywordsChange('')}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[264px] overflow-y-auto p-1">
|
||||
<CheckboxGroup
|
||||
aria-label={t('createTool.toolInput.labelPlaceholder', { ns: 'tools' })}
|
||||
value={value}
|
||||
onValueChange={nextValue => onChange(nextValue)}
|
||||
className="max-h-[264px] overflow-y-auto p-1"
|
||||
>
|
||||
{filteredLabelList.map(label => (
|
||||
<label
|
||||
key={label.name}
|
||||
@ -94,8 +86,7 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
>
|
||||
<Checkbox
|
||||
className="shrink-0"
|
||||
checked={value.includes(label.name)}
|
||||
onCheckedChange={() => selectLabel(label)}
|
||||
value={label.name}
|
||||
/>
|
||||
<div title={label.label} className="grow truncate text-sm leading-5 text-text-secondary">{label.label}</div>
|
||||
</label>
|
||||
@ -106,7 +97,7 @@ const LabelSelector: FC<LabelSelectorProps> = ({
|
||||
<div className="text-xs leading-[14px] text-text-tertiary">{t('tag.noTag', { ns: 'common' })}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CheckboxGroup>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user