From b5dc7740935c339ce31eee9cdfc5ee51969b4f77 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 15 Apr 2026 16:53:37 +0800 Subject: [PATCH] feat(web): empty list of snippet --- .../components/apps/__tests__/empty.spec.tsx | 22 +++++++------ .../components/apps/__tests__/list.spec.tsx | 33 +++++++++++++++++-- web/app/components/apps/empty.tsx | 9 ++--- web/app/components/apps/list.tsx | 16 ++++----- 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/web/app/components/apps/__tests__/empty.spec.tsx b/web/app/components/apps/__tests__/empty.spec.tsx index 8dbbbc3ffb..2536d61006 100644 --- a/web/app/components/apps/__tests__/empty.spec.tsx +++ b/web/app/components/apps/__tests__/empty.spec.tsx @@ -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() - expect(screen.getByText('app.newApp.noAppsFound')).toBeInTheDocument() + render() + expect(screen.getByText(defaultMessage)).toBeInTheDocument() }) it('should render 36 placeholder cards', () => { - const { container } = render() + const { container } = render() const placeholderCards = container.querySelectorAll('.bg-background-default-lighter') expect(placeholderCards).toHaveLength(36) }) - it('should display the no apps found message', () => { - render() + it('should display the provided message', () => { + render() expect(screen.getByText('app.newApp.noAppsFound')).toBeInTheDocument() }) }) describe('Styling', () => { it('should have correct container styling for overlay', () => { - const { container } = render() + const { container } = render() 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() + const { container } = render() 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() - expect(screen.getByText('app.newApp.noAppsFound')).toBeInTheDocument() + const { rerender } = render() + expect(screen.getByText(defaultMessage)).toBeInTheDocument() - rerender() + rerender() expect(screen.getByText('app.newApp.noAppsFound')).toBeInTheDocument() }) }) diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index b316843e70..9e48ce84a8 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -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 = {}, 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') + }) }) }) diff --git a/web/app/components/apps/empty.tsx b/web/app/components/apps/empty.tsx index 0dee3c908a..0876101d79 100644 --- a/web/app/components/apps/empty.tsx +++ b/web/app/components/apps/empty.tsx @@ -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 ( <>
- {t('newApp.noAppsFound', { ns: 'app' })} + {message}
diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 47dcbf34ad..9b680c9087 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -228,12 +228,16 @@ const List: FC = ({ 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 ( <>
{dragging && ( -
+
)}
@@ -273,7 +277,7 @@ const List: FC = ({
{(isCurrentWorkspaceEditor || isLoadingCurrentWorkspace) && ( @@ -300,13 +304,7 @@ const List: FC = ({ ))} - {!showSkeleton && isAppsPage && !hasAnyApp && } - - {!showSkeleton && !isAppsPage && !hasAnySnippet && ( -
- {t('tabs.noSnippetsFound', { ns: 'workflow' })} -
- )} + {showEmptyState && } {isAppsPage && isFetchingNextPage && (