fix(oa-board): prevent production unified table redirect

This commit is contained in:
WuYongJie 2026-09-01 11:10:59 +08:00
parent d5f2b68d88
commit b01579ca99
5 changed files with 92 additions and 7 deletions

View File

@ -42,6 +42,7 @@ import type {
} from '@/server/services/oa-brand-planning-board.service'
import SampleProgressClient from '@/app/intake/sample-progress/SampleProgressClient'
import type { SampleProgressDTO } from '@/server/services/sample-progress.service'
import { getTimeoutFollowupBoardKeys } from './oaBoardTimeoutFollowupRequest'
type FilterState = {
keyword: string
@ -4121,7 +4122,7 @@ export default function OaBoardClient({
)
useEffect(() => {
const boardKeys = Array.from(new Set(items.map((item) => item.boardKey).filter(Boolean))).slice(0, 500)
const boardKeys = getTimeoutFollowupBoardKeys(pagedItems)
if (boardKeys.length === 0) {
setTimeoutFollowups([])
return
@ -4161,7 +4162,7 @@ export default function OaBoardClient({
return () => {
cancelled = true
}
}, [items, permissionScope])
}, [pagedItems, permissionScope])
const metricCardClass = (isActive: boolean) =>
[
'group flex h-[72px] items-center justify-between rounded-md border px-3.5 text-left transition',

View File

@ -0,0 +1,22 @@
import { getTimeoutFollowupBoardKeys } from './oaBoardTimeoutFollowupRequest'
describe('getTimeoutFollowupBoardKeys', () => {
it('deduplicates and trims the visible page keys', () => {
expect(
getTimeoutFollowupBoardKeys([
{ boardKey: ' flow-1 ' },
{ boardKey: 'flow-1' },
{ boardKey: 'flow-2' },
{ boardKey: '' },
{ boardKey: null },
]),
).toEqual(['flow-1', 'flow-2'])
})
it('keeps the request bounded even when the dashboard has many records', () => {
const items = Array.from({ length: 600 }, (_, index) => ({ boardKey: `flow-${index}` }))
expect(getTimeoutFollowupBoardKeys(items)).toHaveLength(500)
expect(getTimeoutFollowupBoardKeys(items)[499]).toBe('flow-499')
})
})

View File

@ -0,0 +1,12 @@
type BoardKeyItem = { boardKey: string | null | undefined }
/** Keep the follow-up lookup bounded to the records currently visible on the page. */
export function getTimeoutFollowupBoardKeys(items: readonly BoardKeyItem[]) {
return Array.from(
new Set(
items
.map((item) => item.boardKey?.trim() ?? '')
.filter(Boolean),
),
).slice(0, 500)
}

View File

@ -75,6 +75,21 @@ interface AuthMeResponse {
authenticated?: boolean
user?: WorkspaceShellUser
}
error?: {
code?: string
}
}
export function isExplicitAuthFailureResponse(
response: Pick<Response, 'status'>,
payload: AuthMeResponse | null,
) {
return Boolean(
payload?.data?.authenticated === false ||
response.status === 401 ||
response.status === 403 ||
payload?.error?.code?.startsWith('AUTH_'),
)
}
function isProductWorkbenchRoute(pathname: string) {
@ -188,11 +203,11 @@ function AppShellContent({ children, user, localOaBoardPreview = false }: AppShe
return
}
setFetchedUser(null)
} catch {
if (active) {
if (isExplicitAuthFailureResponse(response, payload)) {
setFetchedUser(null)
}
} catch {
// A transient network/proxy failure must not turn a valid session into a redirect.
}
})()

View File

@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react'
import { AppShell, shouldUseAppShell } from '../AppShell'
import { render, screen, waitFor } from '@testing-library/react'
import { AppShell, isExplicitAuthFailureResponse, shouldUseAppShell } from '../AppShell'
const usePathnameMock = jest.fn()
const replaceMock = jest.fn()
@ -238,6 +238,41 @@ describe('AppShell', () => {
expect(screen.getByText('intake content')).toBeInTheDocument()
})
it('仅把明确的认证失败响应视为未登录', () => {
expect(
isExplicitAuthFailureResponse(
{ status: 401 },
{ success: false, error: { code: 'AUTH_002' } },
),
).toBe(true)
expect(
isExplicitAuthFailureResponse(
{ status: 500 },
{ success: false, error: { code: 'SYSTEM_001' } },
),
).toBe(false)
})
it('鉴权网络失败时不触发默认工作区跳转', async () => {
usePathnameMock.mockReturnValue('/supply-chain/new-products/unified-table')
const originalFetch = global.fetch
global.fetch = jest.fn(() => Promise.reject(new Error('network unavailable')))
try {
render(
<AppShell>
<div>unified table content</div>
</AppShell>,
)
await waitFor(() => expect(global.fetch).toHaveBeenCalledWith('/api/auth/me', { cache: 'no-store' }))
expect(replaceMock).not.toHaveBeenCalled()
expect(screen.getByText('unified table content')).toBeInTheDocument()
} finally {
global.fetch = originalFetch
}
})
it('桌面供应链沿用自身布局,不渲染产品开发侧栏', () => {
usePathnameMock.mockReturnValue('/supply-chain/dashboard')