Merge branch 'feat/sidebar-role-based-visibility' into 'main'

feat(sidebar): role-based visibility для /reviews

See merge request 2-6/2-6-4/terravault/ordinis!166
This commit is contained in:
Александр Зимин
2026-05-13 10:54:24 +00:00
@@ -14,6 +14,10 @@ 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 {
useCanReviewRecord,
useCanReviewSchema,
} from '@/auth/usePermissions'
import { import {
useDictionaries, useDictionaries,
useMyDrafts, useMyDrafts,
@@ -52,12 +56,24 @@ type NavItem = {
badge?: React.ReactNode badge?: React.ReactNode
/** Только аутентифицированным пользователям. */ /** Только аутентифицированным пользователям. */
authRequired?: boolean authRequired?: boolean
/**
* Visible только если user имеет хотя бы одну из workflow ролей
* (или {@code ordinis:admin} super-role). Hide если нет.
*/
reviewerOnly?: boolean
} }
export function Sidebar() { export function Sidebar() {
const { t } = useTranslation() const { t } = useTranslation()
const auth = useAuth() const auth = useAuth()
const canMutate = useCanMutate() const canMutate = useCanMutate()
// Phase 1d role-based sidebar visibility. /reviews пункт показываем
// только тем у кого есть schema:reviewer OR record:reviewer OR
// ordinis:admin super-role. Иначе пункт скрыт — у user'а нет смысла
// его видеть (clicking всё равно даст empty queue / 403).
const canReviewSchema = useCanReviewSchema()
const canReviewRecord = useCanReviewRecord()
const isReviewer = canReviewSchema || canReviewRecord
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)
@@ -105,6 +121,7 @@ export function Sidebar() {
icon: ClipboardList, icon: ClipboardList,
badge: reviewsCount > 0 ? reviewsCount : undefined, badge: reviewsCount > 0 ? reviewsCount : undefined,
authRequired: true, authRequired: true,
reviewerOnly: true,
}, },
{ {
to: '/outbox', to: '/outbox',
@@ -129,7 +146,11 @@ export function Sidebar() {
const visibleSections = sections const visibleSections = sections
.map((s) => ({ .map((s) => ({
...s, ...s,
items: s.items.filter((i) => !i.authRequired || canMutate), items: s.items.filter((i) => {
if (i.authRequired && !canMutate) return false
if (i.reviewerOnly && !isReviewer) return false
return true
}),
})) }))
.filter((s) => s.items.length > 0) .filter((s) => s.items.length > 0)
@@ -418,6 +439,9 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
const { t } = useTranslation() const { t } = useTranslation()
const auth = useAuth() const auth = useAuth()
const canMutate = useCanMutate() const canMutate = useCanMutate()
const canReviewSchema = useCanReviewSchema()
const canReviewRecord = useCanReviewRecord()
const isReviewer = canReviewSchema || canReviewRecord
const dictsQuery = useDictionaries() const dictsQuery = useDictionaries()
const myDrafts = useMyDrafts(0, 1) const myDrafts = useMyDrafts(0, 1)
const reviews = useReviewsBadgeCount(auth.isAuthenticated) const reviews = useReviewsBadgeCount(auth.isAuthenticated)
@@ -455,6 +479,7 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
icon: ClipboardList, icon: ClipboardList,
badge: reviewsCount > 0 ? reviewsCount : undefined, badge: reviewsCount > 0 ? reviewsCount : undefined,
authRequired: true, authRequired: true,
reviewerOnly: true,
}, },
{ {
to: '/outbox', to: '/outbox',
@@ -476,7 +501,14 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
}, },
] ]
const visibleSections = sections const visibleSections = sections
.map((s) => ({ ...s, items: s.items.filter((i) => !i.authRequired || canMutate) })) .map((s) => ({
...s,
items: s.items.filter((i) => {
if (i.authRequired && !canMutate) return false
if (i.reviewerOnly && !isReviewer) return false
return true
}),
}))
.filter((s) => s.items.length > 0) .filter((s) => s.items.length > 0)
return ( return (