From f94f941fe85cacf373d662898d05d2ea36729794 Mon Sep 17 00:00:00 2001 From: YuTengjing Date: Sat, 16 May 2026 20:20:32 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20style(home):=20polish=20brief=20?= =?UTF-8?q?recommendations=20layout=20(#14871)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/react/SKILL.md | 17 +- .../DailyBrief/__tests__/index.test.tsx | 147 ++++++++++++++++++ src/features/DailyBrief/index.tsx | 16 +- src/features/Recommendations/index.tsx | 17 +- 4 files changed, 170 insertions(+), 27 deletions(-) create mode 100644 src/features/DailyBrief/__tests__/index.test.tsx diff --git a/.agents/skills/react/SKILL.md b/.agents/skills/react/SKILL.md index 9bac31b731..b180bc1a25 100644 --- a/.agents/skills/react/SKILL.md +++ b/.agents/skills/react/SKILL.md @@ -85,11 +85,12 @@ errorElement: ; ## Common Mistakes -| Mistake | Fix | -| ---------------------------------------- | ------------------------------------------------------ | -| Using `next/link` in SPA | Use `react-router-dom` `Link` | -| Using antd directly | Use `@lobehub/ui/base-ui` first, then `@lobehub/ui` | -| `createStyles` for static styles | Use `createStaticStyles` + `cssVar` | -| Editing only `desktopRouter.config.tsx` | Must edit both `.tsx` and `.desktop.tsx` | -| Using `margin` for flex spacing | Use `gap` prop on Flexbox | -| Accessing zustand store without selector | Use selectors to access store data (see zustand skill) | +| Mistake | Fix | +| ----------------------------------------------------------------- | ----------------------------------------------------------------- | +| Using `next/link` in SPA | Use `react-router-dom` `Link` | +| Using antd directly | Use `@lobehub/ui/base-ui` first, then `@lobehub/ui` | +| `createStyles` for static styles | Use `createStaticStyles` + `cssVar` | +| Editing only `desktopRouter.config.tsx` | Must edit both `.tsx` and `.desktop.tsx` | +| Using `margin` for flex spacing | Use `gap` prop on Flexbox | +| Accessing zustand store without selector | Use selectors to access store data (see zustand skill) | +| Text or icon-text actions built with `Flexbox`/`Text` + `onClick` | Use `Button type={'text'} size={'small'}` with `icon` when needed | diff --git a/src/features/DailyBrief/__tests__/index.test.tsx b/src/features/DailyBrief/__tests__/index.test.tsx new file mode 100644 index 0000000000..a6a66286df --- /dev/null +++ b/src/features/DailyBrief/__tests__/index.test.tsx @@ -0,0 +1,147 @@ +import { render, screen } from '@testing-library/react'; +import type { ReactNode } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import DailyBrief from '..'; + +interface MockBrief { + id: string; + title: string; +} + +const mocks = vi.hoisted(() => ({ + state: { + briefs: [] as MockBrief[], + isBriefsInit: true, + isLogin: true, + recommendationsVisible: true, + }, + useFetchBriefs: vi.fn(), +})); + +vi.mock('@lobehub/ui', () => ({ + Button: ({ children, onClick }: { children: ReactNode; onClick?: () => void }) => ( + + ), + Flexbox: ({ children }: { children: ReactNode }) =>
{children}
, +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const map: Record = { + 'brief.title': 'Brief', + 'brief.viewAllTasks': 'View all tasks', + }; + return map[key] || key; + }, + }), +})); + +vi.mock('react-router-dom', () => ({ + useNavigate: () => vi.fn(), +})); + +vi.mock('@/features/AgentTasks/AgentTaskDetail/TopicChatDrawer', () => ({ + default: () =>
, +})); + +vi.mock('@/features/DocumentModal/Preview', () => ({ + default: () =>
, +})); + +vi.mock('@/features/Recommendations', () => ({ + default: () =>
Recommendations
, + useRecommendationsVisible: () => mocks.state.recommendationsVisible, +})); + +vi.mock('@/routes/(main)/home/features/components/GroupBlock', () => ({ + default: ({ + action, + children, + title, + }: { + action?: ReactNode; + children: ReactNode; + title?: ReactNode; + }) => ( +
+
+ {title} + {action} +
+ {children} +
+ ), +})); + +vi.mock('@/store/brief', () => ({ + useBriefStore: ( + selector: (state: { + briefs: MockBrief[]; + isBriefsInit: boolean; + useFetchBriefs: typeof mocks.useFetchBriefs; + }) => unknown, + ) => + selector({ + briefs: mocks.state.briefs, + isBriefsInit: mocks.state.isBriefsInit, + useFetchBriefs: mocks.useFetchBriefs, + }), +})); + +vi.mock('@/store/brief/selectors', () => ({ + briefListSelectors: { + briefs: (state: { briefs: MockBrief[] }) => state.briefs, + isBriefsInit: (state: { isBriefsInit: boolean }) => state.isBriefsInit, + }, +})); + +vi.mock('@/store/user', () => ({ + useUserStore: (selector: (state: { isLogin: boolean }) => unknown) => + selector({ isLogin: mocks.state.isLogin }), +})); + +vi.mock('@/store/user/slices/auth/selectors', () => ({ + authSelectors: { + isLogin: (state: { isLogin: boolean }) => state.isLogin, + }, +})); + +vi.mock('../BriefCard', () => ({ + default: ({ brief }: { brief: MockBrief }) =>
{brief.title}
, +})); + +vi.mock('../BriefCardSkeleton', () => ({ + BriefCardSkeleton: () =>
Brief skeleton
, +})); + +beforeEach(() => { + mocks.state.briefs = []; + mocks.state.isBriefsInit = true; + mocks.state.isLogin = true; + mocks.state.recommendationsVisible = true; + mocks.useFetchBriefs.mockClear(); +}); + +describe('DailyBrief', () => { + it('renders recommendations without the brief group header when no briefs are available', () => { + render(); + + expect(screen.getByText('Recommendations')).toBeInTheDocument(); + expect(screen.queryByTestId('group-block')).not.toBeInTheDocument(); + expect(screen.queryByText('Brief')).not.toBeInTheDocument(); + expect(screen.queryByText('View all tasks')).not.toBeInTheDocument(); + }); + + it('renders the brief group header when briefs are available', () => { + mocks.state.briefs = [{ id: 'brief-1', title: 'Brief item' }]; + + render(); + + expect(screen.getByTestId('group-block')).toBeInTheDocument(); + expect(screen.getByText('Brief')).toBeInTheDocument(); + expect(screen.getByText('Brief item')).toBeInTheDocument(); + expect(screen.getByText('View all tasks')).toBeInTheDocument(); + }); +}); diff --git a/src/features/DailyBrief/index.tsx b/src/features/DailyBrief/index.tsx index e3e4a0ca22..ae59312f2c 100644 --- a/src/features/DailyBrief/index.tsx +++ b/src/features/DailyBrief/index.tsx @@ -41,21 +41,19 @@ const DailyBrief = memo(() => { ); } - if (briefs.length === 0 && !recommendationsVisible) return null; - - const showViewAllTasks = briefs.length > 0 || recommendationsVisible; + if (briefs.length === 0) { + return recommendationsVisible ? : null; + } return ( navigate('/tasks')}> - {t('brief.viewAllTasks')} - - ) : undefined + } > diff --git a/src/features/Recommendations/index.tsx b/src/features/Recommendations/index.tsx index 3290dd0ca0..5e9df45ac1 100644 --- a/src/features/Recommendations/index.tsx +++ b/src/features/Recommendations/index.tsx @@ -1,5 +1,4 @@ -import { Flexbox, Icon, Text } from '@lobehub/ui'; -import { cssVar } from 'antd-style'; +import { Button, Flexbox, Text } from '@lobehub/ui'; import { RefreshCw } from 'lucide-react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -39,16 +38,14 @@ const Recommendations = memo(() => { {t('recommendations.subtitle')} {taskTemplatesState.mode === 'cards' && ( - } + size={'small'} + type={'text'} onClick={taskTemplatesState.onRefresh} > - - {tTaskTemplate('action.refresh.button')} - + {tTaskTemplate('action.refresh.button')} + )}