fix(oa-board): prevent production unified table redirect
This commit is contained in:
parent
d5f2b68d88
commit
b01579ca99
@ -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',
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
}
|
||||
@ -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.
|
||||
}
|
||||
})()
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user