Merge branch 'fix/schema-draft-author-display-and-withdraw-gate' into 'main'
fix(schema-draft): author display + gate Отозвать только maker'у See merge request 2-6/2-6-4/terravault/ordinis!158
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useAuth } from 'react-oidc-context'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Badge, Button, Drawer, LoadingBlock, QueryErrorState, TextArea, toast } from '@/ui'
|
import { Badge, Button, Drawer, LoadingBlock, QueryErrorState, TextArea, toast } from '@/ui'
|
||||||
import {
|
import {
|
||||||
@@ -241,6 +242,7 @@ export function SchemaDraftDrawer({
|
|||||||
inline т.к. Drawer sheet flow-based. */}
|
inline т.к. Drawer sheet flow-based. */}
|
||||||
<DraftActions
|
<DraftActions
|
||||||
status={draft.status}
|
status={draft.status}
|
||||||
|
makerId={draft.makerId}
|
||||||
onSubmitReview={onSubmitReview}
|
onSubmitReview={onSubmitReview}
|
||||||
onDecide={onDecide}
|
onDecide={onDecide}
|
||||||
onPublish={onPublish}
|
onPublish={onPublish}
|
||||||
@@ -319,10 +321,33 @@ function StatusBadge({ status }: { status: SchemaDraftStatus }) {
|
|||||||
|
|
||||||
function DraftMeta({ draft }: { draft: SchemaDraft }) {
|
function DraftMeta({ draft }: { draft: SchemaDraft }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const auth = useAuth()
|
||||||
|
// Resolve author display: если maker = current user, показываем имя текущего
|
||||||
|
// юзера ("Я • <username>"). Иначе — short UUID (первые 8 chars) с full
|
||||||
|
// tooltip. Полное UUID показывать бессмысленно — это Keycloak sub claim,
|
||||||
|
// нечитаемый для оператора.
|
||||||
|
//
|
||||||
|
// TODO Phase 1d: backend endpoint /admin/users/{sub}/display чтобы для
|
||||||
|
// не-current-user тоже показывать username. Пока — best-effort: current
|
||||||
|
// user resolved через JWT profile claim, другие — UUID prefix.
|
||||||
|
const currentSub = auth.user?.profile?.sub
|
||||||
|
const isMe = currentSub && currentSub === draft.makerId
|
||||||
|
const myDisplay =
|
||||||
|
auth.user?.profile?.preferred_username ||
|
||||||
|
auth.user?.profile?.name ||
|
||||||
|
auth.user?.profile?.email
|
||||||
|
const authorDisplay = isMe
|
||||||
|
? `Я${myDisplay ? ` • ${myDisplay}` : ''}`
|
||||||
|
: draft.makerId.substring(0, 8)
|
||||||
return (
|
return (
|
||||||
<dl className="grid grid-cols-[120px_1fr] gap-y-1 gap-x-3 text-cell">
|
<dl className="grid grid-cols-[120px_1fr] gap-y-1 gap-x-3 text-cell">
|
||||||
<Row label={t('workflow.schemaDraft.maker', { defaultValue: 'Автор' })}>
|
<Row label={t('workflow.schemaDraft.maker', { defaultValue: 'Автор' })}>
|
||||||
<span className="font-mono">{draft.makerId}</span>
|
<span
|
||||||
|
className={isMe ? 'text-ink' : 'font-mono text-mute'}
|
||||||
|
title={draft.makerId}
|
||||||
|
>
|
||||||
|
{authorDisplay}
|
||||||
|
</span>
|
||||||
</Row>
|
</Row>
|
||||||
<Row label={t('workflow.schemaDraft.from', { defaultValue: 'От версии' })}>
|
<Row label={t('workflow.schemaDraft.from', { defaultValue: 'От версии' })}>
|
||||||
v{draft.branchedFromVersion}
|
v{draft.branchedFromVersion}
|
||||||
@@ -396,6 +421,7 @@ function DraftActions({
|
|||||||
onEdit,
|
onEdit,
|
||||||
disabled,
|
disabled,
|
||||||
hasCurrentVersion,
|
hasCurrentVersion,
|
||||||
|
makerId,
|
||||||
}: {
|
}: {
|
||||||
status: SchemaDraftStatus
|
status: SchemaDraftStatus
|
||||||
onSubmitReview: () => void
|
onSubmitReview: () => void
|
||||||
@@ -405,13 +431,21 @@ function DraftActions({
|
|||||||
onEdit?: () => void
|
onEdit?: () => void
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
hasCurrentVersion: boolean
|
hasCurrentVersion: boolean
|
||||||
|
/** UUID maker'а draft'а — для gate "Отозвать" (только сам автор может). */
|
||||||
|
makerId: string
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const auth = useAuth()
|
||||||
// Phase 1c RBAC stub: для review_pending reviewer-only действия скрываем
|
// Phase 1c RBAC stub: для review_pending reviewer-only действия скрываем
|
||||||
// если нет approve role. Publish — отдельная role. Backend всё равно
|
// если нет approve role. Publish — отдельная role. Backend всё равно
|
||||||
// защищает. Phase 1d intergrate'нет Keycloak realm_access.roles.
|
// защищает. Phase 1d intergrate'нет Keycloak realm_access.roles.
|
||||||
const canReview = useCanReviewSchema()
|
const canReview = useCanReviewSchema()
|
||||||
const canPublish = useCanPublishSchema()
|
const canPublish = useCanPublishSchema()
|
||||||
|
// "Отозвать" — maker-only action (withdraw own draft). Backend всё равно
|
||||||
|
// 403'нет non-maker call, но скрытие на UI level убирает confusion:
|
||||||
|
// reviewer видел кнопку которая всегда fail'ила. Сравниваем JWT sub с makerId.
|
||||||
|
const currentSub = auth.user?.profile?.sub
|
||||||
|
const isMaker = Boolean(currentSub) && currentSub === makerId
|
||||||
|
|
||||||
if (!isActiveSchemaDraft(status)) {
|
if (!isActiveSchemaDraft(status)) {
|
||||||
return (
|
return (
|
||||||
@@ -501,6 +535,7 @@ function DraftActions({
|
|||||||
{t('workflow.schemaDraft.actions.publish', { defaultValue: 'Опубликовать' })}
|
{t('workflow.schemaDraft.actions.publish', { defaultValue: 'Опубликовать' })}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{isMaker && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -511,6 +546,7 @@ function DraftActions({
|
|||||||
>
|
>
|
||||||
{t('workflow.schemaDraft.actions.withdraw', { defaultValue: 'Отозвать' })}
|
{t('workflow.schemaDraft.actions.withdraw', { defaultValue: 'Отозвать' })}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user