mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
feat: introduce CardTags component for displaying tags in cards
This commit is contained in:
parent
08508b006d
commit
434eb0e924
@ -5,7 +5,7 @@ type Props = {
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
const CardMoreInfoComponent = ({
|
||||
const CardTagsComponent = ({
|
||||
tags,
|
||||
}: Props) => {
|
||||
return (
|
||||
@ -29,6 +29,6 @@ const CardMoreInfoComponent = ({
|
||||
}
|
||||
|
||||
// Memoize to prevent unnecessary re-renders when tags array hasn't changed
|
||||
const CardMoreInfo = React.memo(CardMoreInfoComponent)
|
||||
const CardTags = React.memo(CardTagsComponent)
|
||||
|
||||
export default CardMoreInfo
|
||||
export default CardTags
|
||||
@ -11,7 +11,7 @@ import DownloadCount from './base/download-count'
|
||||
import OrgInfo from './base/org-info'
|
||||
import Placeholder, { LoadingPlaceholder } from './base/placeholder'
|
||||
import Title from './base/title'
|
||||
import CardMoreInfo from './card-more-info'
|
||||
import CardTags from './card-tags'
|
||||
// ================================
|
||||
// Import Components Under Test
|
||||
// ================================
|
||||
@ -642,9 +642,9 @@ describe('Card', () => {
|
||||
})
|
||||
|
||||
// ================================
|
||||
// CardMoreInfo Component Tests
|
||||
// CardTags Component Tests
|
||||
// ================================
|
||||
describe('CardMoreInfo', () => {
|
||||
describe('CardTags', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
@ -654,66 +654,24 @@ describe('CardMoreInfo', () => {
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<CardMoreInfo downloadCount={100} tags={['tag1']} />)
|
||||
render(<CardTags tags={['tag1']} />)
|
||||
|
||||
expect(document.body).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render download count when provided', () => {
|
||||
render(<CardMoreInfo downloadCount={1000} tags={[]} />)
|
||||
it('should render tags in uppercase', () => {
|
||||
render(<CardTags tags={['search', 'image']} />)
|
||||
|
||||
expect(screen.getByText('1,000')).toBeInTheDocument()
|
||||
expect(screen.getByText('SEARCH')).toBeInTheDocument()
|
||||
expect(screen.getByText('IMAGE')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tags when provided', () => {
|
||||
render(<CardMoreInfo tags={['search', 'image']} />)
|
||||
it('should render at most two tags', () => {
|
||||
render(<CardTags tags={['one', 'two', 'three']} />)
|
||||
|
||||
expect(screen.getByText('search')).toBeInTheDocument()
|
||||
expect(screen.getByText('image')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render both download count and tags with separator', () => {
|
||||
render(<CardMoreInfo downloadCount={500} tags={['tag1']} />)
|
||||
|
||||
expect(screen.getByText('500')).toBeInTheDocument()
|
||||
expect(screen.getByText('·')).toBeInTheDocument()
|
||||
expect(screen.getByText('tag1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Props Testing
|
||||
// ================================
|
||||
describe('Props', () => {
|
||||
it('should not render download count when undefined', () => {
|
||||
render(<CardMoreInfo tags={['tag1']} />)
|
||||
|
||||
expect(screen.queryByTestId('ri-install-line')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render separator when download count is undefined', () => {
|
||||
render(<CardMoreInfo tags={['tag1']} />)
|
||||
|
||||
expect(screen.queryByText('·')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render separator when tags are empty', () => {
|
||||
render(<CardMoreInfo downloadCount={100} tags={[]} />)
|
||||
|
||||
expect(screen.queryByText('·')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render hash symbol before each tag', () => {
|
||||
render(<CardMoreInfo tags={['search']} />)
|
||||
|
||||
expect(screen.getByText('#')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should set title attribute with hash prefix for tags', () => {
|
||||
render(<CardMoreInfo tags={['search']} />)
|
||||
|
||||
const tagElement = screen.getByTitle('# search')
|
||||
expect(tagElement).toBeInTheDocument()
|
||||
expect(screen.getByText('ONE')).toBeInTheDocument()
|
||||
expect(screen.getByText('TWO')).toBeInTheDocument()
|
||||
expect(screen.queryByText('THREE')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -722,54 +680,8 @@ describe('CardMoreInfo', () => {
|
||||
// ================================
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized with React.memo', () => {
|
||||
expect(CardMoreInfo).toBeDefined()
|
||||
expect(typeof CardMoreInfo).toBe('object')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle zero download count', () => {
|
||||
render(<CardMoreInfo downloadCount={0} tags={[]} />)
|
||||
|
||||
// 0 should still render since downloadCount is defined
|
||||
expect(screen.getByText('0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty tags array', () => {
|
||||
render(<CardMoreInfo downloadCount={100} tags={[]} />)
|
||||
|
||||
expect(screen.queryByText('#')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle large download count', () => {
|
||||
render(<CardMoreInfo downloadCount={1234567890} tags={[]} />)
|
||||
|
||||
expect(screen.getByText('1,234,567,890')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle many tags', () => {
|
||||
const tags = Array.from({ length: 10 }, (_, i) => `tag${i}`)
|
||||
render(<CardMoreInfo downloadCount={100} tags={tags} />)
|
||||
|
||||
expect(screen.getByText('tag0')).toBeInTheDocument()
|
||||
expect(screen.getByText('tag9')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle tags with special characters', () => {
|
||||
render(<CardMoreInfo tags={['tag-with-dash', 'tag_with_underscore']} />)
|
||||
|
||||
expect(screen.getByText('tag-with-dash')).toBeInTheDocument()
|
||||
expect(screen.getByText('tag_with_underscore')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should truncate long tag names', () => {
|
||||
const longTag = 'a'.repeat(200)
|
||||
const { container } = render(<CardMoreInfo tags={[longTag]} />)
|
||||
|
||||
expect(container.querySelector('.truncate')).toBeInTheDocument()
|
||||
expect(CardTags).toBeDefined()
|
||||
expect(typeof CardTags).toBe('object')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1688,7 +1600,7 @@ describe('Icon', () => {
|
||||
render(
|
||||
<Card
|
||||
payload={plugin}
|
||||
footer={<CardMoreInfo downloadCount={5000} tags={['search', 'api']} />}
|
||||
footer={<CardTags tags={['search', 'api']} />}
|
||||
/>,
|
||||
)
|
||||
|
||||
@ -1700,9 +1612,8 @@ describe('Icon', () => {
|
||||
expect(screen.getByText('Tool')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('partner-badge')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
|
||||
expect(screen.getByText('5,000')).toBeInTheDocument()
|
||||
expect(screen.getByText('search')).toBeInTheDocument()
|
||||
expect(screen.getByText('api')).toBeInTheDocument()
|
||||
expect(screen.getByText('SEARCH')).toBeInTheDocument()
|
||||
expect(screen.getByText('API')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading state correctly', () => {
|
||||
@ -1728,12 +1639,12 @@ describe('Icon', () => {
|
||||
<Card
|
||||
payload={plugin}
|
||||
installed={true}
|
||||
footer={<CardMoreInfo downloadCount={100} tags={['tag1']} />}
|
||||
footer={<CardTags tags={['tag1']} />}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('ri-check-line')).toBeInTheDocument()
|
||||
expect(screen.getByText('100')).toBeInTheDocument()
|
||||
expect(screen.getByText('TAG1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1817,9 +1728,9 @@ describe('Icon', () => {
|
||||
})
|
||||
|
||||
it('should have title attribute on tags', () => {
|
||||
render(<CardMoreInfo downloadCount={100} tags={['search']} />)
|
||||
render(<CardTags tags={['search']} />)
|
||||
|
||||
expect(screen.getByTitle('# search')).toBeInTheDocument()
|
||||
expect(screen.getByTitle('search')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have semantic structure', () => {
|
||||
@ -1864,11 +1775,11 @@ describe('Icon', () => {
|
||||
expect(endTime - startTime).toBeLessThan(1000)
|
||||
})
|
||||
|
||||
it('should handle CardMoreInfo with many tags', () => {
|
||||
it('should handle CardTags with many tags', () => {
|
||||
const tags = Array.from({ length: 20 }, (_, i) => `tag-${i}`)
|
||||
|
||||
const startTime = performance.now()
|
||||
render(<CardMoreInfo downloadCount={1000} tags={tags} />)
|
||||
render(<CardTags tags={tags} />)
|
||||
const endTime = performance.now()
|
||||
|
||||
expect(endTime - startTime).toBeLessThan(100)
|
||||
|
||||
@ -322,11 +322,10 @@ vi.mock('@/app/components/plugins/card', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock CardMoreInfo component
|
||||
vi.mock('@/app/components/plugins/card/card-more-info', () => ({
|
||||
default: ({ downloadCount, tags }: { downloadCount: number, tags: string[] }) => (
|
||||
<div data-testid="card-more-info">
|
||||
<span data-testid="download-count">{downloadCount}</span>
|
||||
// Mock CardTags component
|
||||
vi.mock('@/app/components/plugins/card/card-tags', () => ({
|
||||
default: ({ tags }: { tags: string[] }) => (
|
||||
<div data-testid="card-tags">
|
||||
<span data-testid="tags">{tags.join(',')}</span>
|
||||
</div>
|
||||
),
|
||||
|
||||
@ -8,7 +8,7 @@ import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Card from '@/app/components/plugins/card'
|
||||
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
||||
import CardTags from '@/app/components/plugins/card/card-tags'
|
||||
import { useTags } from '@/app/components/plugins/hooks'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import { getPluginDetailLinkInMarketplace, getPluginLinkInMarketplace } from '../utils'
|
||||
@ -49,7 +49,7 @@ const CardWrapperComponent = ({
|
||||
key={plugin.name}
|
||||
payload={plugin}
|
||||
footer={(
|
||||
<CardMoreInfo
|
||||
<CardTags
|
||||
tags={tagLabels}
|
||||
/>
|
||||
)}
|
||||
@ -95,7 +95,7 @@ const CardWrapperComponent = ({
|
||||
payload={plugin}
|
||||
disableOrgLink
|
||||
footer={(
|
||||
<CardMoreInfo
|
||||
<CardTags
|
||||
tags={tagLabels}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -131,11 +131,10 @@ vi.mock('@/app/components/plugins/card', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock CardMoreInfo component
|
||||
vi.mock('@/app/components/plugins/card/card-more-info', () => ({
|
||||
default: ({ downloadCount, tags }: { downloadCount: number, tags: string[] }) => (
|
||||
<div data-testid="card-more-info">
|
||||
<span data-testid="download-count">{downloadCount}</span>
|
||||
// Mock CardTags component
|
||||
vi.mock('@/app/components/plugins/card/card-tags', () => ({
|
||||
default: ({ tags }: { tags: string[] }) => (
|
||||
<div data-testid="card-tags">
|
||||
<span data-testid="tags">{tags.join(',')}</span>
|
||||
</div>
|
||||
),
|
||||
@ -983,7 +982,7 @@ describe('CardWrapper (via List integration)', () => {
|
||||
expect(screen.getByTestId('card-test-plugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render CardMoreInfo with download count and tags', () => {
|
||||
it('should render CardTags with tags', () => {
|
||||
const plugin = createMockPlugin({
|
||||
name: 'test-plugin',
|
||||
install_count: 5000,
|
||||
@ -998,8 +997,7 @@ describe('CardWrapper (via List integration)', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('card-more-info')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('download-count')).toHaveTextContent('5000')
|
||||
expect(screen.getByTestId('card-tags')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||
import Card from '@/app/components/plugins/card'
|
||||
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
||||
import CardTags from '@/app/components/plugins/card/card-tags'
|
||||
import { useTags } from '@/app/components/plugins/hooks'
|
||||
import Empty from '@/app/components/plugins/marketplace/empty'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
@ -183,7 +183,7 @@ const ProviderList = () => {
|
||||
name: collection.plugin_id ? collection.plugin_id.split('/')[1] : collection.name,
|
||||
} as any}
|
||||
footer={(
|
||||
<CardMoreInfo
|
||||
<CardTags
|
||||
tags={collection.labels?.map(label => getTagLabel(label)) || []}
|
||||
/>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user