diff --git a/eslint-suppressions.json b/eslint-suppressions.json index e169248299..cd737f35cc 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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 diff --git a/web/app/components/datasets/metadata/__tests__/add-metadata-button.spec.tsx b/web/app/components/datasets/metadata/__tests__/add-metadata-button.spec.tsx deleted file mode 100644 index 2dbfb6febe..0000000000 --- a/web/app/components/datasets/metadata/__tests__/add-metadata-button.spec.tsx +++ /dev/null @@ -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() - expect(screen.getByRole('button')).toBeInTheDocument() - }) - - it('should render with translated text', () => { - render() - // The button should contain text from i18n - expect(screen.getByRole('button')).toBeInTheDocument() - }) - - it('should render add icon', () => { - const { container } = render() - // 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() - const button = screen.getByRole('button') - expect(button).toHaveClass('custom-class') - }) - - it('should apply default classes', () => { - render() - const button = screen.getByRole('button') - expect(button).toHaveClass('flex', 'w-full', 'items-center') - }) - - it('should merge custom className with default classes', () => { - render() - 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() - - fireEvent.click(screen.getByRole('button')) - - expect(handleClick).toHaveBeenCalledTimes(1) - }) - - it('should not throw when onClick is not provided and button is clicked', () => { - render() - - expect(() => { - fireEvent.click(screen.getByRole('button')) - }).not.toThrow() - }) - - it('should call onClick multiple times on multiple clicks', () => { - const handleClick = vi.fn() - render() - - 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() - expect(screen.getByRole('button')).toBeInTheDocument() - }) - - it('should render with empty className', () => { - render() - expect(screen.getByRole('button')).toBeInTheDocument() - }) - - it('should render with undefined onClick', () => { - render() - expect(screen.getByRole('button')).toBeInTheDocument() - }) - }) -}) diff --git a/web/app/components/datasets/metadata/add-metadata-button.tsx b/web/app/components/datasets/metadata/add-metadata-button.tsx deleted file mode 100644 index a8070dcaa1..0000000000 --- a/web/app/components/datasets/metadata/add-metadata-button.tsx +++ /dev/null @@ -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 = ({ - className, - onClick, -}) => { - const { t } = useTranslation() - return ( - - ) -} -export default React.memo(AddedMetadataButton) diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx index 060befe7d3..d3c3581513 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/__tests__/modal.spec.tsx @@ -56,42 +56,44 @@ type AddRowProps = { onRemove: () => void } -type SelectModalProps = { - trigger: React.ReactNode - onSelect: (item: MetadataItemInBatchEdit) => void - onSave: (data: { name: string, type: DataType }) => Promise - 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) => ( -
- {payload.name} - - - +
+ {payload.name} + + +
), })) vi.mock('../add-row', () => ({ default: ({ payload, onChange, onRemove }: AddRowProps) => ( -
- {payload.name} - - -
- ), -})) - -vi.mock('../../metadata-dataset/select-metadata-modal', () => ({ - default: ({ trigger, onSelect, onSave, onManage }: SelectModalProps) => ( -
- {trigger} - - - +
+ {payload.name} + +
), })) @@ -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() @@ -134,8 +155,7 @@ describe('EditMetadataBatchModal', () => { it('should render all edit rows for existing items', async () => { render() 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() 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() 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() 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() 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() }) }) }) diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx index abc5cb7fcc..964f48c152 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx @@ -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 = ({ datasetId, documentNum, list, onSav {addedList.map((item, i) => ())}
- } onSave={handleAddMetaData} onSelect={data => setAddedList([...addedList, data as MetadataItemWithEdit])} onManage={onShowManage} /> + setAddedList([...addedList, data as MetadataItemWithEdit])} + onOpenMetadataManagement={onShowManage} + />
- setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} id="apply-to-all" /> + setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} + id="apply-to-all" + ariaLabel={t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })} + />
{t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}
{ return render( diff --git a/web/app/components/datasets/metadata/metadata-dataset/__tests__/create-metadata-modal.spec.tsx b/web/app/components/datasets/metadata/metadata-dataset/__tests__/create-metadata-modal.spec.tsx index 7d09f4749a..a987104d70 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/__tests__/create-metadata-modal.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/__tests__/create-metadata-modal.spec.tsx @@ -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) => ( - -
{children}
-
- ), - 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 - }, - PopoverContent: ({ children, className }: ContentProps) => { - const context = React.useContext(PopoverContext) - if (!context?.open) - return null - - return
{children}
- }, - } -}) - -// Mock CreateContent component -vi.mock('../create-content', () => ({ - default: ({ onSave, onClose, onBack, hasBack }: CreateContentProps) => ( -
- {String(hasBack)} - - - {hasBack && } -
- ), -})) +import { CreateMetadataModal } from '../create-metadata-modal' describe('CreateMetadataModal', () => { - const mockTrigger = + const mockTrigger = 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( { />, ) - expect(screen.getByTestId('popover-root')).toHaveAttribute('data-open', 'true') + expect(screen.getByRole('dialog')).toBeInTheDocument() }) it('should handle different trigger elements', () => { - const customTrigger =
Custom
+ const customTrigger = render( { />, ) - expect(screen.getByTestId('custom-trigger')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Custom' })).toBeInTheDocument() }) }) }) diff --git a/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx b/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx index f10f9a72a7..f6d168dbe9 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx @@ -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) => ( -
-
setOpen(true)}>{trigger}
- {open && ( -
- - -
- )} -
- ), -})) - 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() @@ -125,7 +114,7 @@ describe('DatasetMetadataDrawer', () => { it('should render add metadata button', async () => { render() 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 diff --git a/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-picker.spec.tsx b/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-picker.spec.tsx new file mode 100644 index 0000000000..48b8d3ad66 --- /dev/null +++ b/web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-picker.spec.tsx @@ -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> = {}) { + const props = { + datasetId: 'dataset-1', + onSelectMetadata: vi.fn(), + onCreateMetadata: vi.fn(), + onOpenMetadataManagement: vi.fn(), + ...overrides, + } + + return { + props, + ...render(), + } +} + +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() + }) + }) +}) diff --git a/web/app/components/datasets/metadata/metadata-dataset/__tests__/select-metadata-modal.spec.tsx b/web/app/components/datasets/metadata/metadata-dataset/__tests__/select-metadata-modal.spec.tsx deleted file mode 100644 index 0ac3743a86..0000000000 --- a/web/app/components/datasets/metadata/metadata-dataset/__tests__/select-metadata-modal.spec.tsx +++ /dev/null @@ -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) => ( - -
{children}
-
- ), - 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 - }, - PopoverContent: ({ children }: ContentProps) => { - const context = React.useContext(PopoverContext) - if (!context?.open) - return null - - return
{children}
- }, - } -}) - -// Mock SelectMetadata component -vi.mock('../select-metadata', () => ({ - default: ({ onSelect, onNew, onManage, list }: SelectMetadataProps) => ( -
- {list?.length || 0} - - - -
- ), -})) - -// Mock CreateContent component -vi.mock('../create-content', () => ({ - default: ({ onSave, onBack, onClose, hasBack }: CreateContentProps) => ( -
- - {hasBack && } - -
- ), -})) - -describe('SelectMetadataModal', () => { - const mockTrigger = - - describe('Rendering', () => { - it('should render without crashing', () => { - render( - , - ) - expect(screen.getByTestId('popover-root')).toBeInTheDocument() - }) - - it('should render trigger element', () => { - render( - , - ) - expect(screen.getByTestId('trigger-button')).toBeInTheDocument() - }) - - it('should not render SelectMetadata before opening', () => { - render( - , - ) - expect(screen.queryByTestId('select-metadata')).not.toBeInTheDocument() - }) - - it('should pass dataset metadata to SelectMetadata', () => { - render( - , - ) - 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( - , - ) - - 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( - , - ) - - 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( - , - ) - - 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( - , - ) - - 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( - , - ) - - // 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( - , - ) - - // 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( - , - ) - expect(screen.getByTestId('popover-root')).toBeInTheDocument() - }) - - it('should accept custom popupOffset', () => { - render( - , - ) - expect(screen.getByTestId('popover-root')).toBeInTheDocument() - }) - }) - - describe('Edge Cases', () => { - it('should handle different datasetIds', () => { - const { rerender } = render( - , - ) - - expect(screen.getByTestId('popover-root')).toBeInTheDocument() - - rerender( - , - ) - - expect(screen.getByTestId('popover-root')).toBeInTheDocument() - }) - - it('should handle empty trigger', () => { - render( - } - onSelect={vi.fn()} - onSave={vi.fn()} - onManage={vi.fn()} - />, - ) - expect(screen.getByTestId('empty-trigger')).toBeInTheDocument() - }) - }) -}) diff --git a/web/app/components/datasets/metadata/metadata-dataset/__tests__/select-metadata.spec.tsx b/web/app/components/datasets/metadata/metadata-dataset/__tests__/select-metadata.spec.tsx deleted file mode 100644 index 70432ebf9d..0000000000 --- a/web/app/components/datasets/metadata/metadata-dataset/__tests__/select-metadata.spec.tsx +++ /dev/null @@ -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) => Icon, -})) - -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( - , - ) - expect(container.firstChild).toBeInTheDocument() - }) - - it('should render search input', () => { - render( - , - ) - expect(screen.getByRole('textbox')).toBeInTheDocument() - }) - - it('should render all metadata items', () => { - render( - , - ) - 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( - , - ) - expect(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.newAction' })).toBeInTheDocument() - }) - - it('should render manage action button', () => { - render( - , - ) - expect(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.manageAction' })).toBeInTheDocument() - }) - - it('should display type for each item', () => { - render( - , - ) - 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( - , - ) - - 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( - , - ) - - 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( - , - ) - - 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( - , - ) - - 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( - , - ) - - 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( - , - ) - - 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( - , - ) - - fireEvent.click(screen.getByRole('button', { name: 'dataset.metadata.selectMetadata.manageAction' })) - - expect(handleManage).toHaveBeenCalled() - }) - }) - - describe('Empty State', () => { - it('should render empty list', () => { - const { container } = render( - , - ) - expect(container.firstChild).toBeInTheDocument() - }) - - it('should still show new and manage buttons with empty list', () => { - render( - , - ) - 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( - , - ) - 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( - , - ) - 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( - , - ) - expect(screen.getByText('this_is_a_very_long_field_name_that_might_overflow')).toBeInTheDocument() - }) - - it('should handle rapid search input changes', () => { - render( - , - ) - - 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() - }) - }) -}) diff --git a/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx b/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx index 691d1512bc..a1236f0ea3 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx @@ -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 = ({ +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 = ({ }, [onSave, type, name]) return ( -
+
{hasBack && ( )}
- +
{t(`${i18nPrefix}.title`, { ns: 'dataset' })} - +
{!hasBack && (
) } -export default React.memo(CreateContent) diff --git a/web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx b/web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx index fad6fd6bb4..68163bd8d5 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx @@ -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 = ({ +export function CreateMetadataModal({ open, setOpen, trigger, popupLeft = 20, ...createContentProps -}) => { +}: Props) { const triggerElement = React.isValidElement(trigger) ? trigger : @@ -33,7 +32,7 @@ const CreateMetadataModal: FC = ({ placement="left-start" sideOffset={popupLeft} alignOffset={-38} - popupClassName="border-none bg-transparent shadow-none" + popupClassName="w-[320px]" > setOpen(false)} onBack={() => setOpen(false)} /> @@ -41,4 +40,3 @@ const CreateMetadataModal: FC = ({ ) } -export default React.memo(CreateMetadataModal) diff --git a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx index 8553eddacf..9bf31766c0 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx @@ -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 = ({ 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 = ({ )} >
- +
@@ -222,7 +222,7 @@ const DatasetMetadataDrawer: FC = ({
{t(`${i18nPrefix}.description`, { ns: 'dataset' })}
- = ({ setTempleName(e.target.value)} placeholder={t(`${i18nPrefix}.namePlaceholder`, { ns: 'dataset' })} diff --git a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-picker.tsx b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-picker.tsx new file mode 100644 index 0000000000..81ac317010 --- /dev/null +++ b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-picker.tsx @@ -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 + 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.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 ( + + + + + + )} + /> + + {view === PickerView.select + ? ( + + value={null} + items={metadataItems} + inputValue={query} + onInputValueChange={handleInputValueChange} + onValueChange={handleMetadataChange} + itemToStringLabel={getMetadataLabel} + itemToStringValue={getMetadataValue} + isItemEqualToValue={isSameMetadata} + filter={metadataFilter} + > + { + setView(PickerView.create) + setQuery('') + }} + onOpenMetadataManagement={handleOpenManagement} + /> + + ) + : ( + + )} + + + ) +} + +function MetadataPickerSelectPanel({ + query, + onNewMetadata, + onOpenMetadataManagement, +}: { + query: string + onNewMetadata: () => void + onOpenMetadataManagement: () => void +}) { + const { t } = useTranslation() + + return ( + <> +
+ + +
+ + {(metadata: MetadataItem) => ( + + )} + + + {t('noData', { ns: 'common' })} + + + + + ) +} + +function MetadataOption({ + metadata, +}: { + metadata: MetadataItem +}) { + const iconClassName = getIconClassName(metadata.type) + + return ( + + + + + {metadata.type} + + + ) +} + +function MetadataPickerActions({ + onNewMetadata, + onOpenMetadataManagement, +}: { + onNewMetadata: () => void + onOpenMetadataManagement: () => void +}) { + const { t } = useTranslation() + + return ( +
+ +
+
+ +
+
+ ) +} diff --git a/web/app/components/datasets/metadata/metadata-dataset/select-metadata-modal.tsx b/web/app/components/datasets/metadata/metadata-dataset/select-metadata-modal.tsx deleted file mode 100644 index 352b95f248..0000000000 --- a/web/app/components/datasets/metadata/metadata-dataset/select-metadata-modal.tsx +++ /dev/null @@ -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 = ({ - 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.select) - const triggerElement = React.isValidElement(trigger) - ? trigger - : - 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 ( - - - - {step === Step.select - ? ( - { - onSelect(data) - setOpen(false) - }} - list={datasetMetaData?.doc_metadata || []} - onNew={() => setStep(Step.create)} - onManage={() => { - setOpen(false) - setStep(Step.select) - onManage() - }} - /> - ) - : ( - setStep(Step.select)} - onClose={() => setStep(Step.select)} - /> - )} - - - - ) -} -export default React.memo(SelectMetadataModal) diff --git a/web/app/components/datasets/metadata/metadata-dataset/select-metadata.tsx b/web/app/components/datasets/metadata/metadata-dataset/select-metadata.tsx deleted file mode 100644 index 2886601170..0000000000 --- a/web/app/components/datasets/metadata/metadata-dataset/select-metadata.tsx +++ /dev/null @@ -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 = ({ - 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 ( -
- -
- {list.map((item) => { - const Icon = getIcon(item.type) - return ( - - ) - })} -
-
- -
-
- -
-
-
- ) -} -export default React.memo(SelectMetadata) diff --git a/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx b/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx index 71f23324f6..d51ef2be4d 100644 --- a/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-document/__tests__/index.spec.tsx @@ -377,7 +377,7 @@ describe('MetadataDocument', () => { setTempList, }) - const { container } = render( + render( { />, ) - 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( { />, ) - 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 () => { diff --git a/web/app/components/datasets/metadata/metadata-document/__tests__/info-group.spec.tsx b/web/app/components/datasets/metadata/metadata-document/__tests__/info-group.spec.tsx index bcae6de66f..1086db9fd6 100644 --- a/web/app/components/datasets/metadata/metadata-document/__tests__/info-group.spec.tsx +++ b/web/app/components/datasets/metadata/metadata-document/__tests__/info-group.spec.tsx @@ -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: () => , -})) - // Mock InputCombined vi.mock('../../edit-metadata-batch/input-combined', () => ({ default: ({ value, onChange, type }: InputCombinedProps) => ( 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) => ( -
- {trigger} - - - -
- ), -})) - -// Mock Field -vi.mock('../field', () => ({ - default: ({ label, children }: FieldProps) => ( -
- {label} -
{children}
-
- ), -})) - 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( , ) - 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={} + headerRight={} />, ) - 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( , ) - 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( , ) - 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( , ) - const inputs = screen.getAllByTestId('input-combined') + const inputs = screen.getAllByRole('textbox') expect(inputs).toHaveLength(3) }) @@ -178,7 +158,7 @@ describe('InfoGroup', () => { , ) - 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( , ) - 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( , ) - 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( , ) - 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( , ) - 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( , ) - expect(screen.getByTestId('field'))!.toBeInTheDocument() + expect(screen.getByText('field'))!.toBeInTheDocument() }) }) }) diff --git a/web/app/components/datasets/metadata/metadata-document/info-group.tsx b/web/app/components/datasets/metadata/metadata-document/info-group.tsx index 0368aa18b6..3c4c8729f1 100644 --- a/web/app/components/datasets/metadata/metadata-document/info-group.tsx +++ b/web/app/components/datasets/metadata/metadata-document/info-group.tsx @@ -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 = ({
{isEdit && (
- - } - 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 && }
diff --git a/web/app/components/datasets/metadata/utils/__tests__/get-icon.spec.ts b/web/app/components/datasets/metadata/utils/__tests__/get-icon.spec.ts index 07eef6c320..737401cfcb 100644 --- a/web/app/components/datasets/metadata/utils/__tests__/get-icon.spec.ts +++ b/web/app/components/datasets/metadata/utils/__tests__/get-icon.spec.ts @@ -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') }) }) }) diff --git a/web/app/components/datasets/metadata/utils/get-icon.ts b/web/app/components/datasets/metadata/utils/get-icon.ts index 2d446e5eed..6c39b7ec9c 100644 --- a/web/app/components/datasets/metadata/utils/get-icon.ts +++ b/web/app/components/datasets/metadata/utils/get-icon.ts @@ -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]: '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] }