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 (