fix(sidebar): подключить reviews badge к real count

Бэйдж 'На ревью' в sidebar (desktop + mobile) был захардкожен в 0 —
TODO comment ждал cheap /drafts/pending/count endpoint. На самом деле
existing /admin/reviews/pending и /admin/schema-reviews/pending уже
возвращают paged response с totalElements/total в metadata — request
size=1 даёт нужный counter с минимальным payload (~1 строка + 4
metadata field'а).

Новый hook useReviewsBadgeCount():
- Параллельно дёргает обе queue (record + schema)
- Суммирует totalElements (record) + total (schema)
- Полл каждые 60s (lighter чем 30s queue refresh — badge не critical)
- Gated на auth.isAuthenticated (anonymous не должен дёргать admin endpoint'ы)

Sidebar.tsx (оба варианта — desktop + mobile MobileSidebar):
- reviewsCount: 0 → reviews.total
This commit is contained in:
Zimin A.N.
2026-05-13 10:08:57 +03:00
parent 0d157de803
commit 252939f63a
2 changed files with 42 additions and 6 deletions
+34
View File
@@ -622,6 +622,40 @@ export const reviewQueueQuery = (page = 0, size = 50) =>
export const useReviewQueue = (page = 0, size = 50) => export const useReviewQueue = (page = 0, size = 50) =>
useQuery(reviewQueueQuery(page, size)) useQuery(reviewQueueQuery(page, size))
/**
* Lightweight count для sidebar badge — fetch only metadata (size=1).
*
* <p>Combines record-reviews + schema-reviews queues. Возвращает сумму pending
* drafts обоих видов. Поллится каждые 30s в синк с queue queries — reviewer
* увидит свежий counter при возврате на app даже если /reviews не открыт.
*
* <p>Gated на authenticated — anonymous не должен дёргать admin endpoint'ы
* (вернут 401, шум в логах). Когда Phase 1d Keycloak roles подключатся,
* этот gate можно сузить до approver-role (useCanReviewSchema).
*/
export const useReviewsBadgeCount = (enabled = true) => {
const recordQ = useQuery({
...reviewQueueQuery(0, 1),
enabled,
// Override interval: badge ОК с 60s — это не deep view, lower frequency
// снижает background load когда tab idle.
refetchInterval: 60_000,
})
const schemaQ = useQuery({
...schemaReviewQueueQuery(0, 1),
enabled,
refetchInterval: 60_000,
})
const recordCount = recordQ.data?.totalElements ?? 0
const schemaCount = schemaQ.data?.total ?? 0
return {
total: recordCount + schemaCount,
recordCount,
schemaCount,
isLoading: recordQ.isLoading || schemaQ.isLoading,
}
}
/** /**
* Pending drafts на конкретный dict — для бейджа "На review" в records list. * Pending drafts на конкретный dict — для бейджа "На review" в records list.
* Лёгкий poll каждые 30s. Backend возвращает только PENDING (фильтрация в SQL). * Лёгкий poll каждые 30s. Backend возвращает только PENDING (фильтрация в SQL).
@@ -14,7 +14,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { useAuth } from 'react-oidc-context' import { useAuth } from 'react-oidc-context'
import { useCanMutate } from '@/auth/useCanMutate' import { useCanMutate } from '@/auth/useCanMutate'
import { useDictionaries, useMyDrafts } from '@/api/queries' import { useDictionaries, useMyDrafts, useReviewsBadgeCount } from '@/api/queries'
import { Sheet, SheetContent } from '@/ui' import { Sheet, SheetContent } from '@/ui'
import { ThemeSwitch } from '@/components/layout/ThemeSwitch' import { ThemeSwitch } from '@/components/layout/ThemeSwitch'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -55,13 +55,14 @@ export function Sidebar() {
const dictsQuery = useDictionaries() const dictsQuery = useDictionaries()
// Loaded just for сидбар counter — light request, кэшируется. // Loaded just for сидбар counter — light request, кэшируется.
const myDrafts = useMyDrafts(0, 1) const myDrafts = useMyDrafts(0, 1)
// Reviews badge — суммирует record-reviews + schema-reviews pending queues
// через size=1 paged запросы (cheap: одна запись + metadata). Gated на
// authenticated, чтобы anonymous не пытался admin endpoint'ы.
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
const dictsCount = dictsQuery.data?.length ?? 0 const dictsCount = dictsQuery.data?.length ?? 0
const myDraftsCount = myDrafts.data?.totalElements ?? 0 const myDraftsCount = myDrafts.data?.totalElements ?? 0
// Reviews count бэйдж — TODO когда будет cheap endpoint /drafts/pending/count. const reviewsCount = reviews.total
// Сейчас GET /drafts/pending возвращает page, paged запрос на каждый mount
// sidebar'а — overkill. Skip badge.
const reviewsCount = 0
// Flat list per redesign/compact.html — no section labels, /graph removed // Flat list per redesign/compact.html — no section labels, /graph removed
// (now reachable via catalog toolbar "Граф ⇄" button). Order: Главная → // (now reachable via catalog toolbar "Граф ⇄" button). Order: Главная →
@@ -392,10 +393,11 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
const canMutate = useCanMutate() const canMutate = useCanMutate()
const dictsQuery = useDictionaries() const dictsQuery = useDictionaries()
const myDrafts = useMyDrafts(0, 1) const myDrafts = useMyDrafts(0, 1)
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
const dictsCount = dictsQuery.data?.length ?? 0 const dictsCount = dictsQuery.data?.length ?? 0
const myDraftsCount = myDrafts.data?.totalElements ?? 0 const myDraftsCount = myDrafts.data?.totalElements ?? 0
const reviewsCount = 0 const reviewsCount = reviews.total
// Flat list per redesign — same as desktop Sidebar above. // Flat list per redesign — same as desktop Sidebar above.
const sections: NavSection[] = [ const sections: NavSection[] = [