From b62a487037c7c7b91fc39057e93b9a3a7d72b67f Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:42:00 +0800 Subject: [PATCH 1/2] fix(web): preserve dashboard return path from skill details Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- web/src/pages/dashboard/stars.test.ts | 37 ++++++++++-- web/src/pages/dashboard/stars.tsx | 9 ++- web/src/pages/dashboard/subscriptions.test.ts | 60 +++++++++++++++++++ web/src/pages/dashboard/subscriptions.tsx | 9 ++- 4 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 web/src/pages/dashboard/subscriptions.test.ts diff --git a/web/src/pages/dashboard/stars.test.ts b/web/src/pages/dashboard/stars.test.ts index 20e7279f..e54171b5 100644 --- a/web/src/pages/dashboard/stars.test.ts +++ b/web/src/pages/dashboard/stars.test.ts @@ -1,7 +1,18 @@ -import { describe, expect, it, vi } from 'vitest' +// @vitest-environment jsdom + +import { createElement } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const navigate = vi.fn() vi.mock('@tanstack/react-router', () => ({ - useNavigate: () => vi.fn(), + useNavigate: () => navigate, + useLocation: () => ({ + pathname: '/dashboard/stars', + searchStr: '?page=2', + hash: '#saved', + }), })) vi.mock('react-i18next', async () => { @@ -15,7 +26,7 @@ vi.mock('react-i18next', async () => { }) vi.mock('@/features/skill/skill-card', () => ({ - SkillCard: () => null, + SkillCard: ({ onClick }: { onClick?: () => void }) => createElement('button', { onClick }, 'skill-card'), })) vi.mock('@/shared/components/pagination', () => ({ @@ -24,7 +35,12 @@ vi.mock('@/shared/components/pagination', () => ({ vi.mock('@/shared/hooks/use-user-queries', () => ({ useMyStarsPage: () => ({ - data: { items: [], total: 0, page: 0, size: 12 }, + data: { + items: [{ id: 1, namespace: 'team-a', slug: 'demo skill' }], + total: 1, + page: 0, + size: 12, + }, isLoading: false, }), })) @@ -40,7 +56,20 @@ vi.mock('@/shared/components/dashboard-page-header', () => ({ import { MyStarsPage } from './stars' describe('MyStarsPage', () => { + beforeEach(() => navigate.mockClear()) + it('exports a named component function', () => { expect(typeof MyStarsPage).toBe('function') }) + + it('preserves the favorites page when opening a skill', () => { + render(createElement(MyStarsPage)) + + fireEvent.click(screen.getByRole('button', { name: 'skill-card' })) + + expect(navigate).toHaveBeenCalledWith({ + to: '/space/team-a/demo%20skill', + search: { returnTo: '/dashboard/stars?page=2#saved' }, + }) + }) }) diff --git a/web/src/pages/dashboard/stars.tsx b/web/src/pages/dashboard/stars.tsx index 36a5dc33..7c07bef6 100644 --- a/web/src/pages/dashboard/stars.tsx +++ b/web/src/pages/dashboard/stars.tsx @@ -1,17 +1,19 @@ import { useState } from 'react' -import { useNavigate } from '@tanstack/react-router' +import { useLocation, useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { SkillCard } from '@/features/skill/skill-card' import { Pagination } from '@/shared/components/pagination' import { useMyStarsPage } from '@/shared/hooks/use-user-queries' import { Card } from '@/shared/ui/card' import { DashboardPageHeader } from '@/shared/components/dashboard-page-header' +import { buildReturnTo } from '@/shared/lib/auth-route' const PAGE_SIZE = 12 export function MyStarsPage() { const { t } = useTranslation() const navigate = useNavigate() + const location = useLocation() const [page, setPage] = useState(0) const { data, isLoading } = useMyStarsPage({ page, size: PAGE_SIZE }) const skills = data?.items ?? [] @@ -40,7 +42,10 @@ export function MyStarsPage() { navigate({ to: `/space/${skill.namespace}/${encodeURIComponent(skill.slug)}` })} + onClick={() => navigate({ + to: `/space/${skill.namespace}/${encodeURIComponent(skill.slug)}`, + search: { returnTo: buildReturnTo(location) }, + })} /> ))} diff --git a/web/src/pages/dashboard/subscriptions.test.ts b/web/src/pages/dashboard/subscriptions.test.ts new file mode 100644 index 00000000..9dd3b2c1 --- /dev/null +++ b/web/src/pages/dashboard/subscriptions.test.ts @@ -0,0 +1,60 @@ +// @vitest-environment jsdom + +import { createElement } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const navigate = vi.fn() + +vi.mock('@tanstack/react-router', () => ({ + useNavigate: () => navigate, + useLocation: () => ({ + pathname: '/dashboard/subscriptions', + searchStr: '?page=1', + hash: '', + }), +})) + +vi.mock('react-i18next', async () => { + const actual = await vi.importActual('react-i18next') + return { + ...actual, + useTranslation: () => ({ t: (key: string) => key }), + } +}) + +vi.mock('@/features/skill/skill-card', () => ({ + SkillCard: ({ onClick }: { onClick?: () => void }) => createElement('button', { onClick }, 'skill-card'), +})) + +vi.mock('@/shared/components/pagination', () => ({ Pagination: () => null })) +vi.mock('@/shared/hooks/use-user-queries', () => ({ + useMySubscriptionsPage: () => ({ + data: { + items: [{ id: 1, namespace: 'team-a', slug: 'demo-skill' }], + total: 1, + page: 0, + size: 12, + }, + isLoading: false, + }), +})) +vi.mock('@/shared/ui/card', () => ({ Card: ({ children }: { children: unknown }) => children })) +vi.mock('@/shared/components/dashboard-page-header', () => ({ DashboardPageHeader: () => null })) + +import { MySubscriptionsPage } from './subscriptions' + +describe('MySubscriptionsPage', () => { + beforeEach(() => navigate.mockClear()) + + it('preserves the subscriptions page when opening a skill', () => { + render(createElement(MySubscriptionsPage)) + + fireEvent.click(screen.getByRole('button', { name: 'skill-card' })) + + expect(navigate).toHaveBeenCalledWith({ + to: '/space/team-a/demo-skill', + search: { returnTo: '/dashboard/subscriptions?page=1' }, + }) + }) +}) diff --git a/web/src/pages/dashboard/subscriptions.tsx b/web/src/pages/dashboard/subscriptions.tsx index 6c0523dd..d128ccc1 100644 --- a/web/src/pages/dashboard/subscriptions.tsx +++ b/web/src/pages/dashboard/subscriptions.tsx @@ -1,17 +1,19 @@ import { useState } from 'react' -import { useNavigate } from '@tanstack/react-router' +import { useLocation, useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { SkillCard } from '@/features/skill/skill-card' import { Pagination } from '@/shared/components/pagination' import { useMySubscriptionsPage } from '@/shared/hooks/use-user-queries' import { Card } from '@/shared/ui/card' import { DashboardPageHeader } from '@/shared/components/dashboard-page-header' +import { buildReturnTo } from '@/shared/lib/auth-route' const PAGE_SIZE = 12 export function MySubscriptionsPage() { const { t } = useTranslation() const navigate = useNavigate() + const location = useLocation() const [page, setPage] = useState(0) const { data, isLoading } = useMySubscriptionsPage({ page, size: PAGE_SIZE }) const skills = data?.items ?? [] @@ -40,7 +42,10 @@ export function MySubscriptionsPage() { navigate({ to: `/space/${skill.namespace}/${encodeURIComponent(skill.slug)}` })} + onClick={() => navigate({ + to: `/space/${skill.namespace}/${encodeURIComponent(skill.slug)}`, + search: { returnTo: buildReturnTo(location) }, + })} /> ))} From 5045901c9e59591f0bdaf5204909c0bea6c7fbf6 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:27:51 +0800 Subject: [PATCH 2/2] fix(web): retain dashboard pagination on detail return Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- web/src/app/router.tsx | 6 ++++ web/src/pages/dashboard/stars.test.ts | 28 +++++++++++++------ web/src/pages/dashboard/stars.tsx | 15 +++++++--- web/src/pages/dashboard/subscriptions.test.ts | 28 +++++++++++++------ web/src/pages/dashboard/subscriptions.tsx | 15 +++++++--- 5 files changed, 66 insertions(+), 26 deletions(-) diff --git a/web/src/app/router.tsx b/web/src/app/router.tsx index 1a2d175b..64df10cf 100644 --- a/web/src/app/router.tsx +++ b/web/src/app/router.tsx @@ -393,6 +393,9 @@ const dashboardStarsRoute = createRoute({ getParentRoute: () => rootRoute, path: 'dashboard/stars', beforeLoad: requireAuth, + validateSearch: (search: Record): { page?: number } => ({ + page: typeof search.page === 'number' && search.page > 0 ? search.page : undefined, + }), component: MyStarsPage, }) @@ -400,6 +403,9 @@ const dashboardSubscriptionsRoute = createRoute({ getParentRoute: () => rootRoute, path: 'dashboard/subscriptions', beforeLoad: requireAuth, + validateSearch: (search: Record): { page?: number } => ({ + page: typeof search.page === 'number' && search.page > 0 ? search.page : undefined, + }), component: MySubscriptionsPage, }) diff --git a/web/src/pages/dashboard/stars.test.ts b/web/src/pages/dashboard/stars.test.ts index e54171b5..907e5935 100644 --- a/web/src/pages/dashboard/stars.test.ts +++ b/web/src/pages/dashboard/stars.test.ts @@ -5,9 +5,21 @@ import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' const navigate = vi.fn() +const { useMyStarsPage } = vi.hoisted(() => ({ + useMyStarsPage: vi.fn(() => ({ + data: { + items: [{ id: 1, namespace: 'team-a', slug: 'demo skill' }], + total: 1, + page: 0, + size: 12, + }, + isLoading: false, + })), +})) vi.mock('@tanstack/react-router', () => ({ useNavigate: () => navigate, + useSearch: () => ({ page: 2 }), useLocation: () => ({ pathname: '/dashboard/stars', searchStr: '?page=2', @@ -34,15 +46,7 @@ vi.mock('@/shared/components/pagination', () => ({ })) vi.mock('@/shared/hooks/use-user-queries', () => ({ - useMyStarsPage: () => ({ - data: { - items: [{ id: 1, namespace: 'team-a', slug: 'demo skill' }], - total: 1, - page: 0, - size: 12, - }, - isLoading: false, - }), + useMyStarsPage, })) vi.mock('@/shared/ui/card', () => ({ @@ -72,4 +76,10 @@ describe('MyStarsPage', () => { search: { returnTo: '/dashboard/stars?page=2#saved' }, }) }) + + it('uses the URL page as the query source', () => { + render(createElement(MyStarsPage)) + + expect(useMyStarsPage).toHaveBeenCalledWith({ page: 2, size: 12 }) + }) }) diff --git a/web/src/pages/dashboard/stars.tsx b/web/src/pages/dashboard/stars.tsx index 7c07bef6..cbaacab5 100644 --- a/web/src/pages/dashboard/stars.tsx +++ b/web/src/pages/dashboard/stars.tsx @@ -1,5 +1,4 @@ -import { useState } from 'react' -import { useLocation, useNavigate } from '@tanstack/react-router' +import { useLocation, useNavigate, useSearch } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { SkillCard } from '@/features/skill/skill-card' import { Pagination } from '@/shared/components/pagination' @@ -14,7 +13,8 @@ export function MyStarsPage() { const { t } = useTranslation() const navigate = useNavigate() const location = useLocation() - const [page, setPage] = useState(0) + const search = useSearch({ from: '/dashboard/stars' }) + const page = search.page ?? 0 const { data, isLoading } = useMyStarsPage({ page, size: PAGE_SIZE }) const skills = data?.items ?? [] const totalPages = data ? Math.max(Math.ceil(data.total / data.size), 1) : 1 @@ -50,7 +50,14 @@ export function MyStarsPage() { ))} {data && data.total > PAGE_SIZE ? ( - + navigate({ + to: '/dashboard/stars', + search: { page: nextPage > 0 ? nextPage : undefined }, + })} + /> ) : null} )} diff --git a/web/src/pages/dashboard/subscriptions.test.ts b/web/src/pages/dashboard/subscriptions.test.ts index 9dd3b2c1..922278ed 100644 --- a/web/src/pages/dashboard/subscriptions.test.ts +++ b/web/src/pages/dashboard/subscriptions.test.ts @@ -5,9 +5,21 @@ import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' const navigate = vi.fn() +const { useMySubscriptionsPage } = vi.hoisted(() => ({ + useMySubscriptionsPage: vi.fn(() => ({ + data: { + items: [{ id: 1, namespace: 'team-a', slug: 'demo-skill' }], + total: 1, + page: 0, + size: 12, + }, + isLoading: false, + })), +})) vi.mock('@tanstack/react-router', () => ({ useNavigate: () => navigate, + useSearch: () => ({ page: 1 }), useLocation: () => ({ pathname: '/dashboard/subscriptions', searchStr: '?page=1', @@ -29,15 +41,7 @@ vi.mock('@/features/skill/skill-card', () => ({ vi.mock('@/shared/components/pagination', () => ({ Pagination: () => null })) vi.mock('@/shared/hooks/use-user-queries', () => ({ - useMySubscriptionsPage: () => ({ - data: { - items: [{ id: 1, namespace: 'team-a', slug: 'demo-skill' }], - total: 1, - page: 0, - size: 12, - }, - isLoading: false, - }), + useMySubscriptionsPage, })) vi.mock('@/shared/ui/card', () => ({ Card: ({ children }: { children: unknown }) => children })) vi.mock('@/shared/components/dashboard-page-header', () => ({ DashboardPageHeader: () => null })) @@ -57,4 +61,10 @@ describe('MySubscriptionsPage', () => { search: { returnTo: '/dashboard/subscriptions?page=1' }, }) }) + + it('uses the URL page as the query source', () => { + render(createElement(MySubscriptionsPage)) + + expect(useMySubscriptionsPage).toHaveBeenCalledWith({ page: 1, size: 12 }) + }) }) diff --git a/web/src/pages/dashboard/subscriptions.tsx b/web/src/pages/dashboard/subscriptions.tsx index d128ccc1..85955a28 100644 --- a/web/src/pages/dashboard/subscriptions.tsx +++ b/web/src/pages/dashboard/subscriptions.tsx @@ -1,5 +1,4 @@ -import { useState } from 'react' -import { useLocation, useNavigate } from '@tanstack/react-router' +import { useLocation, useNavigate, useSearch } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { SkillCard } from '@/features/skill/skill-card' import { Pagination } from '@/shared/components/pagination' @@ -14,7 +13,8 @@ export function MySubscriptionsPage() { const { t } = useTranslation() const navigate = useNavigate() const location = useLocation() - const [page, setPage] = useState(0) + const search = useSearch({ from: '/dashboard/subscriptions' }) + const page = search.page ?? 0 const { data, isLoading } = useMySubscriptionsPage({ page, size: PAGE_SIZE }) const skills = data?.items ?? [] const totalPages = data ? Math.max(Math.ceil(data.total / data.size), 1) : 1 @@ -50,7 +50,14 @@ export function MySubscriptionsPage() { ))} {data && data.total > PAGE_SIZE ? ( - + navigate({ + to: '/dashboard/subscriptions', + search: { page: nextPage > 0 ? nextPage : undefined }, + })} + /> ) : null} )}