Merge branch 'fix/user-display-helper-reviews-audit' into 'main'
fix(ui): shared UserCell для UUID actor display (reviews + audit + drawer) See merge request 2-6/2-6-4/terravault/ordinis!160
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
|||||||
useWithdrawSchemaDraft,
|
useWithdrawSchemaDraft,
|
||||||
} from '@/api/mutations'
|
} from '@/api/mutations'
|
||||||
import { useCanPublishSchema, useCanReviewSchema } from '@/auth/usePermissions'
|
import { useCanPublishSchema, useCanReviewSchema } from '@/auth/usePermissions'
|
||||||
|
import { UserCell } from '@/lib/useUserDisplay'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -321,33 +322,10 @@ 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
|
<UserCell uuid={draft.makerId} />
|
||||||
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}
|
||||||
@@ -541,7 +519,7 @@ function DraftActions({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={onWithdraw}
|
onClick={onWithdraw}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="ml-auto text-danger hover:text-danger"
|
className="text-danger hover:text-danger"
|
||||||
leftIcon={<TrashIcon weight="regular" size={14} />}
|
leftIcon={<TrashIcon weight="regular" size={14} />}
|
||||||
>
|
>
|
||||||
{t('workflow.schemaDraft.actions.withdraw', { defaultValue: 'Отозвать' })}
|
{t('workflow.schemaDraft.actions.withdraw', { defaultValue: 'Отозвать' })}
|
||||||
|
|||||||
@@ -537,7 +537,7 @@ i18n
|
|||||||
'workflow.schemaDraft.comment.notePlaceholder': 'На что обратить внимание ревьюеру',
|
'workflow.schemaDraft.comment.notePlaceholder': 'На что обратить внимание ревьюеру',
|
||||||
'workflow.schemaDraft.actions.submit': 'Отправить на ревью',
|
'workflow.schemaDraft.actions.submit': 'Отправить на ревью',
|
||||||
'workflow.schemaDraft.actions.resubmit': 'Доработать и отправить',
|
'workflow.schemaDraft.actions.resubmit': 'Доработать и отправить',
|
||||||
'workflow.schemaDraft.actions.approve': 'Approve',
|
'workflow.schemaDraft.actions.approve': 'Одобрить',
|
||||||
'workflow.schemaDraft.actions.requestChanges': 'Запросить правки',
|
'workflow.schemaDraft.actions.requestChanges': 'Запросить правки',
|
||||||
'workflow.schemaDraft.actions.reject': 'Отклонить',
|
'workflow.schemaDraft.actions.reject': 'Отклонить',
|
||||||
'workflow.schemaDraft.actions.publish': 'Опубликовать',
|
'workflow.schemaDraft.actions.publish': 'Опубликовать',
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useAuth } from 'react-oidc-context'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve UUID sub claim (Keycloak {@code sub} → makerId / userId) к
|
||||||
|
* human-friendly display string.
|
||||||
|
*
|
||||||
|
* <p>Cases:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>uuid matches current user</b> → {@code "Я • <preferred_username>"}
|
||||||
|
* (или just {@code "Я"} если profile неполный).</li>
|
||||||
|
* <li><b>different user</b> → first 8 chars of UUID. Full ID в {@code fullId}
|
||||||
|
* чтобы consumer мог положить в {@code title=...} для tooltip.</li>
|
||||||
|
* <li><b>null/undefined uuid</b> → {@code "anonymous"}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>TODO Phase 1d: для не-current-user backend endpoint
|
||||||
|
* {@code /admin/users/{sub}/display} → resolve в username. Пока best-effort:
|
||||||
|
* current user resolved через JWT profile claim, остальные — UUID prefix
|
||||||
|
* (achievable via JWT alone).
|
||||||
|
*
|
||||||
|
* <p>Используется везде где UI показывает actor: AUTHOR в таблицах
|
||||||
|
* /reviews (record + schema), USER в /audit log, AUTHOR в schema draft
|
||||||
|
* drawer.
|
||||||
|
*/
|
||||||
|
export function useUserDisplay(uuid: string | null | undefined): {
|
||||||
|
display: string
|
||||||
|
isMe: boolean
|
||||||
|
fullId: string
|
||||||
|
} {
|
||||||
|
const auth = useAuth()
|
||||||
|
const fullId = uuid ?? ''
|
||||||
|
if (!fullId) {
|
||||||
|
return { display: 'anonymous', isMe: false, fullId: '' }
|
||||||
|
}
|
||||||
|
const currentSub = auth.user?.profile?.sub
|
||||||
|
const isMe = Boolean(currentSub) && currentSub === fullId
|
||||||
|
if (isMe) {
|
||||||
|
const me =
|
||||||
|
auth.user?.profile?.preferred_username ||
|
||||||
|
auth.user?.profile?.name ||
|
||||||
|
auth.user?.profile?.email
|
||||||
|
return { display: me ? `Я • ${me}` : 'Я', isMe, fullId }
|
||||||
|
}
|
||||||
|
return { display: fullId.substring(0, 8), isMe, fullId }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserCell — convenience component для тех мест где нужен readonly displaу
|
||||||
|
* с tooltip. Используется в таблицах.
|
||||||
|
*/
|
||||||
|
export function UserCell({ uuid }: { uuid: string | null | undefined }) {
|
||||||
|
const { display, isMe, fullId } = useUserDisplay(uuid)
|
||||||
|
if (!fullId) return <span className="text-mute italic">anonymous</span>
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={isMe ? 'text-ink' : 'font-mono text-ink-2'}
|
||||||
|
title={fullId}
|
||||||
|
>
|
||||||
|
{display}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
XIcon,
|
XIcon,
|
||||||
} from '@phosphor-icons/react'
|
} from '@phosphor-icons/react'
|
||||||
import { buildAuditExportUrl, useAudit, useDictionaries } from '@/api/queries'
|
import { buildAuditExportUrl, useAudit, useDictionaries } from '@/api/queries'
|
||||||
|
import { UserCell } from '@/lib/useUserDisplay'
|
||||||
import type { AuditAction, AuditEntry, AuditFilters } from '@/api/client'
|
import type { AuditAction, AuditEntry, AuditFilters } from '@/api/client'
|
||||||
import { localTzOffset, parseFormDate } from '@/lib/dates'
|
import { localTzOffset, parseFormDate } from '@/lib/dates'
|
||||||
|
|
||||||
@@ -473,7 +474,7 @@ function AuditRow({ row }: { row: AuditEntry }) {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<span className=" text-mono">{row.businessKey ?? '—'}</span>
|
<span className=" text-mono">{row.businessKey ?? '—'}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.userId ?? 'anonymous'}</TableCell>
|
<TableCell><UserCell uuid={row.userId} /></TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{row.userScope ? <Badge variant="info">{row.userScope}</Badge> : null}
|
{row.userScope ? <Badge variant="info">{row.userScope}</Badge> : null}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { CheckCircleIcon, XCircleIcon } from '@phosphor-icons/react'
|
|||||||
import { useDraft, useLiveRecord, useReviewQueue, useSchemaReviewQueue } from '@/api/queries'
|
import { useDraft, useLiveRecord, useReviewQueue, useSchemaReviewQueue } from '@/api/queries'
|
||||||
import { useApproveDraft, useRejectDraft } from '@/api/mutations'
|
import { useApproveDraft, useRejectDraft } from '@/api/mutations'
|
||||||
import type { DraftOperation, DraftResponse } from '@/api/client'
|
import type { DraftOperation, DraftResponse } from '@/api/client'
|
||||||
|
import { UserCell } from '@/lib/useUserDisplay'
|
||||||
|
|
||||||
export const Route = createFileRoute('/reviews')({
|
export const Route = createFileRoute('/reviews')({
|
||||||
component: ReviewsPage,
|
component: ReviewsPage,
|
||||||
@@ -331,7 +332,7 @@ function ReviewsPage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={operationVariant(d.operation)}>{d.operation}</Badge>
|
<Badge variant={operationVariant(d.operation)}>{d.operation}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-mono">{d.makerId}</TableCell>
|
<TableCell><UserCell uuid={d.makerId} /></TableCell>
|
||||||
<TableCell className="text-cell text-ink-2">
|
<TableCell className="text-cell text-ink-2">
|
||||||
{new Date(d.submittedAt).toLocaleString()}
|
{new Date(d.submittedAt).toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -622,7 +623,7 @@ function SchemaReviewQueuePanel() {
|
|||||||
</Link>
|
</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono tabular-nums">v{d.branchedFromVersion}</TableCell>
|
<TableCell className="font-mono tabular-nums">v{d.branchedFromVersion}</TableCell>
|
||||||
<TableCell className="font-mono">{d.makerId}</TableCell>
|
<TableCell><UserCell uuid={d.makerId} /></TableCell>
|
||||||
<TableCell className="tabular-nums">
|
<TableCell className="tabular-nums">
|
||||||
{d.submittedAt ? new Date(d.submittedAt).toLocaleString() : '—'}
|
{d.submittedAt ? new Date(d.submittedAt).toLocaleString() : '—'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
Reference in New Issue
Block a user