Phase 4 last optional UX из design doc — bulk operations:
- Multi-select column в reviewer queue (Checkbox per row + select-all
header). Set<draftId> selection state, persisted между refetch'ами.
- "Approve выбранные" — confirm dialog → parallel approveDraft mutations
через Promise.all. Каждая mutation independent: если одна fail
(409 self_approve_forbidden / draft_not_pending race), остальные
продолжают.
- "Reject выбранные" — Modal с TextArea для shared reason (required,
matches backend reject_reason_required validation). Reason одинаковый
для всех — bulk обычно "duplicate registration" / "wrong scope" / etc.
- Bulk result Alert: approved count / rejected count / failed list с
per-draft reason'ами (err code из 409). User видит точно что прошло
и что нет.
- Auto-clear selection после bulk operation. Queue refetch'ит через
refetchInterval=30s, плюс mutation onSuccess invalidate'ит ['drafts'].
i18n RU/EN: 22 keys (selectAll/selectRow/selectedCount/approveSelected/
rejectSelected/clear/confirmApprove/rejectModal*/confirmReject/cancel/
resultTitle/approvedCount/rejectedCount/failedCount). Pluralization
через i18next plurals.
Why ship without soak data:
- Implementation не зависит от queue depth — это просто loop'ит над
выбранными drafts.
- Reviewer на любом dict с queue >5 уже выгадывает время.
- Если queue staying small после soak — feature не bothering нас (UI
toolbar показывается только когда anySelected).
Tests: vitest 89/89, tsc clean, prod build clean.