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:
Александр Зимин
2026-05-13 07:39:59 +00:00
@@ -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,16 +535,18 @@ function DraftActions({
{t('workflow.schemaDraft.actions.publish', { defaultValue: 'Опубликовать' })} {t('workflow.schemaDraft.actions.publish', { defaultValue: 'Опубликовать' })}
</Button> </Button>
)} )}
<Button {isMaker && (
variant="ghost" <Button
size="sm" variant="ghost"
onClick={onWithdraw} size="sm"
disabled={disabled} onClick={onWithdraw}
className="ml-auto text-danger hover:text-danger" disabled={disabled}
leftIcon={<TrashIcon weight="regular" size={14} />} className="ml-auto text-danger hover:text-danger"
> leftIcon={<TrashIcon weight="regular" size={14} />}
{t('workflow.schemaDraft.actions.withdraw', { defaultValue: 'Отозвать' })} >
</Button> {t('workflow.schemaDraft.actions.withdraw', { defaultValue: 'Отозвать' })}
</Button>
)}
</div> </div>
) )
} }