feat(web): empty list of snippet

This commit is contained in:
JzoNg 2026-04-15 16:53:37 +08:00
parent b7fe45d800
commit b5dc774093
4 changed files with 55 additions and 25 deletions

View File

@ -2,6 +2,8 @@ import { render, screen } from '@testing-library/react'
import * as React from 'react'
import Empty from '../empty'
const defaultMessage = 'workflow.tabs.noSnippetsFound'
describe('Empty', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -9,32 +11,32 @@ describe('Empty', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Empty />)
expect(screen.getByText('app.newApp.noAppsFound')).toBeInTheDocument()
render(<Empty message={defaultMessage} />)
expect(screen.getByText(defaultMessage)).toBeInTheDocument()
})
it('should render 36 placeholder cards', () => {
const { container } = render(<Empty />)
const { container } = render(<Empty message={defaultMessage} />)
const placeholderCards = container.querySelectorAll('.bg-background-default-lighter')
expect(placeholderCards).toHaveLength(36)
})
it('should display the no apps found message', () => {
render(<Empty />)
it('should display the provided message', () => {
render(<Empty message="app.newApp.noAppsFound" />)
expect(screen.getByText('app.newApp.noAppsFound')).toBeInTheDocument()
})
})
describe('Styling', () => {
it('should have correct container styling for overlay', () => {
const { container } = render(<Empty />)
const { container } = render(<Empty message={defaultMessage} />)
const overlay = container.querySelector('.pointer-events-none')
expect(overlay).toBeInTheDocument()
expect(overlay).toHaveClass('absolute', 'inset-0', 'z-20')
})
it('should have correct styling for placeholder cards', () => {
const { container } = render(<Empty />)
const { container } = render(<Empty message={defaultMessage} />)
const card = container.querySelector('.bg-background-default-lighter')
expect(card).toHaveClass('inline-flex', 'h-[160px]', 'rounded-xl')
})
@ -42,10 +44,10 @@ describe('Empty', () => {
describe('Edge Cases', () => {
it('should handle multiple renders without issues', () => {
const { rerender } = render(<Empty />)
expect(screen.getByText('app.newApp.noAppsFound')).toBeInTheDocument()
const { rerender } = render(<Empty message={defaultMessage} />)
expect(screen.getByText(defaultMessage)).toBeInTheDocument()
rerender(<Empty />)
rerender(<Empty message="app.newApp.noAppsFound" />)
expect(screen.getByText('app.newApp.noAppsFound')).toBeInTheDocument()
})
})

View File

@ -258,8 +258,8 @@ vi.mock('../new-app-card', () => ({
}))
vi.mock('../empty', () => ({
default: () => {
return React.createElement('div', { 'data-testid': 'empty-state', 'role': 'status' }, 'No apps found')
default: ({ message }: { message: string }) => {
return React.createElement('div', { 'data-testid': 'empty-state', 'role': 'status' }, message)
},
}))
@ -296,6 +296,26 @@ const renderList = (props: React.ComponentProps<typeof List> = {}, searchParams
describe('List', () => {
beforeEach(() => {
vi.clearAllMocks()
defaultSnippetData.pages[0].data = [
{
id: 'snippet-1',
name: 'Tone Rewriter',
description: 'Rewrites rough drafts into a concise, professional tone for internal stakeholder updates.',
type: 'node',
is_published: false,
use_count: 19,
icon_info: {
icon_type: 'emoji',
icon: '🪄',
icon_background: '#E0EAFF',
icon_url: '',
},
created_at: 1704067200,
updated_at: '2024-01-02 10:00',
author: '',
},
]
defaultSnippetData.pages[0].total = 1
useTagStore.setState({
tagList: [{ id: 'tag-1', name: 'Test Tag', type: 'app', binding_count: 0 }],
showTagManagementModal: false,
@ -444,5 +464,14 @@ describe('List', () => {
expect(mockFetchSnippetNextPage).not.toHaveBeenCalled()
})
it('should reuse the shared empty state when no snippets are available', () => {
defaultSnippetData.pages[0].data = []
defaultSnippetData.pages[0].total = 0
renderList({ pageType: 'snippets' })
expect(screen.getByTestId('empty-state')).toHaveTextContent('workflow.tabs.noSnippetsFound')
})
})
})

View File

@ -1,5 +1,4 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
const DefaultCards = React.memo(() => {
const renderArray = Array.from({ length: 36 })
@ -17,15 +16,17 @@ const DefaultCards = React.memo(() => {
)
})
const Empty = () => {
const { t } = useTranslation()
type Props = {
message: string
}
const Empty = ({ message }: Props) => {
return (
<>
<DefaultCards />
<div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-linear-to-t from-background-body to-transparent">
<span className="system-md-medium text-text-tertiary">
{t('newApp.noAppsFound', { ns: 'app' })}
{message}
</span>
</div>
</>

View File

@ -228,12 +228,16 @@ const List: FC<Props> = ({
const hasAnyApp = (data?.pages?.[0]?.total ?? 0) > 0
const hasAnySnippet = snippetItems.length > 0
const currentKeywords = isAppsPage ? keywords : snippetKeywordsInput
const showEmptyState = !showSkeleton && (isAppsPage ? !hasAnyApp : !hasAnySnippet)
const emptyStateMessage = isAppsPage
? t('newApp.noAppsFound', { ns: 'app' })
: t('tabs.noSnippetsFound', { ns: 'workflow' })
return (
<>
<div ref={containerRef} className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body">
{dragging && (
<div className="inset-0 absolute z-50 m-0.5 rounded-2xl border-2 border-dashed border-components-dropzone-border-accent bg-[rgba(21,90,239,0.14)] p-2" />
<div className="absolute inset-0 z-50 m-0.5 rounded-2xl border-2 border-dashed border-components-dropzone-border-accent bg-[rgba(21,90,239,0.14)] p-2" />
)}
<div className="sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pt-7 pb-5">
@ -273,7 +277,7 @@ const List: FC<Props> = ({
<div className={cn(
'relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 2k:grid-cols-6 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5',
isAppsPage && !hasAnyApp && 'overflow-hidden',
showEmptyState && 'overflow-hidden',
)}
>
{(isCurrentWorkspaceEditor || isLoadingCurrentWorkspace) && (
@ -300,13 +304,7 @@ const List: FC<Props> = ({
<SnippetCard key={snippet.id} snippet={snippet} />
))}
{!showSkeleton && isAppsPage && !hasAnyApp && <Empty />}
{!showSkeleton && !isAppsPage && !hasAnySnippet && (
<div className="col-span-full flex min-h-[240px] items-center justify-center rounded-xl border border-dashed border-divider-regular bg-components-card-bg p-6 text-center text-sm text-text-tertiary">
{t('tabs.noSnippetsFound', { ns: 'workflow' })}
</div>
)}
{showEmptyState && <Empty message={emptyStateMessage} />}
{isAppsPage && isFetchingNextPage && (
<AppCardSkeleton count={3} />