fix(admin-ui): backlog sweep — 7 review items

This commit is contained in:
Александр Зимин
2026-05-10 19:21:33 +00:00
parent c7f6559bed
commit 48dd8a487a
6 changed files with 156 additions and 16 deletions
+47 -6
View File
@@ -17,6 +17,40 @@ import {
type WebhookTestPingResult,
} from './client'
/**
* Parse `Content-Disposition` filename (RFC 5987 + legacy):
* - `filename*=UTF-8''Cyrillic%20Name.csv` (RFC 5987 — для не-ASCII names)
* - `filename="legacy.csv"` (RFC 2183 — ASCII-only)
*
* Возвращает первое match'нувшее имя или null. RFC 5987 priority — стандарт
* требует UA prefer the encoded form when both present.
*/
export const parseContentDispositionFilename = (
cd: string | undefined,
): string | null => {
if (!cd) return null
// RFC 5987: filename*=charset''url-encoded-value
const rfc5987 = cd.match(/filename\*=(?:([^']*)'[^']*')?([^;]+)/i)
if (rfc5987) {
const charset = rfc5987[1] || 'UTF-8'
const raw = rfc5987[2].trim().replace(/^"(.*)"$/, '$1')
try {
// decodeURIComponent works для UTF-8 (и compatible сюда же ISO-8859-1).
// Если backend present extended-charset — fallback на raw value.
if (charset.toUpperCase() === 'UTF-8' || charset.toUpperCase() === 'ISO-8859-1') {
return decodeURIComponent(raw)
}
return raw
} catch {
return raw
}
}
// RFC 2183: filename="..." or filename=raw
const legacy = cd.match(/filename=("([^"]+)"|([^;]+))/i)
if (legacy) return (legacy[2] ?? legacy[3])?.trim() ?? null
return null
}
/**
* Generate UUID v4 (или fallback). Раньше вызывался **per mutate** в каждой
* useCreateRecord/useUpdateRecord — двойной submit формы (Enter + click) давал
@@ -186,7 +220,11 @@ export const useCascadeCloseRecord = (dictionaryName: string) => {
const { data } = await apiClient.post<CascadeCloseResult>(
`/dictionaries/${dictionaryName}/records/${encodeURIComponent(params.businessKey)}/cascade-close`,
null,
{ params: queryParams },
// Cascade tx до 30s на backend (см. 503 x_references_cascade_timeout).
// Дефолт apiClient timeout=10s слишком агрессивен — обрезает запрос
// ДО того как backend сам timeout'ает с понятной ошибкой. Per-request
// override на 60s гарантирует что user видит backend response.
{ params: queryParams, timeout: 60_000 },
)
return data
},
@@ -208,9 +246,12 @@ export const useBulkCloseRecords = (dictionaryName: string) => {
const qc = useQueryClient()
return useMutation({
mutationFn: async (req: BulkCloseRequest): Promise<BulkCloseResponse> => {
// Bulk close — per-record транзакция; на 1000+ keys запрос идёт >10s.
// Дефолт apiClient timeout слишком жёсткий → 60s safety.
const { data } = await apiClient.post<BulkCloseResponse>(
`/dictionaries/${dictionaryName}/records/bulk-close`,
req,
{ timeout: 60_000 },
)
return data
},
@@ -233,13 +274,13 @@ export const useBulkExportRecords = (dictionaryName: string) => {
const response = await apiClient.post(
`/dictionaries/${dictionaryName}/records/export-selected.csv`,
{ businessKeys },
{ responseType: 'blob' },
// 60s timeout: 10k+ записей export может идти >10s.
{ responseType: 'blob', timeout: 60_000 },
)
// Извлекаем filename из Content-Disposition если есть
const cd = response.headers['content-disposition'] as string | undefined
const match = cd?.match(/filename="?([^";]+)"?/)
const filename = match?.[1]
?? `${dictionaryName}-selected-${new Date().toISOString().slice(0, 10)}.csv`
const filename =
parseContentDispositionFilename(cd) ??
`${dictionaryName}-selected-${new Date().toISOString().slice(0, 10)}.csv`
// Trigger browser download через blob URL + temporary <a download>
const url = window.URL.createObjectURL(response.data as Blob)