mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
feat: introduce CardTags component for displaying tags in cards
This commit is contained in:
parent
6329647f3b
commit
9a698eaad9
@ -5,7 +5,7 @@ type Props = {
|
|||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardMoreInfoComponent = ({
|
const CardTagsComponent = ({
|
||||||
tags,
|
tags,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
@ -29,6 +29,6 @@ const CardMoreInfoComponent = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Memoize to prevent unnecessary re-renders when tags array hasn't changed
|
// 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 OrgInfo from './base/org-info'
|
||||||
import Placeholder, { LoadingPlaceholder } from './base/placeholder'
|
import Placeholder, { LoadingPlaceholder } from './base/placeholder'
|
||||||
import Title from './base/title'
|
import Title from './base/title'
|
||||||
import CardMoreInfo from './card-more-info'
|
import CardTags from './card-tags'
|
||||||
// ================================
|
// ================================
|
||||||
// Import Components Under Test
|
// Import Components Under Test
|
||||||
// ================================
|
// ================================
|
||||||
@ -642,9 +642,9 @@ describe('Card', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
// CardMoreInfo Component Tests
|
// CardTags Component Tests
|
||||||
// ================================
|
// ================================
|
||||||
describe('CardMoreInfo', () => {
|
describe('CardTags', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
@ -654,66 +654,24 @@ describe('CardMoreInfo', () => {
|
|||||||
// ================================
|
// ================================
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('should render without crashing', () => {
|
it('should render without crashing', () => {
|
||||||
render(<CardMoreInfo downloadCount={100} tags={['tag1']} />)
|
render(<CardTags tags={['tag1']} />)
|
||||||
|
|
||||||
expect(document.body).toBeInTheDocument()
|
expect(document.body).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render download count when provided', () => {
|
it('should render tags in uppercase', () => {
|
||||||
render(<CardMoreInfo downloadCount={1000} tags={[]} />)
|
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', () => {
|
it('should render at most two tags', () => {
|
||||||
render(<CardMoreInfo tags={['search', 'image']} />)
|
render(<CardTags tags={['one', 'two', 'three']} />)
|
||||||
|
|
||||||
expect(screen.getByText('search')).toBeInTheDocument()
|
expect(screen.getByText('ONE')).toBeInTheDocument()
|
||||||
expect(screen.getByText('image')).toBeInTheDocument()
|
expect(screen.getByText('TWO')).toBeInTheDocument()
|
||||||
})
|
expect(screen.queryByText('THREE')).not.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()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -722,54 +680,8 @@ describe('CardMoreInfo', () => {
|
|||||||
// ================================
|
// ================================
|
||||||
describe('Memoization', () => {
|
describe('Memoization', () => {
|
||||||
it('should be memoized with React.memo', () => {
|
it('should be memoized with React.memo', () => {
|
||||||
expect(CardMoreInfo).toBeDefined()
|
expect(CardTags).toBeDefined()
|
||||||
expect(typeof CardMoreInfo).toBe('object')
|
expect(typeof CardTags).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()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1688,7 +1600,7 @@ describe('Icon', () => {
|
|||||||
render(
|
render(
|
||||||
<Card
|
<Card
|
||||||
payload={plugin}
|
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.getByText('Tool')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('partner-badge')).toBeInTheDocument()
|
expect(screen.getByTestId('partner-badge')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
|
expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
|
||||||
expect(screen.getByText('5,000')).toBeInTheDocument()
|
expect(screen.getByText('SEARCH')).toBeInTheDocument()
|
||||||
expect(screen.getByText('search')).toBeInTheDocument()
|
expect(screen.getByText('API')).toBeInTheDocument()
|
||||||
expect(screen.getByText('api')).toBeInTheDocument()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render loading state correctly', () => {
|
it('should render loading state correctly', () => {
|
||||||
@ -1728,12 +1639,12 @@ describe('Icon', () => {
|
|||||||
<Card
|
<Card
|
||||||
payload={plugin}
|
payload={plugin}
|
||||||
installed={true}
|
installed={true}
|
||||||
footer={<CardMoreInfo downloadCount={100} tags={['tag1']} />}
|
footer={<CardTags tags={['tag1']} />}
|
||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(screen.getByTestId('ri-check-line')).toBeInTheDocument()
|
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', () => {
|
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', () => {
|
it('should have semantic structure', () => {
|
||||||
@ -1864,11 +1775,11 @@ describe('Icon', () => {
|
|||||||
expect(endTime - startTime).toBeLessThan(1000)
|
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 tags = Array.from({ length: 20 }, (_, i) => `tag-${i}`)
|
||||||
|
|
||||||
const startTime = performance.now()
|
const startTime = performance.now()
|
||||||
render(<CardMoreInfo downloadCount={1000} tags={tags} />)
|
render(<CardTags tags={tags} />)
|
||||||
const endTime = performance.now()
|
const endTime = performance.now()
|
||||||
|
|
||||||
expect(endTime - startTime).toBeLessThan(100)
|
expect(endTime - startTime).toBeLessThan(100)
|
||||||
|
|||||||
@ -322,11 +322,10 @@ vi.mock('@/app/components/plugins/card', () => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock CardMoreInfo component
|
// Mock CardTags component
|
||||||
vi.mock('@/app/components/plugins/card/card-more-info', () => ({
|
vi.mock('@/app/components/plugins/card/card-tags', () => ({
|
||||||
default: ({ downloadCount, tags }: { downloadCount: number, tags: string[] }) => (
|
default: ({ tags }: { tags: string[] }) => (
|
||||||
<div data-testid="card-more-info">
|
<div data-testid="card-tags">
|
||||||
<span data-testid="download-count">{downloadCount}</span>
|
|
||||||
<span data-testid="tags">{tags.join(',')}</span>
|
<span data-testid="tags">{tags.join(',')}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import * as React from 'react'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Card from '@/app/components/plugins/card'
|
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 { useTags } from '@/app/components/plugins/hooks'
|
||||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||||
import { getPluginDetailLinkInMarketplace, getPluginLinkInMarketplace } from '../utils'
|
import { getPluginDetailLinkInMarketplace, getPluginLinkInMarketplace } from '../utils'
|
||||||
@ -49,7 +49,7 @@ const CardWrapperComponent = ({
|
|||||||
key={plugin.name}
|
key={plugin.name}
|
||||||
payload={plugin}
|
payload={plugin}
|
||||||
footer={(
|
footer={(
|
||||||
<CardMoreInfo
|
<CardTags
|
||||||
tags={tagLabels}
|
tags={tagLabels}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -95,7 +95,7 @@ const CardWrapperComponent = ({
|
|||||||
payload={plugin}
|
payload={plugin}
|
||||||
disableOrgLink
|
disableOrgLink
|
||||||
footer={(
|
footer={(
|
||||||
<CardMoreInfo
|
<CardTags
|
||||||
tags={tagLabels}
|
tags={tagLabels}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -131,11 +131,10 @@ vi.mock('@/app/components/plugins/card', () => ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock CardMoreInfo component
|
// Mock CardTags component
|
||||||
vi.mock('@/app/components/plugins/card/card-more-info', () => ({
|
vi.mock('@/app/components/plugins/card/card-tags', () => ({
|
||||||
default: ({ downloadCount, tags }: { downloadCount: number, tags: string[] }) => (
|
default: ({ tags }: { tags: string[] }) => (
|
||||||
<div data-testid="card-more-info">
|
<div data-testid="card-tags">
|
||||||
<span data-testid="download-count">{downloadCount}</span>
|
|
||||||
<span data-testid="tags">{tags.join(',')}</span>
|
<span data-testid="tags">{tags.join(',')}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@ -983,7 +982,7 @@ describe('CardWrapper (via List integration)', () => {
|
|||||||
expect(screen.getByTestId('card-test-plugin')).toBeInTheDocument()
|
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({
|
const plugin = createMockPlugin({
|
||||||
name: 'test-plugin',
|
name: 'test-plugin',
|
||||||
install_count: 5000,
|
install_count: 5000,
|
||||||
@ -998,8 +997,7 @@ describe('CardWrapper (via List integration)', () => {
|
|||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(screen.getByTestId('card-more-info')).toBeInTheDocument()
|
expect(screen.getByTestId('card-tags')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('download-count')).toHaveTextContent('5000')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||||
import Card from '@/app/components/plugins/card'
|
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 { useTags } from '@/app/components/plugins/hooks'
|
||||||
import Empty from '@/app/components/plugins/marketplace/empty'
|
import Empty from '@/app/components/plugins/marketplace/empty'
|
||||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
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,
|
name: collection.plugin_id ? collection.plugin_id.split('/')[1] : collection.name,
|
||||||
} as any}
|
} as any}
|
||||||
footer={(
|
footer={(
|
||||||
<CardMoreInfo
|
<CardTags
|
||||||
tags={collection.labels?.map(label => getTagLabel(label)) || []}
|
tags={collection.labels?.map(label => getTagLabel(label)) || []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user