import { useCallback, useRef } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { apiClient, type BulkCloseRequest, type BulkCloseResponse, type CascadeCloseResult, type CreateDictionaryRequest, type CreateRecordRequest, type CreateSchemaDraftRequest, type CreateWebhookSubscriptionRequest, type DecisionRequest, type DictionaryDetail, type DraftResponse, type PublishSchemaDraftRequest, type PublishSchemaDraftResult, type RecordResponse, type SchemaDraft, type SubmitDraftRequest, type SubmitForReviewRequest, type UpdateSchemaDraftRequest, type WebhookDelivery, type WebhookSubscription, 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) давал * два разных ключа → server создавал два состояния вместо идемпотентного * deduping. Теперь caller генерирует ключ один раз на форму через * {@link useFormIdempotencyKey} и передаёт его mutation'у. */ export const generateIdempotencyKey = (): string => typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}` /** * Stable idempotency key для жизненного цикла одной формы. * *
Возвращает {@code [key, reset]}. {@code key} стабилен между ререндерами; * {@code reset()} генерирует новый — вызывается на onSuccess чтобы следующая * форма (например, edit того же record после save) получила свежий ключ. * *
Защита от двойного submit (Enter + click за одну сессию формы): оба * запроса несут один и тот же {@code Idempotency-Key} → backend deduplicate'ит * на втором. * *
Usage: *
{@code
* const [idempotencyKey, resetIdempotencyKey] = useFormIdempotencyKey()
* const createMut = useCreateRecord(name)
* // submit:
* createMut.mutate(
* { payload: req, idempotencyKey },
* { onSuccess: () => { resetIdempotencyKey(); closeModal() } },
* )
* }
*/
export const useFormIdempotencyKey = (): [string, () => void] => {
const ref = useRefBackend errors: *
onSuccess invalidates: active-draft query (banner picks up) +
* drafts list + reviewer pool queue.
*/
export const useCreateSchemaDraft = (dictionaryName: string) => {
const qc = useQueryClient()
return useMutation({
mutationFn: async (payload: CreateSchemaDraftRequest): Promise onSuccess invalidates dict detail (schema/version changed) + changelog +
* draft queries.
*/
export const usePublishSchemaDraft = (dictionaryName: string) => {
const qc = useQueryClient()
return useMutation({
mutationFn: async (params: {
draftId: string
body: PublishSchemaDraftRequest
}): Promise