mirror of
https://github.com/langgenius/dify.git
synced 2026-05-25 19:00:43 -04:00
fix(web): migrate metadata picker to combobox (#36255)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2100,16 +2100,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx": {
|
||||
"react/static-components": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/metadata/metadata-dataset/select-metadata.tsx": {
|
||||
"react/static-components": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/metadata/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import AddedMetadataButton from '../add-metadata-button'
|
||||
|
||||
describe('AddedMetadataButton', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<AddedMetadataButton />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with translated text', () => {
|
||||
render(<AddedMetadataButton />)
|
||||
// The button should contain text from i18n
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render add icon', () => {
|
||||
const { container } = render(<AddedMetadataButton />)
|
||||
// Check if there's an SVG element (the RiAddLine icon)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should apply custom className', () => {
|
||||
render(<AddedMetadataButton className="custom-class" />)
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('should apply default classes', () => {
|
||||
render(<AddedMetadataButton />)
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toHaveClass('flex', 'w-full', 'items-center')
|
||||
})
|
||||
|
||||
it('should merge custom className with default classes', () => {
|
||||
render(<AddedMetadataButton className="my-custom-class" />)
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toHaveClass('flex', 'w-full', 'items-center', 'my-custom-class')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onClick when button is clicked', () => {
|
||||
const handleClick = vi.fn()
|
||||
render(<AddedMetadataButton onClick={handleClick} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not throw when onClick is not provided and button is clicked', () => {
|
||||
render(<AddedMetadataButton />)
|
||||
|
||||
expect(() => {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should call onClick multiple times on multiple clicks', () => {
|
||||
const handleClick = vi.fn()
|
||||
render(<AddedMetadataButton onClick={handleClick} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should render with undefined className', () => {
|
||||
render(<AddedMetadataButton className={undefined} />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with empty className', () => {
|
||||
render(<AddedMetadataButton className="" />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with undefined onClick', () => {
|
||||
render(<AddedMetadataButton onClick={undefined} />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const AddedMetadataButton: FC<Props> = ({
|
||||
className,
|
||||
onClick,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Button
|
||||
className={cn('flex w-full items-center', className)}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={onClick}
|
||||
>
|
||||
<RiAddLine className="mr-1 size-3.5" />
|
||||
<div>{t('metadata.addMetadata', { ns: 'dataset' })}</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
export default React.memo(AddedMetadataButton)
|
||||
@@ -56,42 +56,44 @@ type AddRowProps = {
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
type SelectModalProps = {
|
||||
trigger: React.ReactNode
|
||||
onSelect: (item: MetadataItemInBatchEdit) => void
|
||||
onSave: (data: { name: string, type: DataType }) => Promise<void>
|
||||
onManage: () => void
|
||||
}
|
||||
|
||||
// Mock child components with callback exposure
|
||||
// Mock row components to exercise parent state transitions with accessible controls.
|
||||
vi.mock('../edit-row', () => ({
|
||||
default: ({ payload, onChange, onRemove, onReset }: EditRowProps) => (
|
||||
<div data-testid="edit-row" data-id={payload.id}>
|
||||
<span data-testid="edit-row-name">{payload.name}</span>
|
||||
<button data-testid={`change-${payload.id}`} onClick={() => onChange({ ...payload, value: 'changed', isUpdated: true, updateType: UpdateType.changeValue })}>Change</button>
|
||||
<button data-testid={`remove-${payload.id}`} onClick={() => onRemove(payload.id)}>Remove</button>
|
||||
<button data-testid={`reset-${payload.id}`} onClick={() => onReset(payload.id)}>Reset</button>
|
||||
<div role="group" aria-label={`Edit metadata ${payload.name}`}>
|
||||
<span>{payload.name}</span>
|
||||
<button type="button" onClick={() => onChange({ ...payload, value: 'changed', isUpdated: true, updateType: UpdateType.changeValue })}>
|
||||
Change
|
||||
{' '}
|
||||
{payload.name}
|
||||
</button>
|
||||
<button type="button" onClick={() => onRemove(payload.id)}>
|
||||
Remove
|
||||
{' '}
|
||||
{payload.name}
|
||||
</button>
|
||||
<button type="button" onClick={() => onReset(payload.id)}>
|
||||
Reset
|
||||
{' '}
|
||||
{payload.name}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../add-row', () => ({
|
||||
default: ({ payload, onChange, onRemove }: AddRowProps) => (
|
||||
<div data-testid="add-row" data-id={payload.id}>
|
||||
<span data-testid="add-row-name">{payload.name}</span>
|
||||
<button data-testid={`add-change-${payload.id}`} onClick={() => onChange({ ...payload, value: 'new_value' })}>Change</button>
|
||||
<button data-testid="add-remove" onClick={onRemove}>Remove</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../metadata-dataset/select-metadata-modal', () => ({
|
||||
default: ({ trigger, onSelect, onSave, onManage }: SelectModalProps) => (
|
||||
<div data-testid="select-modal">
|
||||
{trigger}
|
||||
<button data-testid="select-metadata" onClick={() => onSelect({ id: 'new-1', name: 'new_field', type: DataType.string, value: null, isMultipleValue: false })}>Select</button>
|
||||
<button data-testid="save-metadata" onClick={() => onSave({ name: 'created_field', type: DataType.string }).catch(() => { })}>Save</button>
|
||||
<button data-testid="manage-metadata" onClick={onManage}>Manage</button>
|
||||
<div role="group" aria-label={`Added metadata ${payload.name}`}>
|
||||
<span>{payload.name}</span>
|
||||
<button type="button" onClick={() => onChange({ ...payload, value: 'new_value' })}>
|
||||
Change
|
||||
{' '}
|
||||
{payload.name}
|
||||
</button>
|
||||
<button type="button" onClick={onRemove}>
|
||||
Remove
|
||||
{' '}
|
||||
{payload.name}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
@@ -116,6 +118,25 @@ describe('EditMetadataBatchModal', () => {
|
||||
mockCheckNameResult = { errorMsg: '' }
|
||||
})
|
||||
|
||||
const getEditRows = () => screen.getAllByRole('group', { name: /^Edit metadata / })
|
||||
const getEditRow = (name: string) => screen.getByRole('group', { name: `Edit metadata ${name}` })
|
||||
const getAddedRow = (name: string) => screen.getByRole('group', { name: `Added metadata ${name}` })
|
||||
const queryAddedRow = (name: string) => screen.queryByRole('group', { name: `Added metadata ${name}` })
|
||||
const openMetadataPicker = () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
}
|
||||
const selectMetadata = async (name = 'existing_field') => {
|
||||
openMetadataPicker()
|
||||
fireEvent.click(await screen.findByRole('option', { name: new RegExp(name) }))
|
||||
}
|
||||
const createMetadata = async (name = 'created_field') => {
|
||||
openMetadataPicker()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.newAction' }))
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'dataset.metadata.createMetadata.name' }), { target: { value: name } })
|
||||
const saveButtons = screen.getAllByRole('button', { name: 'common.operation.save' })
|
||||
fireEvent.click(saveButtons[saveButtons.length - 1]!)
|
||||
}
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', async () => {
|
||||
render(<EditMetadataBatchModal {...defaultProps} />)
|
||||
@@ -134,8 +155,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
it('should render all edit rows for existing items', async () => {
|
||||
render(<EditMetadataBatchModal {...defaultProps} />)
|
||||
await waitFor(() => {
|
||||
const editRows = screen.getAllByTestId('edit-row')
|
||||
expect(editRows).toHaveLength(2)
|
||||
expect(getEditRows()).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -150,15 +170,14 @@ describe('EditMetadataBatchModal', () => {
|
||||
it('should render checkbox for apply to all', async () => {
|
||||
render(<EditMetadataBatchModal {...defaultProps} />)
|
||||
await waitFor(() => {
|
||||
const checkboxes = document.querySelectorAll('[data-testid*="checkbox"]')
|
||||
expect(checkboxes.length).toBeGreaterThan(0)
|
||||
expect(screen.getByRole('checkbox', { name: 'dataset.metadata.batchEditMetadata.applyToAllSelectDocument' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render select metadata modal', async () => {
|
||||
it('should render dataset metadata picker', async () => {
|
||||
render(<EditMetadataBatchModal {...defaultProps} />)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-modal'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -186,7 +205,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Find the primary save button (not the one in SelectMetadataModal)
|
||||
// Find the primary save button (not the one in DatasetMetadataPicker)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(onSave).toHaveBeenCalled()
|
||||
@@ -199,16 +218,12 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
const checkboxContainer = document.querySelector('[data-testid*="checkbox"]')
|
||||
expect(checkboxContainer)!.toBeInTheDocument()
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'dataset.metadata.batchEditMetadata.applyToAllSelectDocument' })
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
if (checkboxContainer) {
|
||||
fireEvent.click(checkboxContainer)
|
||||
await waitFor(() => {
|
||||
const checkIcon = screen.getByTestId('check-icon-apply-to-all')
|
||||
expect(checkIcon)!.toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(checkbox).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onHide when modal close button is clicked', async () => {
|
||||
@@ -229,10 +244,10 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('change-1'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Change field_one' }))
|
||||
|
||||
// The component should update internally
|
||||
expect(screen.getAllByTestId('edit-row').length).toBe(2)
|
||||
expect(getEditRows()).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should mark item as deleted when remove is clicked', async () => {
|
||||
@@ -242,10 +257,10 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('remove-1'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Remove field_one' }))
|
||||
|
||||
// The component should update internally - item marked as deleted
|
||||
expect(screen.getAllByTestId('edit-row').length).toBe(2)
|
||||
expect(getEditRows()).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should reset item when reset is clicked', async () => {
|
||||
@@ -256,11 +271,11 @@ describe('EditMetadataBatchModal', () => {
|
||||
})
|
||||
|
||||
// First change the item
|
||||
fireEvent.click(screen.getByTestId('change-1'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Change field_one' }))
|
||||
// Then reset it
|
||||
fireEvent.click(screen.getByTestId('reset-1'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Reset field_one' }))
|
||||
|
||||
expect(screen.getAllByTestId('edit-row').length).toBe(2)
|
||||
expect(getEditRows()).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -272,11 +287,11 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('select-metadata'))
|
||||
await selectMetadata()
|
||||
|
||||
// Should now have add-row for the new item
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('add-row'))!.toBeInTheDocument()
|
||||
expect(getAddedRow('existing_field'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -288,17 +303,17 @@ describe('EditMetadataBatchModal', () => {
|
||||
})
|
||||
|
||||
// First add an item
|
||||
fireEvent.click(screen.getByTestId('select-metadata'))
|
||||
await selectMetadata()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('add-row'))!.toBeInTheDocument()
|
||||
expect(getAddedRow('existing_field'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Then remove it
|
||||
fireEvent.click(screen.getByTestId('add-remove'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Remove existing_field' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('add-row')).not.toBeInTheDocument()
|
||||
expect(queryAddedRow('existing_field')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -310,16 +325,16 @@ describe('EditMetadataBatchModal', () => {
|
||||
})
|
||||
|
||||
// First add an item
|
||||
fireEvent.click(screen.getByTestId('select-metadata'))
|
||||
await selectMetadata()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('add-row'))!.toBeInTheDocument()
|
||||
expect(getAddedRow('existing_field'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Then change it
|
||||
fireEvent.click(screen.getByTestId('add-change-new-1'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Change existing_field' }))
|
||||
|
||||
expect(screen.getByTestId('add-row'))!.toBeInTheDocument()
|
||||
expect(getAddedRow('existing_field'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call doAddMetaData when saving new metadata with valid name', async () => {
|
||||
@@ -331,7 +346,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('save-metadata'))
|
||||
await createMetadata()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDoAddMetaData).toHaveBeenCalled()
|
||||
@@ -347,7 +362,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('save-metadata'))
|
||||
await createMetadata()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDoAddMetaData).toHaveBeenCalled()
|
||||
@@ -371,7 +386,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('save-metadata'))
|
||||
await createMetadata()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(
|
||||
@@ -391,7 +406,8 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('manage-metadata'))
|
||||
openMetadataPicker()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.manageAction' }))
|
||||
|
||||
expect(onShowManage).toHaveBeenCalled()
|
||||
})
|
||||
@@ -415,7 +431,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
it('should handle empty list', async () => {
|
||||
render(<EditMetadataBatchModal {...defaultProps} list={[]} />)
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('edit-row')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('group', { name: /^Edit metadata / })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -427,7 +443,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
]
|
||||
render(<EditMetadataBatchModal {...defaultProps} list={multipleValueList} />)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('edit-row'))!.toBeInTheDocument()
|
||||
expect(getEditRow('field'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -473,9 +489,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
const checkboxContainer = document.querySelector('[data-testid*="checkbox"]')
|
||||
if (checkboxContainer)
|
||||
fireEvent.click(checkboxContainer)
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: 'dataset.metadata.batchEditMetadata.applyToAllSelectDocument' }))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
@@ -497,7 +511,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
})
|
||||
|
||||
// Remove an item
|
||||
fireEvent.click(screen.getByTestId('remove-1'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Remove field_one' }))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
@@ -516,22 +530,22 @@ describe('EditMetadataBatchModal', () => {
|
||||
})
|
||||
|
||||
// Add first item
|
||||
fireEvent.click(screen.getByTestId('select-metadata'))
|
||||
await selectMetadata()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('add-row'))!.toBeInTheDocument()
|
||||
expect(getAddedRow('existing_field'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Remove it
|
||||
fireEvent.click(screen.getByTestId('add-remove'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Remove existing_field' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('add-row')).not.toBeInTheDocument()
|
||||
expect(queryAddedRow('existing_field')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Add again
|
||||
fireEvent.click(screen.getByTestId('select-metadata'))
|
||||
await selectMetadata()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('add-row'))!.toBeInTheDocument()
|
||||
expect(getAddedRow('existing_field'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,9 +12,8 @@ import Divider from '@/app/components/base/divider'
|
||||
import { useCreateMetaData } from '@/service/knowledge/use-metadata'
|
||||
import Checkbox from '../../../base/checkbox'
|
||||
import { Infotip } from '../../../base/infotip'
|
||||
import AddMetadataButton from '../add-metadata-button'
|
||||
import useCheckMetadataName from '../hooks/use-check-metadata-name'
|
||||
import SelectMetadataModal from '../metadata-dataset/select-metadata-modal'
|
||||
import { DatasetMetadataPicker } from '../metadata-dataset/dataset-metadata-picker'
|
||||
import { UpdateType } from '../types'
|
||||
import AddedMetadataItem from './add-row'
|
||||
import EditMetadataBatchItem from './edit-row'
|
||||
@@ -117,14 +116,27 @@ const EditMetadataBatchModal: FC<Props> = ({ datasetId, documentNum, list, onSav
|
||||
{addedList.map((item, i) => (<AddedMetadataItem key={i} payload={item} onChange={handleAddedListChange} onRemove={handleAddedItemRemove(i)} />))}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<SelectMetadataModal datasetId={datasetId} popupPlacement="top-start" popupOffset={{ mainAxis: 4, crossAxis: 0 }} trigger={<AddMetadataButton />} onSave={handleAddMetaData} onSelect={data => setAddedList([...addedList, data as MetadataItemWithEdit])} onManage={onShowManage} />
|
||||
<DatasetMetadataPicker
|
||||
datasetId={datasetId}
|
||||
placement="top-start"
|
||||
sideOffset={4}
|
||||
alignOffset={0}
|
||||
onCreateMetadata={handleAddMetaData}
|
||||
onSelectMetadata={data => setAddedList([...addedList, data as MetadataItemWithEdit])}
|
||||
onOpenMetadataManagement={onShowManage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="flex items-center select-none">
|
||||
<Checkbox checked={isApplyToAllSelectDocument} onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} id="apply-to-all" />
|
||||
<Checkbox
|
||||
checked={isApplyToAllSelectDocument}
|
||||
onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)}
|
||||
id="apply-to-all"
|
||||
ariaLabel={t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}
|
||||
/>
|
||||
<div className="mr-1 ml-2 system-xs-medium text-text-secondary">{t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}</div>
|
||||
<Infotip
|
||||
aria-label={t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Popover } from '@langgenius/dify-ui/popover'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { DataType } from '../../types'
|
||||
import CreateContent from '../create-content'
|
||||
import { CreateContent } from '../create-content'
|
||||
|
||||
const renderCreateContent = (props: CreateContentProps) => {
|
||||
return render(
|
||||
|
||||
@@ -1,77 +1,10 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { DataType } from '../../types'
|
||||
import CreateMetadataModal from '../create-metadata-modal'
|
||||
|
||||
type PopoverProps = {
|
||||
children: React.ReactNode
|
||||
open: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
type TriggerProps = {
|
||||
children?: React.ReactNode
|
||||
render?: React.ReactNode
|
||||
}
|
||||
|
||||
type ContentProps = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
type CreateContentProps = {
|
||||
onSave: (data: { type: DataType, name: string }) => void
|
||||
onClose?: () => void
|
||||
onBack?: () => void
|
||||
hasBack?: boolean
|
||||
}
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => {
|
||||
const React = await import('react')
|
||||
const PopoverContext = React.createContext<{ open: boolean, onOpenChange?: (open: boolean) => void } | null>(null)
|
||||
|
||||
return {
|
||||
Popover: ({ children, open, onOpenChange }: PopoverProps) => (
|
||||
<PopoverContext.Provider value={{ open, onOpenChange }}>
|
||||
<div data-testid="popover-root" data-open={String(open)}>{children}</div>
|
||||
</PopoverContext.Provider>
|
||||
),
|
||||
PopoverTrigger: ({ children, render }: TriggerProps) => {
|
||||
const context = React.useContext(PopoverContext)
|
||||
const content = render ?? children
|
||||
const handleClick = () => context?.onOpenChange?.(!context.open)
|
||||
|
||||
if (React.isValidElement(content)) {
|
||||
const element = content as React.ReactElement<{ onClick?: () => void }>
|
||||
return React.cloneElement(element, { onClick: handleClick })
|
||||
}
|
||||
|
||||
return <button type="button" data-testid="popover-trigger" onClick={handleClick}>{content}</button>
|
||||
},
|
||||
PopoverContent: ({ children, className }: ContentProps) => {
|
||||
const context = React.useContext(PopoverContext)
|
||||
if (!context?.open)
|
||||
return null
|
||||
|
||||
return <div data-testid="popover-content" className={className}>{children}</div>
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Mock CreateContent component
|
||||
vi.mock('../create-content', () => ({
|
||||
default: ({ onSave, onClose, onBack, hasBack }: CreateContentProps) => (
|
||||
<div data-testid="create-content">
|
||||
<span data-testid="has-back">{String(hasBack)}</span>
|
||||
<button data-testid="save-btn" onClick={() => onSave({ type: DataType.string, name: 'test' })}>Save</button>
|
||||
<button data-testid="close-btn" onClick={onClose}>Close</button>
|
||||
{hasBack && <button data-testid="back-btn" onClick={onBack}>Back</button>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
import { CreateMetadataModal } from '../create-metadata-modal'
|
||||
|
||||
describe('CreateMetadataModal', () => {
|
||||
const mockTrigger = <button data-testid="trigger-button">Open Modal</button>
|
||||
const mockTrigger = <button>Open Modal</button>
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render trigger when closed', () => {
|
||||
@@ -83,8 +16,8 @@ describe('CreateMetadataModal', () => {
|
||||
onSave={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('popover-root')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('popover-root')).toHaveAttribute('data-open', 'false')
|
||||
expect(screen.getByRole('button', { name: 'Open Modal' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render content when open', () => {
|
||||
@@ -96,8 +29,8 @@ describe('CreateMetadataModal', () => {
|
||||
onSave={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('popover-root')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('create-content')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox', { name: 'dataset.metadata.createMetadata.name' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render trigger element', () => {
|
||||
@@ -109,7 +42,7 @@ describe('CreateMetadataModal', () => {
|
||||
onSave={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('trigger-button')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Open Modal' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -124,7 +57,7 @@ describe('CreateMetadataModal', () => {
|
||||
hasBack
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('has-back')).toHaveTextContent('true')
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.createMetadata.back' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass hasBack=undefined when not provided', () => {
|
||||
@@ -136,7 +69,7 @@ describe('CreateMetadataModal', () => {
|
||||
onSave={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('has-back')).toHaveTextContent('undefined')
|
||||
expect(screen.queryByRole('button', { name: 'dataset.metadata.createMetadata.back' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should accept custom popupLeft', () => {
|
||||
@@ -149,7 +82,7 @@ describe('CreateMetadataModal', () => {
|
||||
popupLeft={50}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('popover-root')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -165,9 +98,9 @@ describe('CreateMetadataModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('trigger-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open Modal' }))
|
||||
|
||||
expect(setOpen).toHaveBeenCalledWith(true)
|
||||
expect(setOpen).toHaveBeenCalledWith(true, expect.any(Object))
|
||||
})
|
||||
|
||||
it('should call onSave when save button is clicked', () => {
|
||||
@@ -181,7 +114,10 @@ describe('CreateMetadataModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('save-btn'))
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'dataset.metadata.createMetadata.name' }), {
|
||||
target: { value: 'test' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(handleSave).toHaveBeenCalledWith({
|
||||
type: DataType.string,
|
||||
@@ -200,7 +136,7 @@ describe('CreateMetadataModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('close-btn'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
|
||||
expect(setOpen).toHaveBeenCalledWith(false)
|
||||
})
|
||||
@@ -217,7 +153,7 @@ describe('CreateMetadataModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('back-btn'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.createMetadata.back' }))
|
||||
|
||||
expect(setOpen).toHaveBeenCalledWith(false)
|
||||
})
|
||||
@@ -234,7 +170,7 @@ describe('CreateMetadataModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('popover-root')).toHaveAttribute('data-open', 'false')
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<CreateMetadataModal
|
||||
@@ -245,11 +181,11 @@ describe('CreateMetadataModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('popover-root')).toHaveAttribute('data-open', 'true')
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle different trigger elements', () => {
|
||||
const customTrigger = <div data-testid="custom-trigger">Custom</div>
|
||||
const customTrigger = <button>Custom</button>
|
||||
render(
|
||||
<CreateMetadataModal
|
||||
open={false}
|
||||
@@ -259,7 +195,7 @@ describe('CreateMetadataModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('custom-trigger')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Custom' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -35,31 +35,6 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Type definitions for mock props
|
||||
type CreateModalProps = {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
trigger: React.ReactNode
|
||||
onSave: (data: BuiltInMetadataItem) => void
|
||||
}
|
||||
|
||||
// Mock CreateModal to expose callbacks
|
||||
vi.mock('@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal', () => ({
|
||||
default: ({ open, setOpen, trigger, onSave }: CreateModalProps) => (
|
||||
<div data-testid="create-modal-wrapper">
|
||||
<div data-testid="create-trigger" onClick={() => setOpen(true)}>{trigger}</div>
|
||||
{open && (
|
||||
<div data-testid="create-modal">
|
||||
<button data-testid="create-save" onClick={() => onSave({ name: 'new_field', type: DataType.string })}>
|
||||
Save
|
||||
</button>
|
||||
<button data-testid="create-close" onClick={() => setOpen(false)}>Close</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('DatasetMetadataDrawer', () => {
|
||||
const mockUserMetadata: MetadataItemWithValueLength[] = [
|
||||
{ id: '1', name: 'field_one', type: DataType.string, count: 5 },
|
||||
@@ -90,6 +65,20 @@ describe('DatasetMetadataDrawer', () => {
|
||||
fireEvent.click(screen.getAllByRole('button', { name })[0]!)
|
||||
}
|
||||
|
||||
const openCreateMetadata = async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.datasetMetadata.addMetaData' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox', { name: 'dataset.metadata.createMetadata.name' })).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
|
||||
const saveCreatedMetadata = (name = 'new_field') => {
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'dataset.metadata.createMetadata.name' }), {
|
||||
target: { value: name },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
}
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', async () => {
|
||||
render(<DatasetMetadataDrawer {...defaultProps} />)
|
||||
@@ -125,7 +114,7 @@ describe('DatasetMetadataDrawer', () => {
|
||||
it('should render add metadata button', async () => {
|
||||
render(<DatasetMetadataDrawer {...defaultProps} />)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('create-trigger'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.datasetMetadata.addMetaData' }))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -180,12 +169,9 @@ describe('DatasetMetadataDrawer', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
const trigger = screen.getByTestId('create-trigger')
|
||||
fireEvent.click(trigger)
|
||||
await openCreateMetadata()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('create-modal'))!.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByRole('textbox', { name: 'dataset.metadata.createMetadata.name' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onAdd and show success toast when metadata is added', async () => {
|
||||
@@ -197,15 +183,10 @@ describe('DatasetMetadataDrawer', () => {
|
||||
})
|
||||
|
||||
// Open create modal
|
||||
const trigger = screen.getByTestId('create-trigger')
|
||||
fireEvent.click(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('create-modal'))!.toBeInTheDocument()
|
||||
})
|
||||
await openCreateMetadata()
|
||||
|
||||
// Save new metadata
|
||||
fireEvent.click(screen.getByTestId('create-save'))
|
||||
saveCreatedMetadata()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onAdd).toHaveBeenCalled()
|
||||
@@ -229,16 +210,12 @@ describe('DatasetMetadataDrawer', () => {
|
||||
})
|
||||
|
||||
// Open create modal
|
||||
fireEvent.click(screen.getByTestId('create-trigger'))
|
||||
await openCreateMetadata()
|
||||
|
||||
saveCreatedMetadata()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('create-modal'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('create-save'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('create-modal')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('textbox', { name: 'dataset.metadata.createMetadata.name' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -276,7 +253,7 @@ describe('DatasetMetadataDrawer', () => {
|
||||
expect(inputs.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
const input = screen.getByPlaceholderText('dataset.metadata.datasetMetadata.namePlaceholder')
|
||||
const input = screen.getByRole('textbox', { name: 'dataset.metadata.datasetMetadata.name' })
|
||||
fireEvent.change(input, { target: { value: 'renamed_field' } })
|
||||
|
||||
// Find and click save button
|
||||
@@ -311,7 +288,7 @@ describe('DatasetMetadataDrawer', () => {
|
||||
})
|
||||
|
||||
// Change name first
|
||||
const input = screen.getByPlaceholderText('dataset.metadata.datasetMetadata.namePlaceholder')
|
||||
const input = screen.getByRole('textbox', { name: 'dataset.metadata.datasetMetadata.name' })
|
||||
fireEvent.change(input, { target: { value: 'changed_name' } })
|
||||
|
||||
// Find and click cancel button
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import type { MetadataItem } from '../../types'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DataType } from '../../types'
|
||||
import { DatasetMetadataPicker } from '../dataset-metadata-picker'
|
||||
|
||||
const { mockUseDatasetMetaData } = vi.hoisted(() => ({
|
||||
mockUseDatasetMetaData: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/knowledge/use-metadata', () => ({
|
||||
useDatasetMetaData: mockUseDatasetMetaData,
|
||||
}))
|
||||
|
||||
const metadataItems: MetadataItem[] = [
|
||||
{ id: '1', name: 'field_one', type: DataType.string },
|
||||
{ id: '2', name: 'field_two', type: DataType.number },
|
||||
{ id: '3', name: 'field_three', type: DataType.time },
|
||||
]
|
||||
|
||||
function renderDatasetMetadataPicker(overrides: Partial<React.ComponentProps<typeof DatasetMetadataPicker>> = {}) {
|
||||
const props = {
|
||||
datasetId: 'dataset-1',
|
||||
onSelectMetadata: vi.fn(),
|
||||
onCreateMetadata: vi.fn(),
|
||||
onOpenMetadataManagement: vi.fn(),
|
||||
...overrides,
|
||||
}
|
||||
|
||||
return {
|
||||
props,
|
||||
...render(<DatasetMetadataPicker {...props} />),
|
||||
}
|
||||
}
|
||||
|
||||
describe('DatasetMetadataPicker', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseDatasetMetaData.mockReturnValue({
|
||||
data: {
|
||||
doc_metadata: metadataItems,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render an add metadata picker trigger', () => {
|
||||
renderDatasetMetadataPicker()
|
||||
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show metadata options when opened', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderDatasetMetadataPicker()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
|
||||
expect(await screen.findByRole('option', { name: /field_one/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('option', { name: /field_two/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('option', { name: /field_three/ })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Search', () => {
|
||||
it('should filter metadata options by search query', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderDatasetMetadataPicker()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
await user.type(screen.getByRole('combobox', { name: 'dataset.metadata.selectMetadata.search' }), 'two')
|
||||
|
||||
expect(screen.getByRole('option', { name: /field_two/ })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('option', { name: /field_one/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('option', { name: /field_three/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show an empty state when no metadata matches', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderDatasetMetadataPicker()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
await user.type(screen.getByRole('combobox', { name: 'dataset.metadata.selectMetadata.search' }), 'missing')
|
||||
|
||||
expect(await screen.findByRole('status')).toHaveTextContent('common.noData')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Selection', () => {
|
||||
it('should call onSelectMetadata and close when an option is selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelectMetadata = vi.fn()
|
||||
renderDatasetMetadataPicker({ onSelectMetadata })
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
await user.click(await screen.findByRole('option', { name: /field_two/ }))
|
||||
|
||||
expect(onSelectMetadata).toHaveBeenCalledWith({
|
||||
id: '2',
|
||||
name: 'field_two',
|
||||
type: DataType.number,
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' })).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Actions', () => {
|
||||
it('should switch to create view and save a new metadata item', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onCreateMetadata = vi.fn().mockResolvedValue(undefined)
|
||||
renderDatasetMetadataPicker({ onCreateMetadata })
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.newAction' }))
|
||||
await user.type(screen.getByRole('textbox', { name: 'dataset.metadata.createMetadata.name' }), 'new_field')
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(onCreateMetadata).toHaveBeenCalledWith({
|
||||
name: 'new_field',
|
||||
type: DataType.string,
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('combobox', { name: 'dataset.metadata.selectMetadata.search' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should return from create view without closing the picker', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderDatasetMetadataPicker()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.newAction' }))
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.createMetadata.back' }))
|
||||
|
||||
expect(screen.getByRole('combobox', { name: 'dataset.metadata.selectMetadata.search' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' })).toHaveAttribute('aria-expanded', 'true')
|
||||
})
|
||||
|
||||
it('should open metadata management and close the picker', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenMetadataManagement = vi.fn()
|
||||
renderDatasetMetadataPicker({ onOpenMetadataManagement })
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.manageAction' }))
|
||||
|
||||
expect(onOpenMetadataManagement).toHaveBeenCalled()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' })).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should keep action buttons available when metadata list is empty', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockUseDatasetMetaData.mockReturnValue({
|
||||
data: {
|
||||
doc_metadata: [],
|
||||
},
|
||||
})
|
||||
|
||||
renderDatasetMetadataPicker()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
|
||||
expect(await screen.findByRole('status')).toHaveTextContent('common.noData')
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.newAction' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.manageAction' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,374 +0,0 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { DataType } from '../../types'
|
||||
import SelectMetadataModal from '../select-metadata-modal'
|
||||
|
||||
type MetadataItem = {
|
||||
id: string
|
||||
name: string
|
||||
type: DataType
|
||||
}
|
||||
|
||||
type PopoverProps = {
|
||||
children: React.ReactNode
|
||||
open: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
type TriggerProps = {
|
||||
children?: React.ReactNode
|
||||
render?: React.ReactNode
|
||||
}
|
||||
|
||||
type ContentProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
type SelectMetadataProps = {
|
||||
onSelect: (item: MetadataItem) => void
|
||||
onNew: () => void
|
||||
onManage: () => void
|
||||
list: MetadataItem[]
|
||||
}
|
||||
|
||||
type CreateContentProps = {
|
||||
onSave: (data: { type: DataType, name: string }) => void
|
||||
onBack?: () => void
|
||||
onClose?: () => void
|
||||
hasBack?: boolean
|
||||
}
|
||||
|
||||
// Mock useDatasetMetaData hook
|
||||
vi.mock('@/service/knowledge/use-metadata', () => ({
|
||||
useDatasetMetaData: () => ({
|
||||
data: {
|
||||
doc_metadata: [
|
||||
{ id: '1', name: 'field_one', type: DataType.string },
|
||||
{ id: '2', name: 'field_two', type: DataType.number },
|
||||
],
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => {
|
||||
const React = await import('react')
|
||||
const PopoverContext = React.createContext<{ open: boolean, onOpenChange?: (open: boolean) => void } | null>(null)
|
||||
|
||||
return {
|
||||
Popover: ({ children, open, onOpenChange }: PopoverProps) => (
|
||||
<PopoverContext.Provider value={{ open, onOpenChange }}>
|
||||
<div data-testid="popover-root" data-open={String(open)}>{children}</div>
|
||||
</PopoverContext.Provider>
|
||||
),
|
||||
PopoverTrigger: ({ children, render }: TriggerProps) => {
|
||||
const context = React.useContext(PopoverContext)
|
||||
const content = render ?? children
|
||||
const handleClick = () => context?.onOpenChange?.(!context.open)
|
||||
|
||||
if (React.isValidElement(content)) {
|
||||
const element = content as React.ReactElement<{ onClick?: () => void }>
|
||||
return React.cloneElement(element, { onClick: handleClick })
|
||||
}
|
||||
|
||||
return <button type="button" data-testid="popover-trigger" onClick={handleClick}>{content}</button>
|
||||
},
|
||||
PopoverContent: ({ children }: ContentProps) => {
|
||||
const context = React.useContext(PopoverContext)
|
||||
if (!context?.open)
|
||||
return null
|
||||
|
||||
return <div data-testid="popover-content">{children}</div>
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Mock SelectMetadata component
|
||||
vi.mock('../select-metadata', () => ({
|
||||
default: ({ onSelect, onNew, onManage, list }: SelectMetadataProps) => (
|
||||
<div data-testid="select-metadata">
|
||||
<span data-testid="list-count">{list?.length || 0}</span>
|
||||
<button data-testid="select-item" onClick={() => onSelect({ id: '1', name: 'field_one', type: DataType.string })}>Select</button>
|
||||
<button data-testid="new-btn" onClick={onNew}>New</button>
|
||||
<button data-testid="manage-btn" onClick={onManage}>Manage</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock CreateContent component
|
||||
vi.mock('../create-content', () => ({
|
||||
default: ({ onSave, onBack, onClose, hasBack }: CreateContentProps) => (
|
||||
<div data-testid="create-content">
|
||||
<button data-testid="save-btn" onClick={() => onSave({ type: DataType.string, name: 'new_field' })}>Save</button>
|
||||
{hasBack && <button data-testid="back-btn" onClick={onBack}>Back</button>}
|
||||
<button data-testid="close-btn" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('SelectMetadataModal', () => {
|
||||
const mockTrigger = <button data-testid="trigger-button">Select Metadata</button>
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('popover-root')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render trigger element', () => {
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('trigger-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render SelectMetadata before opening', () => {
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByTestId('select-metadata')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass dataset metadata to SelectMetadata', () => {
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
fireEvent.click(screen.getByTestId('trigger-button'))
|
||||
expect(screen.getByTestId('list-count')).toHaveTextContent('2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should toggle open state when trigger is clicked', () => {
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('trigger-button'))
|
||||
|
||||
expect(screen.getByTestId('popover-root')).toHaveAttribute('data-open', 'true')
|
||||
expect(screen.getByTestId('select-metadata')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onSelect and close when item is selected', () => {
|
||||
const handleSelect = vi.fn()
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={handleSelect}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('trigger-button'))
|
||||
fireEvent.click(screen.getByTestId('select-item'))
|
||||
|
||||
expect(handleSelect).toHaveBeenCalledWith({
|
||||
id: '1',
|
||||
name: 'field_one',
|
||||
type: DataType.string,
|
||||
})
|
||||
})
|
||||
|
||||
it('should switch to create step when new button is clicked', async () => {
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('trigger-button'))
|
||||
fireEvent.click(screen.getByTestId('new-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('create-content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onManage when manage button is clicked', () => {
|
||||
const handleManage = vi.fn()
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={handleManage}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('trigger-button'))
|
||||
fireEvent.click(screen.getByTestId('manage-btn'))
|
||||
|
||||
expect(handleManage).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Create Flow', () => {
|
||||
it('should switch back to select when back is clicked in create step', async () => {
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Go to create step
|
||||
fireEvent.click(screen.getByTestId('trigger-button'))
|
||||
fireEvent.click(screen.getByTestId('new-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('create-content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Go back to select step
|
||||
fireEvent.click(screen.getByTestId('back-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-metadata')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onSave and return to select step when save is clicked', async () => {
|
||||
const handleSave = vi.fn().mockResolvedValue(undefined)
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={handleSave}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Go to create step
|
||||
fireEvent.click(screen.getByTestId('trigger-button'))
|
||||
fireEvent.click(screen.getByTestId('new-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('create-content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Save new metadata
|
||||
fireEvent.click(screen.getByTestId('save-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleSave).toHaveBeenCalledWith({
|
||||
type: DataType.string,
|
||||
name: 'new_field',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should accept custom popupPlacement', () => {
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
popupPlacement="bottom-start"
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('popover-root')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should accept custom popupOffset', () => {
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
popupOffset={{ mainAxis: 10, crossAxis: 5 }}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('popover-root')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle different datasetIds', () => {
|
||||
const { rerender } = render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('popover-root')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-2"
|
||||
trigger={mockTrigger}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('popover-root')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty trigger', () => {
|
||||
render(
|
||||
<SelectMetadataModal
|
||||
datasetId="dataset-1"
|
||||
trigger={<span data-testid="empty-trigger" />}
|
||||
onSelect={vi.fn()}
|
||||
onSave={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('empty-trigger')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,326 +0,0 @@
|
||||
import type { MetadataItem } from '../../types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { DataType } from '../../types'
|
||||
import SelectMetadata from '../select-metadata'
|
||||
|
||||
type IconProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
// Mock getIcon utility
|
||||
vi.mock('../../utils/get-icon', () => ({
|
||||
getIcon: () => (props: IconProps) => <span data-testid="icon" className={props.className}>Icon</span>,
|
||||
}))
|
||||
|
||||
describe('SelectMetadata', () => {
|
||||
const mockList: MetadataItem[] = [
|
||||
{ id: '1', name: 'field_one', type: DataType.string },
|
||||
{ id: '2', name: 'field_two', type: DataType.number },
|
||||
{ id: '3', name: 'field_three', type: DataType.time },
|
||||
]
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const { container } = render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render search input', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render all metadata items', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('field_one')).toBeInTheDocument()
|
||||
expect(screen.getByText('field_two')).toBeInTheDocument()
|
||||
expect(screen.getByText('field_three')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render new action button', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.newAction' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render manage action button', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.manageAction' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display type for each item', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getAllByText(DataType.string).length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByText(DataType.number).length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByText(DataType.time).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Search Functionality', () => {
|
||||
it('should filter items based on search query', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const searchInput = screen.getByRole('textbox')
|
||||
fireEvent.change(searchInput, { target: { value: 'one' } })
|
||||
|
||||
expect(screen.getByText('field_one')).toBeInTheDocument()
|
||||
expect(screen.queryByText('field_two')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('field_three')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should be case insensitive search', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const searchInput = screen.getByRole('textbox')
|
||||
fireEvent.change(searchInput, { target: { value: 'ONE' } })
|
||||
|
||||
expect(screen.getByText('field_one')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show all items when search is cleared', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const searchInput = screen.getByRole('textbox')
|
||||
|
||||
// Search for something
|
||||
fireEvent.change(searchInput, { target: { value: 'one' } })
|
||||
expect(screen.queryByText('field_two')).not.toBeInTheDocument()
|
||||
|
||||
// Clear search
|
||||
fireEvent.change(searchInput, { target: { value: '' } })
|
||||
expect(screen.getByText('field_two')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show no results when search matches nothing', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const searchInput = screen.getByRole('textbox')
|
||||
fireEvent.change(searchInput, { target: { value: 'xyz' } })
|
||||
|
||||
expect(screen.queryByText('field_one')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('field_two')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('field_three')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onSelect with item data when item is clicked', () => {
|
||||
const handleSelect = vi.fn()
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={handleSelect}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /field_one/i }))
|
||||
|
||||
expect(handleSelect).toHaveBeenCalledWith({
|
||||
id: '1',
|
||||
name: 'field_one',
|
||||
type: DataType.string,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onNew when new button is clicked', () => {
|
||||
const handleNew = vi.fn()
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={handleNew}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.newAction' }))
|
||||
|
||||
expect(handleNew).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onManage when manage button is clicked', () => {
|
||||
const handleManage = vi.fn()
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={handleManage}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.manageAction' }))
|
||||
|
||||
expect(handleManage).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Empty State', () => {
|
||||
it('should render empty list', () => {
|
||||
const { container } = render(
|
||||
<SelectMetadata
|
||||
list={[]}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should still show new and manage buttons with empty list', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={[]}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.newAction' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.manageAction' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have correct container styling', () => {
|
||||
const { container } = render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(container.firstChild).toHaveClass('w-[320px]', 'rounded-xl')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle single item list', () => {
|
||||
const singleItem: MetadataItem[] = [
|
||||
{ id: '1', name: 'only_one', type: DataType.string },
|
||||
]
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={singleItem}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('only_one')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle item with long name', () => {
|
||||
const longNameItem: MetadataItem[] = [
|
||||
{ id: '1', name: 'this_is_a_very_long_field_name_that_might_overflow', type: DataType.string },
|
||||
]
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={longNameItem}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('this_is_a_very_long_field_name_that_might_overflow')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle rapid search input changes', () => {
|
||||
render(
|
||||
<SelectMetadata
|
||||
list={mockList}
|
||||
onSelect={vi.fn()}
|
||||
onNew={vi.fn()}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const searchInput = screen.getByRole('textbox')
|
||||
|
||||
// Rapid typing
|
||||
fireEvent.change(searchInput, { target: { value: 'f' } })
|
||||
fireEvent.change(searchInput, { target: { value: 'fi' } })
|
||||
fireEvent.change(searchInput, { target: { value: 'fie' } })
|
||||
fireEvent.change(searchInput, { target: { value: 'fiel' } })
|
||||
fireEvent.change(searchInput, { target: { value: 'field' } })
|
||||
|
||||
expect(screen.getByText('field_one')).toBeInTheDocument()
|
||||
expect(screen.getByText('field_two')).toBeInTheDocument()
|
||||
expect(screen.getByText('field_three')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { BuiltInMetadataItem } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { PopoverTitle } from '@langgenius/dify-ui/popover'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
@@ -21,12 +18,12 @@ export type Props = {
|
||||
onBack?: () => void
|
||||
}
|
||||
|
||||
const CreateContent: FC<Props> = ({
|
||||
export function CreateContent({
|
||||
onClose = noop,
|
||||
hasBack,
|
||||
onBack,
|
||||
onSave,
|
||||
}) => {
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
const [type, setType] = useState(DataType.string)
|
||||
|
||||
@@ -46,7 +43,7 @@ const CreateContent: FC<Props> = ({
|
||||
}, [onSave, type, name])
|
||||
|
||||
return (
|
||||
<div className="w-[320px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg px-3 pt-3.5 pb-4 shadow-xl">
|
||||
<div className="px-3 pt-3.5 pb-4">
|
||||
{hasBack && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -58,9 +55,9 @@ const CreateContent: FC<Props> = ({
|
||||
</button>
|
||||
)}
|
||||
<div className="mb-1 flex h-6 items-center justify-between">
|
||||
<PopoverTitle className="system-xl-semibold text-text-primary">
|
||||
<div className="system-xl-semibold text-text-primary">
|
||||
{t(`${i18nPrefix}.title`, { ns: 'dataset' })}
|
||||
</PopoverTitle>
|
||||
</div>
|
||||
{!hasBack && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -95,6 +92,7 @@ const CreateContent: FC<Props> = ({
|
||||
</Field>
|
||||
<Field label={t(`${i18nPrefix}.name`, { ns: 'dataset' })}>
|
||||
<Input
|
||||
aria-label={t(`${i18nPrefix}.name`, { ns: 'dataset' })}
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
placeholder={t(`${i18nPrefix}.namePlaceholder`, { ns: 'dataset' })}
|
||||
@@ -119,4 +117,3 @@ const CreateContent: FC<Props> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(CreateContent)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { Props as CreateContentProps } from './create-content'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import * as React from 'react'
|
||||
import CreateContent from './create-content'
|
||||
import { CreateContent } from './create-content'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
@@ -12,13 +11,13 @@ type Props = {
|
||||
popupLeft?: number
|
||||
} & CreateContentProps
|
||||
|
||||
const CreateMetadataModal: FC<Props> = ({
|
||||
export function CreateMetadataModal({
|
||||
open,
|
||||
setOpen,
|
||||
trigger,
|
||||
popupLeft = 20,
|
||||
...createContentProps
|
||||
}) => {
|
||||
}: Props) {
|
||||
const triggerElement = React.isValidElement(trigger)
|
||||
? trigger
|
||||
: <button type="button">{trigger}</button>
|
||||
@@ -33,7 +32,7 @@ const CreateMetadataModal: FC<Props> = ({
|
||||
placement="left-start"
|
||||
sideOffset={popupLeft}
|
||||
alignOffset={-38}
|
||||
popupClassName="border-none bg-transparent shadow-none"
|
||||
popupClassName="w-[320px]"
|
||||
>
|
||||
<CreateContent {...createContentProps} onClose={() => setOpen(false)} onBack={() => setOpen(false)} />
|
||||
</PopoverContent>
|
||||
@@ -41,4 +40,3 @@ const CreateMetadataModal: FC<Props> = ({
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(CreateMetadataModal)
|
||||
|
||||
@@ -32,8 +32,8 @@ import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Input from '@/app/components/base/input'
|
||||
import CreateModal from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal'
|
||||
import { getIcon } from '../utils/get-icon'
|
||||
import { CreateMetadataModal } from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal'
|
||||
import { getIconClassName } from '../utils/get-icon'
|
||||
import Field from './field'
|
||||
|
||||
const i18nPrefix = 'metadata.datasetMetadata'
|
||||
@@ -64,7 +64,7 @@ const Item: FC<ItemProps> = ({
|
||||
onDelete,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const Icon = getIcon(payload.type)
|
||||
const iconClassName = getIconClassName(payload.type)
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
onRename?.()
|
||||
@@ -97,7 +97,7 @@ const Item: FC<ItemProps> = ({
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full items-center space-x-1 text-text-tertiary">
|
||||
<Icon className="size-4 shrink-0" />
|
||||
<span className={cn(iconClassName, 'size-4 shrink-0')} aria-hidden="true" />
|
||||
<div className="max-w-[250px] truncate system-sm-medium text-text-primary">{payload.name}</div>
|
||||
<div className="shrink-0 system-xs-regular">{payload.type}</div>
|
||||
</div>
|
||||
@@ -222,7 +222,7 @@ const DatasetMetadataDrawer: FC<Props> = ({
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-6">
|
||||
<div className="system-sm-regular text-text-tertiary">{t(`${i18nPrefix}.description`, { ns: 'dataset' })}</div>
|
||||
<CreateModal
|
||||
<CreateMetadataModal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
trigger={(
|
||||
@@ -283,6 +283,7 @@ const DatasetMetadataDrawer: FC<Props> = ({
|
||||
|
||||
<Field label={t(`${i18nPrefix}.name`, { ns: 'dataset' })} className="mt-4">
|
||||
<Input
|
||||
aria-label={t(`${i18nPrefix}.name`, { ns: 'dataset' })}
|
||||
value={templeName}
|
||||
onChange={e => setTempleName(e.target.value)}
|
||||
placeholder={t(`${i18nPrefix}.namePlaceholder`, { ns: 'dataset' })}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
'use client'
|
||||
|
||||
import type { ComboboxRootChangeEventDetails, Placement } from '@langgenius/dify-ui/combobox'
|
||||
import type { BuiltInMetadataItem, MetadataItem } from '../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxClear,
|
||||
ComboboxEmpty,
|
||||
ComboboxInput,
|
||||
ComboboxInputGroup,
|
||||
ComboboxItem,
|
||||
ComboboxItemText,
|
||||
ComboboxList,
|
||||
ComboboxSeparator,
|
||||
} from '@langgenius/dify-ui/combobox'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDatasetMetaData } from '@/service/knowledge/use-metadata'
|
||||
import { getIconClassName } from '../utils/get-icon'
|
||||
import { CreateContent } from './create-content'
|
||||
|
||||
const i18nPrefix = 'metadata.selectMetadata'
|
||||
|
||||
const PickerView = {
|
||||
select: 'select',
|
||||
create: 'create',
|
||||
} as const
|
||||
|
||||
type PickerView = typeof PickerView[keyof typeof PickerView]
|
||||
|
||||
export type DatasetMetadataPickerProps = {
|
||||
datasetId: string
|
||||
placement?: Placement
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
onSelectMetadata: (metadata: MetadataItem) => void
|
||||
onCreateMetadata: (metadata: BuiltInMetadataItem) => void | Promise<void>
|
||||
onOpenMetadataManagement: () => void
|
||||
}
|
||||
|
||||
function getMetadataLabel(metadata: MetadataItem) {
|
||||
return metadata.name
|
||||
}
|
||||
|
||||
function getMetadataValue(metadata: MetadataItem) {
|
||||
return metadata.id
|
||||
}
|
||||
|
||||
function isSameMetadata(item: MetadataItem, value: MetadataItem) {
|
||||
return item.id === value.id
|
||||
}
|
||||
|
||||
function metadataFilter(metadata: MetadataItem, query: string) {
|
||||
return metadata.name.toLowerCase().includes(query.toLowerCase())
|
||||
}
|
||||
|
||||
export function DatasetMetadataPicker({
|
||||
datasetId,
|
||||
placement = 'left-start',
|
||||
sideOffset = -38,
|
||||
alignOffset = 4,
|
||||
onSelectMetadata,
|
||||
onCreateMetadata,
|
||||
onOpenMetadataManagement,
|
||||
}: DatasetMetadataPickerProps) {
|
||||
const { t } = useTranslation()
|
||||
const { data: datasetMetaData } = useDatasetMetaData(datasetId)
|
||||
const metadataItems = datasetMetaData?.doc_metadata ?? []
|
||||
const [open, setOpen] = useState(false)
|
||||
const [view, setView] = useState<PickerView>(PickerView.select)
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const resetPicker = () => {
|
||||
setView(PickerView.select)
|
||||
setQuery('')
|
||||
}
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
setOpen(nextOpen)
|
||||
if (!nextOpen)
|
||||
resetPicker()
|
||||
}
|
||||
|
||||
const handleInputValueChange = (inputValue: string, details: ComboboxRootChangeEventDetails) => {
|
||||
if (details.reason !== 'item-press')
|
||||
setQuery(inputValue)
|
||||
}
|
||||
|
||||
const handleMetadataChange = (metadata: MetadataItem | null) => {
|
||||
if (!metadata)
|
||||
return
|
||||
|
||||
onSelectMetadata({
|
||||
id: metadata.id,
|
||||
name: metadata.name,
|
||||
type: metadata.type,
|
||||
})
|
||||
setOpen(false)
|
||||
resetPicker()
|
||||
}
|
||||
|
||||
const handleCreateMetadata = async (metadata: BuiltInMetadataItem) => {
|
||||
try {
|
||||
await onCreateMetadata(metadata)
|
||||
resetPicker()
|
||||
}
|
||||
catch {
|
||||
// Keep the create view open so callers can surface validation feedback and the user can correct the input.
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenManagement = () => {
|
||||
setOpen(false)
|
||||
resetPicker()
|
||||
onOpenMetadataManagement()
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('metadata.addMetadata', { ns: 'dataset' })}
|
||||
aria-expanded={open}
|
||||
className="flex h-6 w-full cursor-pointer items-center justify-center rounded-md border-0 bg-components-button-tertiary-bg px-2 py-0 text-xs font-medium text-components-button-tertiary-text hover:bg-components-button-tertiary-bg-hover focus-visible:bg-components-button-tertiary-bg-hover"
|
||||
>
|
||||
<span className="flex min-w-0 items-center justify-center gap-1">
|
||||
<span className="i-ri-add-line size-3.5 shrink-0 text-components-button-tertiary-text" aria-hidden="true" />
|
||||
<span className="truncate text-components-button-tertiary-text">{t('metadata.addMetadata', { ns: 'dataset' })}</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement={placement}
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
popupClassName="w-[320px] bg-components-panel-bg-blur backdrop-blur-[5px]"
|
||||
>
|
||||
{view === PickerView.select
|
||||
? (
|
||||
<Combobox<MetadataItem>
|
||||
value={null}
|
||||
items={metadataItems}
|
||||
inputValue={query}
|
||||
onInputValueChange={handleInputValueChange}
|
||||
onValueChange={handleMetadataChange}
|
||||
itemToStringLabel={getMetadataLabel}
|
||||
itemToStringValue={getMetadataValue}
|
||||
isItemEqualToValue={isSameMetadata}
|
||||
filter={metadataFilter}
|
||||
>
|
||||
<MetadataPickerSelectPanel
|
||||
query={query}
|
||||
onNewMetadata={() => {
|
||||
setView(PickerView.create)
|
||||
setQuery('')
|
||||
}}
|
||||
onOpenMetadataManagement={handleOpenManagement}
|
||||
/>
|
||||
</Combobox>
|
||||
)
|
||||
: (
|
||||
<CreateContent
|
||||
onSave={handleCreateMetadata}
|
||||
hasBack
|
||||
onBack={resetPicker}
|
||||
onClose={resetPicker}
|
||||
/>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function MetadataPickerSelectPanel({
|
||||
query,
|
||||
onNewMetadata,
|
||||
onOpenMetadataManagement,
|
||||
}: {
|
||||
query: string
|
||||
onNewMetadata: () => void
|
||||
onOpenMetadataManagement: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-2 pb-1">
|
||||
<ComboboxInputGroup>
|
||||
<span className="ml-2 i-ri-search-line size-4 shrink-0 text-text-tertiary" aria-hidden="true" />
|
||||
<ComboboxInput
|
||||
aria-label={t(`${i18nPrefix}.search`, { ns: 'dataset' })}
|
||||
placeholder={t(`${i18nPrefix}.search`, { ns: 'dataset' })}
|
||||
className="pl-2"
|
||||
/>
|
||||
{query && (
|
||||
<ComboboxClear
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
/>
|
||||
)}
|
||||
</ComboboxInputGroup>
|
||||
</div>
|
||||
<ComboboxList>
|
||||
{(metadata: MetadataItem) => (
|
||||
<MetadataOption key={metadata.id} metadata={metadata} />
|
||||
)}
|
||||
</ComboboxList>
|
||||
<ComboboxEmpty>
|
||||
{t('noData', { ns: 'common' })}
|
||||
</ComboboxEmpty>
|
||||
<ComboboxSeparator />
|
||||
<MetadataPickerActions
|
||||
onNewMetadata={onNewMetadata}
|
||||
onOpenMetadataManagement={onOpenMetadataManagement}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function MetadataOption({
|
||||
metadata,
|
||||
}: {
|
||||
metadata: MetadataItem
|
||||
}) {
|
||||
const iconClassName = getIconClassName(metadata.type)
|
||||
|
||||
return (
|
||||
<ComboboxItem value={metadata}>
|
||||
<ComboboxItemText className="flex items-center gap-1.5 px-0">
|
||||
<span className={cn(iconClassName, 'size-3.5 shrink-0')} aria-hidden="true" />
|
||||
<span className="min-w-0 grow truncate">{metadata.name}</span>
|
||||
</ComboboxItemText>
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">
|
||||
{metadata.type}
|
||||
</span>
|
||||
</ComboboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function MetadataPickerActions({
|
||||
onNewMetadata,
|
||||
onOpenMetadataManagement,
|
||||
}: {
|
||||
onNewMetadata: () => void
|
||||
onOpenMetadataManagement: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-1">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex h-8 min-w-0 cursor-pointer items-center gap-1 rounded-lg border-none bg-transparent px-2 text-left text-text-secondary outline-hidden',
|
||||
'hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active',
|
||||
)}
|
||||
onClick={onNewMetadata}
|
||||
>
|
||||
<span className="i-ri-add-line size-4 shrink-0 text-text-tertiary" aria-hidden="true" />
|
||||
<span className="truncate system-sm-medium">{t(`${i18nPrefix}.newAction`, { ns: 'dataset' })}</span>
|
||||
</button>
|
||||
<div className="flex h-8 shrink-0 items-center text-text-secondary">
|
||||
<div className="mx-1 h-3 w-px bg-divider-regular" />
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex h-8 cursor-pointer items-center gap-1 rounded-lg border-none bg-transparent px-2 text-left text-text-secondary outline-hidden',
|
||||
'hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active',
|
||||
)}
|
||||
onClick={onOpenMetadataManagement}
|
||||
>
|
||||
<span className="system-sm-medium">{t(`${i18nPrefix}.manageAction`, { ns: 'dataset' })}</span>
|
||||
<span className="i-ri-arrow-right-up-line size-4 shrink-0 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
'use client'
|
||||
import type { Placement } from '@langgenius/dify-ui/popover'
|
||||
import type { FC } from 'react'
|
||||
import type { BuiltInMetadataItem, MetadataItem } from '../types'
|
||||
import type { Props as CreateContentProps } from './create-content'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useDatasetMetaData } from '@/service/knowledge/use-metadata'
|
||||
import CreateContent from './create-content'
|
||||
import SelectMetadata from './select-metadata'
|
||||
|
||||
type Props = {
|
||||
datasetId: string
|
||||
popupPlacement?: Placement
|
||||
popupOffset?: { mainAxis: number, crossAxis: number }
|
||||
onSelect: (data: MetadataItem) => void
|
||||
trigger: React.ReactNode
|
||||
onManage: () => void
|
||||
} & CreateContentProps
|
||||
|
||||
const Step = {
|
||||
select: 'select',
|
||||
create: 'create',
|
||||
} as const
|
||||
|
||||
type Step = typeof Step[keyof typeof Step]
|
||||
|
||||
const SelectMetadataModal: FC<Props> = ({
|
||||
datasetId,
|
||||
popupPlacement = 'left-start',
|
||||
popupOffset = { mainAxis: -38, crossAxis: 4 },
|
||||
trigger,
|
||||
onSelect,
|
||||
onSave,
|
||||
onManage,
|
||||
}) => {
|
||||
const { data: datasetMetaData } = useDatasetMetaData(datasetId)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [step, setStep] = useState<Step>(Step.select)
|
||||
const triggerElement = React.isValidElement(trigger)
|
||||
? trigger
|
||||
: <button type="button">{trigger}</button>
|
||||
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
||||
setOpen(nextOpen)
|
||||
if (!nextOpen)
|
||||
setStep(Step.select)
|
||||
}, [])
|
||||
|
||||
const handleSave = useCallback(async (data: BuiltInMetadataItem) => {
|
||||
await onSave(data)
|
||||
setStep(Step.select)
|
||||
}, [onSave])
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<PopoverTrigger render={triggerElement as React.ReactElement} />
|
||||
<PopoverContent
|
||||
placement={popupPlacement}
|
||||
sideOffset={popupOffset.mainAxis}
|
||||
alignOffset={popupOffset.crossAxis}
|
||||
popupClassName="border-none bg-transparent shadow-none"
|
||||
>
|
||||
{step === Step.select
|
||||
? (
|
||||
<SelectMetadata
|
||||
onSelect={(data) => {
|
||||
onSelect(data)
|
||||
setOpen(false)
|
||||
}}
|
||||
list={datasetMetaData?.doc_metadata || []}
|
||||
onNew={() => setStep(Step.create)}
|
||||
onManage={() => {
|
||||
setOpen(false)
|
||||
setStep(Step.select)
|
||||
onManage()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<CreateContent
|
||||
onSave={handleSave}
|
||||
hasBack
|
||||
onBack={() => setStep(Step.select)}
|
||||
onClose={() => setStep(Step.select)}
|
||||
/>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(SelectMetadataModal)
|
||||
@@ -1,93 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { MetadataItem } from '../types'
|
||||
import { RiAddLine, RiArrowRightUpLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { getIcon } from '../utils/get-icon'
|
||||
|
||||
const i18nPrefix = 'metadata.selectMetadata'
|
||||
|
||||
type Props = {
|
||||
list: MetadataItem[]
|
||||
onSelect: (data: MetadataItem) => void
|
||||
onNew: () => void
|
||||
onManage: () => void
|
||||
}
|
||||
|
||||
const SelectMetadata: FC<Props> = ({
|
||||
list: notFilteredList,
|
||||
onSelect,
|
||||
onNew,
|
||||
onManage,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
const list = useMemo(() => {
|
||||
if (!query)
|
||||
return notFilteredList
|
||||
return notFilteredList.filter((item) => {
|
||||
return item.name.toLowerCase().includes(query.toLowerCase())
|
||||
})
|
||||
}, [query, notFilteredList])
|
||||
return (
|
||||
<div className="w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pt-2 pb-0 shadow-lg backdrop-blur-[5px]">
|
||||
<SearchInput
|
||||
className="mx-2"
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
placeholder={t(`${i18nPrefix}.search`, { ns: 'dataset' })}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
{list.map((item) => {
|
||||
const Icon = getIcon(item.type)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={item.id}
|
||||
className="mx-1 flex h-6 cursor-pointer items-center justify-between rounded-md border-none bg-transparent px-3 text-left hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => onSelect({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
})}
|
||||
>
|
||||
<div className="flex h-full w-0 grow items-center text-text-secondary">
|
||||
<Icon className="mr-[5px] size-3.5 shrink-0" aria-hidden="true" />
|
||||
<div className="w-0 grow truncate system-sm-medium">{item.name}</div>
|
||||
</div>
|
||||
<div className="ml-1 shrink-0 system-xs-regular text-text-tertiary">
|
||||
{item.type}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-1 flex justify-between border-t border-divider-subtle p-1">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-6 cursor-pointer items-center space-x-1 rounded-md border-none bg-transparent px-3 text-left text-text-secondary hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onNew}
|
||||
>
|
||||
<RiAddLine className="size-3.5" aria-hidden="true" />
|
||||
<div className="system-sm-medium">{t(`${i18nPrefix}.newAction`, { ns: 'dataset' })}</div>
|
||||
</button>
|
||||
<div className="flex h-6 items-center text-text-secondary">
|
||||
<div className="mr-[3px] h-3 w-px bg-divider-regular"></div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-full cursor-pointer items-center rounded-md border-none bg-transparent px-1.5 text-left text-text-secondary hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onManage}
|
||||
>
|
||||
<div className="mr-1 system-sm-medium">{t(`${i18nPrefix}.manageAction`, { ns: 'dataset' })}</div>
|
||||
<RiArrowRightUpLine className="size-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(SelectMetadata)
|
||||
@@ -377,7 +377,7 @@ describe('MetadataDocument', () => {
|
||||
setTempList,
|
||||
})
|
||||
|
||||
const { container } = render(
|
||||
render(
|
||||
<MetadataDocument
|
||||
datasetId="ds-1"
|
||||
documentId="doc-1"
|
||||
@@ -385,14 +385,12 @@ describe('MetadataDocument', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const inputs = container.querySelectorAll('input')
|
||||
if (inputs.length > 0) {
|
||||
fireEvent.change(inputs[0]!, { target: { value: 'new value' } })
|
||||
const valueInput = screen.getByDisplayValue('Value 1')
|
||||
fireEvent.change(valueInput, { target: { value: 'new value' } })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setTempList).toHaveBeenCalled()
|
||||
})
|
||||
}
|
||||
await waitFor(() => {
|
||||
expect(setTempList).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should have handleAddMetaData function available', () => {
|
||||
@@ -445,7 +443,7 @@ describe('MetadataDocument', () => {
|
||||
setTempList,
|
||||
})
|
||||
|
||||
const { container } = render(
|
||||
render(
|
||||
<MetadataDocument
|
||||
datasetId="ds-1"
|
||||
documentId="doc-1"
|
||||
@@ -453,13 +451,11 @@ describe('MetadataDocument', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const inputs = container.querySelectorAll('input')
|
||||
if (inputs.length > 0) {
|
||||
fireEvent.change(inputs[0]!, { target: { value: 'updated' } })
|
||||
await waitFor(() => {
|
||||
expect(setTempList).toHaveBeenCalled()
|
||||
})
|
||||
}
|
||||
const valueInput = screen.getByDisplayValue('Value 1')
|
||||
fireEvent.change(valueInput, { target: { value: 'updated' } })
|
||||
await waitFor(() => {
|
||||
expect(setTempList).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass onDelete callback to InfoGroup', async () => {
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
import type { MetadataItemWithValue } from '../../types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DataType } from '../../types'
|
||||
import InfoGroup from '../info-group'
|
||||
|
||||
type SelectModalProps = {
|
||||
trigger: React.ReactNode
|
||||
onSelect: (item: MetadataItemWithValue) => void
|
||||
onSave: (data: { name: string, type: DataType }) => void
|
||||
onManage: () => void
|
||||
}
|
||||
|
||||
type FieldProps = {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
type InputCombinedProps = {
|
||||
value: string | number | null
|
||||
onChange: (value: string | number) => void
|
||||
type: DataType
|
||||
}
|
||||
|
||||
const { mockRouterPush } = vi.hoisted(() => ({
|
||||
mockRouterPush: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
push: mockRouterPush,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/knowledge/use-metadata', () => ({
|
||||
useDatasetMetaData: () => ({
|
||||
data: {
|
||||
doc_metadata: [
|
||||
{ id: '1', name: 'test', type: DataType.string },
|
||||
],
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -39,16 +41,11 @@ vi.mock('@/hooks/use-timestamp', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock AddMetadataButton
|
||||
vi.mock('../../add-metadata-button', () => ({
|
||||
default: () => <button>Add Metadata</button>,
|
||||
}))
|
||||
|
||||
// Mock InputCombined
|
||||
vi.mock('../../edit-metadata-batch/input-combined', () => ({
|
||||
default: ({ value, onChange, type }: InputCombinedProps) => (
|
||||
<input
|
||||
data-testid="input-combined"
|
||||
aria-label={`Metadata ${type} value`}
|
||||
data-type={type}
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
@@ -56,28 +53,6 @@ vi.mock('../../edit-metadata-batch/input-combined', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock SelectMetadataModal
|
||||
vi.mock('../../metadata-dataset/select-metadata-modal', () => ({
|
||||
default: ({ trigger, onSelect, onSave, onManage }: SelectModalProps) => (
|
||||
<div data-testid="select-metadata-modal">
|
||||
{trigger}
|
||||
<button onClick={() => onSelect({ id: '1', name: 'test', type: DataType.string, value: null })}>Select</button>
|
||||
<button onClick={() => onSave({ name: 'new_field', type: DataType.string })}>Save</button>
|
||||
<button onClick={onManage}>Manage</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Field
|
||||
vi.mock('../field', () => ({
|
||||
default: ({ label, children }: FieldProps) => (
|
||||
<div data-testid="field">
|
||||
<span data-testid="field-label">{label}</span>
|
||||
<div data-testid="field-content">{children}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('InfoGroup', () => {
|
||||
const mockList: MetadataItemWithValue[] = [
|
||||
{ id: '1', name: 'field_one', type: DataType.string, value: 'Value 1' },
|
||||
@@ -85,6 +60,10 @@ describe('InfoGroup', () => {
|
||||
{ id: '3', name: 'built-in', type: DataType.time, value: 1609459200 },
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const { container } = render(
|
||||
@@ -111,8 +90,9 @@ describe('InfoGroup', () => {
|
||||
render(
|
||||
<InfoGroup dataSetId="ds-1" list={mockList} />,
|
||||
)
|
||||
const fields = screen.getAllByTestId('field')
|
||||
expect(fields).toHaveLength(3)
|
||||
expect(screen.getByText('field_one'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('field_two'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('built-in'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tooltip when titleTooltip is provided', () => {
|
||||
@@ -133,33 +113,33 @@ describe('InfoGroup', () => {
|
||||
dataSetId="ds-1"
|
||||
list={mockList}
|
||||
title="Test"
|
||||
headerRight={<button data-testid="header-right-btn">Action</button>}
|
||||
headerRight={<button>Action</button>}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('header-right-btn'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Action' }))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edit Mode', () => {
|
||||
it('should render add metadata button when isEdit is true', () => {
|
||||
it('should render dataset metadata picker when isEdit is true', () => {
|
||||
render(
|
||||
<InfoGroup dataSetId="ds-1" list={mockList} isEdit />,
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'Add Metadata' }))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render add metadata button when isEdit is false', () => {
|
||||
it('should not render dataset metadata picker when isEdit is false', () => {
|
||||
render(
|
||||
<InfoGroup dataSetId="ds-1" list={mockList} isEdit={false} />,
|
||||
)
|
||||
expect(screen.queryByRole('button', { name: 'Add Metadata' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'dataset.metadata.addMetadata' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render input combined for each item in edit mode', () => {
|
||||
render(
|
||||
<InfoGroup dataSetId="ds-1" list={mockList} isEdit />,
|
||||
)
|
||||
const inputs = screen.getAllByTestId('input-combined')
|
||||
const inputs = screen.getAllByRole('textbox')
|
||||
expect(inputs).toHaveLength(3)
|
||||
})
|
||||
|
||||
@@ -178,7 +158,7 @@ describe('InfoGroup', () => {
|
||||
<InfoGroup dataSetId="ds-1" list={mockList} isEdit onChange={handleChange} />,
|
||||
)
|
||||
|
||||
const inputs = screen.getAllByTestId('input-combined')
|
||||
const inputs = screen.getAllByRole('textbox')
|
||||
fireEvent.change(inputs[0]!, { target: { value: 'New Value' } })
|
||||
|
||||
expect(handleChange).toHaveBeenCalled()
|
||||
@@ -195,29 +175,34 @@ describe('InfoGroup', () => {
|
||||
expect(handleDelete).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onSelect when metadata is selected', () => {
|
||||
it('should call onSelect when metadata is selected', async () => {
|
||||
const handleSelect = vi.fn()
|
||||
render(
|
||||
<InfoGroup dataSetId="ds-1" list={mockList} isEdit onSelect={handleSelect} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Select' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
fireEvent.click(await screen.findByRole('option', { name: /test/ }))
|
||||
|
||||
expect(handleSelect).toHaveBeenCalledWith({
|
||||
id: '1',
|
||||
name: 'test',
|
||||
type: DataType.string,
|
||||
value: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onAdd when new metadata is saved', () => {
|
||||
it('should call onAdd when new metadata is saved', async () => {
|
||||
const handleAdd = vi.fn()
|
||||
render(
|
||||
<InfoGroup dataSetId="ds-1" list={mockList} isEdit onAdd={handleAdd} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'dataset.metadata.selectMetadata.newAction' }))
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'dataset.metadata.createMetadata.name' }), {
|
||||
target: { value: 'new_field' },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(handleAdd).toHaveBeenCalledWith({
|
||||
name: 'new_field',
|
||||
@@ -225,14 +210,15 @@ describe('InfoGroup', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should navigate to documents page when manage is clicked', () => {
|
||||
it('should navigate to documents page when manage is clicked', async () => {
|
||||
render(
|
||||
<InfoGroup dataSetId="ds-1" list={mockList} isEdit />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Manage' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.addMetadata' }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'dataset.metadata.selectMetadata.manageAction' }))
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Manage' }))!.toBeInTheDocument()
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/datasets/ds-1/documents')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -299,7 +285,7 @@ describe('InfoGroup', () => {
|
||||
)
|
||||
// The mock formatTime returns formatted date
|
||||
// The mock formatTime returns formatted date
|
||||
expect(screen.getByTestId('field-content'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('field'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -318,7 +304,7 @@ describe('InfoGroup', () => {
|
||||
render(
|
||||
<InfoGroup dataSetId="ds-1" list={nullList} />,
|
||||
)
|
||||
expect(screen.getByTestId('field'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('field'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle items with built-in id', () => {
|
||||
@@ -328,7 +314,7 @@ describe('InfoGroup', () => {
|
||||
render(
|
||||
<InfoGroup dataSetId="ds-1" list={builtInList} />,
|
||||
)
|
||||
expect(screen.getByTestId('field'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('field'))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,9 +9,8 @@ import Divider from '@/app/components/base/divider'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import AddMetadataButton from '../add-metadata-button'
|
||||
import InputCombined from '../edit-metadata-batch/input-combined'
|
||||
import SelectMetadataModal from '../metadata-dataset/select-metadata-modal'
|
||||
import { DatasetMetadataPicker } from '../metadata-dataset/dataset-metadata-picker'
|
||||
import { DataType, isShowManageMetadataLocalStorageKey } from '../types'
|
||||
import Field from './field'
|
||||
|
||||
@@ -76,14 +75,11 @@ const InfoGroup: FC<Props> = ({
|
||||
<div className={cn('mt-3 space-y-1', contentClassName)}>
|
||||
{isEdit && (
|
||||
<div>
|
||||
<SelectMetadataModal
|
||||
<DatasetMetadataPicker
|
||||
datasetId={dataSetId}
|
||||
trigger={
|
||||
<AddMetadataButton />
|
||||
}
|
||||
onSelect={data => onSelect?.(data as MetadataItemWithValue)}
|
||||
onSave={data => onAdd?.(data)}
|
||||
onManage={handleMangeMetadata}
|
||||
onSelectMetadata={data => onSelect?.(data as MetadataItemWithValue)}
|
||||
onCreateMetadata={data => onAdd?.(data)}
|
||||
onOpenMetadataManagement={handleMangeMetadata}
|
||||
/>
|
||||
{list.length > 0 && <Divider className="my-3" bgStyle="gradient" />}
|
||||
</div>
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
import { RiHashtag, RiTextSnippet, RiTimeLine } from '@remixicon/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { DataType } from '../../types'
|
||||
import { getIcon } from '../get-icon'
|
||||
import { getIconClassName } from '../get-icon'
|
||||
|
||||
describe('getIcon', () => {
|
||||
describe('getIconClassName', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should return RiTextSnippet for DataType.string', () => {
|
||||
const result = getIcon(DataType.string)
|
||||
expect(result).toBe(RiTextSnippet)
|
||||
it('should return text snippet icon class for DataType.string', () => {
|
||||
const result = getIconClassName(DataType.string)
|
||||
expect(result).toBe('i-ri-text-snippet')
|
||||
})
|
||||
|
||||
it('should return RiHashtag for DataType.number', () => {
|
||||
const result = getIcon(DataType.number)
|
||||
expect(result).toBe(RiHashtag)
|
||||
it('should return hashtag icon class for DataType.number', () => {
|
||||
const result = getIconClassName(DataType.number)
|
||||
expect(result).toBe('i-ri-hashtag')
|
||||
})
|
||||
|
||||
it('should return RiTimeLine for DataType.time', () => {
|
||||
const result = getIcon(DataType.time)
|
||||
expect(result).toBe(RiTimeLine)
|
||||
it('should return time line icon class for DataType.time', () => {
|
||||
const result = getIconClassName(DataType.time)
|
||||
expect(result).toBe('i-ri-time-line')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should return RiTextSnippet as fallback for unknown type', () => {
|
||||
const result = getIcon('unknown' as DataType)
|
||||
expect(result).toBe(RiTextSnippet)
|
||||
it('should return text snippet class as fallback for unknown type', () => {
|
||||
const result = getIconClassName('unknown' as DataType)
|
||||
expect(result).toBe('i-ri-text-snippet')
|
||||
})
|
||||
|
||||
it('should return RiTextSnippet for undefined type', () => {
|
||||
const result = getIcon(undefined as unknown as DataType)
|
||||
expect(result).toBe(RiTextSnippet)
|
||||
it('should return text snippet class for undefined type', () => {
|
||||
const result = getIconClassName(undefined as unknown as DataType)
|
||||
expect(result).toBe('i-ri-text-snippet')
|
||||
})
|
||||
|
||||
it('should return RiTextSnippet for null type', () => {
|
||||
const result = getIcon(null as unknown as DataType)
|
||||
expect(result).toBe(RiTextSnippet)
|
||||
it('should return text snippet class for null type', () => {
|
||||
const result = getIconClassName(null as unknown as DataType)
|
||||
expect(result).toBe('i-ri-text-snippet')
|
||||
})
|
||||
|
||||
it('should return RiTextSnippet for empty string type', () => {
|
||||
const result = getIcon('' as DataType)
|
||||
expect(result).toBe(RiTextSnippet)
|
||||
it('should return text snippet class for empty string type', () => {
|
||||
const result = getIconClassName('' as DataType)
|
||||
expect(result).toBe('i-ri-text-snippet')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { RiHashtag, RiTextSnippet, RiTimeLine } from '@remixicon/react'
|
||||
import { DataType } from '../types'
|
||||
|
||||
export const getIcon = (type: DataType) => {
|
||||
return ({
|
||||
[DataType.string]: RiTextSnippet,
|
||||
[DataType.number]: RiHashtag,
|
||||
[DataType.time]: RiTimeLine,
|
||||
}[type] || RiTextSnippet)
|
||||
const metadataTypeIconClassMap: Record<DataType, string> = {
|
||||
[DataType.string]: 'i-ri-text-snippet',
|
||||
[DataType.number]: 'i-ri-hashtag',
|
||||
[DataType.time]: 'i-ri-time-line',
|
||||
}
|
||||
|
||||
export function getIconClassName(type: DataType) {
|
||||
return metadataTypeIconClassMap[type] ?? metadataTypeIconClassMap[DataType.string]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user