From fe8a0cb21ff673ad17db7cf644232a5198d28ac7 Mon Sep 17 00:00:00 2001
From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Date: Mon, 31 Aug 2026 11:58:11 +0800
Subject: [PATCH] fix(promotion): clamp emptied queue pages
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---
web/src/pages/dashboard/promotions.test.tsx | 30 ++++++++++++++++++++-
web/src/pages/dashboard/promotions.tsx | 16 ++++++++++-
2 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/web/src/pages/dashboard/promotions.test.tsx b/web/src/pages/dashboard/promotions.test.tsx
index a1666c47..7f924af9 100644
--- a/web/src/pages/dashboard/promotions.test.tsx
+++ b/web/src/pages/dashboard/promotions.test.tsx
@@ -1,5 +1,5 @@
/** @vitest-environment jsdom */
-import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'
+import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { PromotionStatus, PromotionTask } from '@/api/types'
@@ -246,6 +246,34 @@ describe('PromotionsPage', () => {
expect(screen.getByRole('button', { name: 'pagination:0/2' })).toBeTruthy()
})
+ it('returns to the last valid page when a mutation empties the current page', async () => {
+ const pending = createPromotion()
+ mocks.usePromotionList.mockImplementation((params: { status?: PromotionStatus; page?: number } = {}) => {
+ const page = params.page ?? 0
+ return {
+ data: {
+ items: params.status === 'PENDING' && page === 0 ? [pending] : [],
+ total: params.status === 'PENDING' ? 20 : 0,
+ page,
+ size: 20,
+ },
+ isLoading: false,
+ }
+ })
+ render()
+
+ mocks.paginationProps[0]?.onPageChange(1)
+
+ await waitFor(() => {
+ expect(mocks.usePromotionList).toHaveBeenLastCalledWith({
+ status: 'PENDING',
+ page: 0,
+ size: 20,
+ })
+ })
+ expect(screen.getByText('Knowledge Helper')).toBeTruthy()
+ })
+
it('renders approved history as a sortable table', () => {
render()
diff --git a/web/src/pages/dashboard/promotions.tsx b/web/src/pages/dashboard/promotions.tsx
index ba1ad811..5ac266eb 100644
--- a/web/src/pages/dashboard/promotions.tsx
+++ b/web/src/pages/dashboard/promotions.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useApprovePromotion, usePromotionList, useRejectPromotion } from '@/features/promotion/use-promotion-list'
import { DashboardPageHeader } from '@/shared/components/dashboard-page-header'
@@ -79,6 +79,18 @@ function PromotionPagination({ data, onPageChange }: { data: PromotionPage; onPa
return
}
+function useClampPromotionPage(data: PromotionPage | undefined, page: number, onPageChange: (page: number) => void) {
+ useEffect(() => {
+ if (!data) {
+ return
+ }
+ const totalPages = data.size > 0 ? Math.ceil(data.total / data.size) : 0
+ if (page > 0 && page >= totalPages) {
+ onPageChange(Math.max(0, totalPages - 1))
+ }
+ }, [data, onPageChange, page])
+}
+
function PendingPromotionCard({
item,
comment,
@@ -141,6 +153,7 @@ function PendingPromotionList({ page, onPageChange }: { page: number; onPageChan
const approveMutation = useApprovePromotion()
const rejectMutation = useRejectPromotion()
const [commentById, setCommentById] = useState>({})
+ useClampPromotionPage(data, page, onPageChange)
if (isLoading) {
return
@@ -192,6 +205,7 @@ function PromotionHistoryTable({
})
const nextDirection = sortDirection === 'DESC' ? 'ASC' : 'DESC'
const sortLabel = nextDirection === 'ASC' ? t('promotions.sortReviewedTimeAsc') : t('promotions.sortReviewedTimeDesc')
+ useClampPromotionPage(data, page, onPageChange)
if (isLoading) {
return (