feat(records): bulk export CSV выбранных + pagination
Backend POST /api/v1/dictionaries/{name}/records/export-selected.csv:
принимает businessKeys[] (1..500), возвращает CSV с колонками
business_key, valid_from, valid_to, scope, data (JSONB inline).
Не-найденные ключи отдаются строкой с пустыми ячейками — admin видит
что ключ обработан, но данных нет. RFC 4180 экранирование.
requireReadAccess: для экспорта достаточно прав чтения (не write).
POST а не GET потому что >100 ключей не помещается в URL params
(Server: Request URI Too Long).
Frontend useBulkExportRecords: axios POST с responseType: blob →
temporary <a download> link → клик → revokeObjectURL через rAF.
Filename берётся из Content-Disposition header.
Toolbar: новая кнопка "Экспорт CSV" (variant=secondary) рядом с
"Закрыть выбранные" (variant=danger). Loading state через isPending.
Pagination для records list (PAGE_SIZE=50, client-side):
- visibleRecords = filteredRecords.slice(...)
- Footer "Записи 1–50 из 100, Стр. 1 из 2" с Prev/Next кнопками
- Auto-reset на page=0 при изменении filter/scope/AOI
- selectAll/visibleKeys теперь = только current page (WYSIWYG)
- При >50 записей видно навигацию, при <=50 footer скрыт
Прогон: 20/20 e2e + 60/60 rest-api + 89/89 admin-ui.
This commit is contained in:
@@ -123,6 +123,41 @@ export const useBulkCloseRecords = (dictionaryName: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk export selected: выгрузить N выбранных записей в CSV. Backend
|
||||
* возвращает text/csv blob с auth header, фронтенд триггерит download
|
||||
* через temporary `<a>` link.
|
||||
*
|
||||
* POST (не GET): >100 ключей не помещается в URL params.
|
||||
*/
|
||||
export const useBulkExportRecords = (dictionaryName: string) => {
|
||||
return useMutation({
|
||||
mutationFn: async (businessKeys: string[]): Promise<void> => {
|
||||
const response = await apiClient.post(
|
||||
`/dictionaries/${dictionaryName}/records/export-selected.csv`,
|
||||
{ businessKeys },
|
||||
{ responseType: 'blob' },
|
||||
)
|
||||
// Извлекаем 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`
|
||||
|
||||
// Trigger browser download через blob URL + temporary <a download>
|
||||
const url = window.URL.createObjectURL(response.data as Blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
// Через rAF чтобы download успел запуститься до revoke
|
||||
requestAnimationFrame(() => window.URL.revokeObjectURL(url))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// === Webhooks (v2) ===
|
||||
|
||||
export const useCreateWebhook = () => {
|
||||
|
||||
Reference in New Issue
Block a user