Merge branch 'fix/admin-self-approve-frontend' into 'main'
fix(reviews): admin может self-approve record draft через UI See merge request 2-6/2-6-4/terravault/ordinis!290
This commit is contained in:
@@ -105,12 +105,51 @@ function tokenRoles(accessToken: string | undefined | null): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True если у actor'а есть указанная role или super-role {@link ROLE_ADMIN}.
|
* True если role — admin-эквивалент. Tail-match по `-`/`_`/`:` →
|
||||||
* Admin grants full workflow access — convenience override для small teams.
|
* `ADMIN`. Зеркало backend `WorkflowRoles.isAdminRole`.
|
||||||
|
*
|
||||||
|
* <p>Matches: `admin`, `ordinis:admin`, `ordinis-admin`, `org:admin`,
|
||||||
|
* `global:admin`, `app_admin`. Юзер может настроить Keycloak с любой из
|
||||||
|
* этих conventions — UI gate должен работать одинаково.
|
||||||
|
*/
|
||||||
|
function isAdminRoleString(role: string): boolean {
|
||||||
|
if (!role) return false
|
||||||
|
const s = role.trim().toUpperCase()
|
||||||
|
const sep = Math.max(s.lastIndexOf('-'), s.lastIndexOf('_'), s.lastIndexOf(':'))
|
||||||
|
const tail = sep >= 0 ? s.substring(sep + 1) : s
|
||||||
|
return tail === 'ADMIN'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True если у актора есть admin-эквивалентная роль (tail-match) в любом
|
||||||
|
* claim path. Используется для UI gate'ов где нужно знать "может ли actor
|
||||||
|
* обойти workflow restriction" — e.g. self-approve override.
|
||||||
|
*/
|
||||||
|
function hasAdminRole(accessToken: string | undefined | null): boolean {
|
||||||
|
return tokenRoles(accessToken).some(isAdminRoleString)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True если у actor'а есть указанная role или **любая** admin-эквивалентная
|
||||||
|
* super-role (tail-match → ADMIN). Раньше matched только exact
|
||||||
|
* `'ordinis:admin'` — юзер с plain ролью `admin` не считался админом и UI
|
||||||
|
* блокировал Approve button. Теперь mirror'ит backend tail-match.
|
||||||
*/
|
*/
|
||||||
function hasRole(accessToken: string | undefined | null, role: string): boolean {
|
function hasRole(accessToken: string | undefined | null, role: string): boolean {
|
||||||
const roles = tokenRoles(accessToken)
|
const roles = tokenRoles(accessToken)
|
||||||
return roles.includes(role) || roles.includes(ROLE_ADMIN)
|
if (roles.includes(role)) return true
|
||||||
|
return roles.some(isAdminRoleString)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook variant: true если currentSub имеет admin-эквивалентную роль.
|
||||||
|
* Mirror backend `WorkflowRoles.isAdmin()` — используется для self-approve
|
||||||
|
* override и других UI gate'ов где admin = bypass workflow rules.
|
||||||
|
*/
|
||||||
|
export function useIsAdmin(): boolean {
|
||||||
|
const auth = useAuth()
|
||||||
|
if (!auth.isAuthenticated) return false
|
||||||
|
return hasAdminRole(auth.user?.access_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Может ли актор approve / request_changes / reject schema draft. */
|
/** Может ли актор approve / request_changes / reject schema draft. */
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from '@/ui'
|
} from '@/ui'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useAuth } from '@/auth/useAuth'
|
import { useAuth } from '@/auth/useAuth'
|
||||||
|
import { useIsAdmin } from '@/auth/usePermissions'
|
||||||
import { ArrowCounterClockwiseIcon, CheckCircleIcon, XCircleIcon } from '@phosphor-icons/react'
|
import { ArrowCounterClockwiseIcon, CheckCircleIcon, XCircleIcon } from '@phosphor-icons/react'
|
||||||
import {
|
import {
|
||||||
useDictionaries,
|
useDictionaries,
|
||||||
@@ -693,11 +694,16 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
|
|||||||
// 1. `isPending` = false: terminal draft (APPROVED/REJECTED/WITHDRAWN).
|
// 1. `isPending` = false: terminal draft (APPROVED/REJECTED/WITHDRAWN).
|
||||||
// Никаких actions — read-only diff + decision banner.
|
// Никаких actions — read-only diff + decision banner.
|
||||||
// 2. `isMineAndPending`: maker открыл свой собственный pending draft из
|
// 2. `isMineAndPending`: maker открыл свой собственный pending draft из
|
||||||
// "Мои" таба. Approve self запрещён backend'ом (self_approve_forbidden),
|
// "Мои" таба. Для regular maker'а Approve self запрещён backend'ом
|
||||||
// reject своего черновика бессмыслен — показываем Withdraw.
|
// (self_approve_forbidden), показываем Withdraw. Admin override
|
||||||
|
// (MR !280): backend разрешает self-approve если у actor'а admin
|
||||||
|
// роль → frontend тоже снимает блокировку, иначе UI скрывает кнопку
|
||||||
|
// которую backend бы пропустил.
|
||||||
// 3. Иначе — reviewer flow: approve/reject как было.
|
// 3. Иначе — reviewer flow: approve/reject как было.
|
||||||
|
const isAdmin = useIsAdmin()
|
||||||
const isPending = draft?.status === 'PENDING'
|
const isPending = draft?.status === 'PENDING'
|
||||||
const isMineAndPending = isPending && Boolean(currentSub) && draft?.makerId === currentSub
|
const isMineAndPending =
|
||||||
|
isPending && Boolean(currentSub) && draft?.makerId === currentSub && !isAdmin
|
||||||
|
|
||||||
const handleApprove = () => {
|
const handleApprove = () => {
|
||||||
if (!draftId) return
|
if (!draftId) return
|
||||||
|
|||||||
Reference in New Issue
Block a user