fix(admin-ui): reviews drawer — dict link, live FK, JSON syntax+diff
Три проблемы по reviews-drawer (по фидбеку: «Открыть ломается / Согласование
не показывает справочник / не показывает live / JSON не подсвечен»):
1. useLiveRecord(draft.dictionaryId, ...) → useLiveRecord(dictName, ...).
Endpoint /api/v1/dictionaries/{name}/records/{bk} ожидает name, не UUID;
до фикса валился с 404 «Dictionary not found: <uuid>» → live-блок был
пустой и показывался "noLive" даже для UPDATE/CLOSE drafts.
2. Header drawer'а теперь рендерит "<dictName> / <businessKey>" вместо
просто businessKey. dictName — link на /dictionaries/$name. Fallback
на UUID-prefix без link'а если dict не виден (scope-hide).
3. JsonView (новый компонент, ~120 строк, без deps) заменяет голые <pre>
JSON.stringify(...):
- syntax highlight: ключи (aurora), строки (pink), числа (warn),
true/false/null (accent), пунктуация (mute)
- diff highlight: top-level ключи из shallowDiffSummary получают
фон+border (зелёный added / красный removed / жёлтый changed).
В live-колонке — removed+changed, в proposed — added+changed.
Также добавлен error-state для liveQ (раньше 404 от useLiveRecord на UUID
молча превращался в "noLive"; теперь explicit "не удалось загрузить").
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
classifyRecordStatus,
|
||||
classifySchemaStatus,
|
||||
} from '@/components/reviews/MyDraftsPanel'
|
||||
import { JsonView } from '@/components/JsonView'
|
||||
|
||||
export const Route = createFileRoute('/reviews')({
|
||||
component: ReviewsPage,
|
||||
@@ -638,12 +639,21 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
|
||||
const currentSub = auth.user?.profile?.sub
|
||||
const draftQ = useDraft(draftId)
|
||||
const draft = draftQ.data
|
||||
// DraftResponse несёт только dictionaryId (UUID); /api/v1/dictionaries/{name}/...
|
||||
// ожидает имя словаря. Резолвим через useDictionaries (TanStack кеш).
|
||||
// До этого useLiveRecord(draft.dictionaryId, ...) валился с 404
|
||||
// "Dictionary not found: <uuid>" → live-блок drawer'а оставался пустым.
|
||||
const dictionariesQ = useDictionaries()
|
||||
const dictName = useMemo(
|
||||
() => dictionariesQ.data?.find((d) => d.id === draft?.dictionaryId)?.name,
|
||||
[dictionariesQ.data, draft?.dictionaryId],
|
||||
)
|
||||
// CREATE-черновик не имеет live-записи по определению — не пробуем её
|
||||
// загружать (GET /dictionaries/{id}/records/{key} → 404, красный шум в
|
||||
// загружать (GET /dictionaries/{name}/records/{key} → 404, красный шум в
|
||||
// консоли на каждый CREATE-draft). Для UPDATE/CLOSE — грузим как раньше.
|
||||
const isCreateDraft = draft?.operation === 'CREATE'
|
||||
const liveQ = useLiveRecord(
|
||||
draft && !isCreateDraft ? draft.dictionaryId : undefined,
|
||||
draft && !isCreateDraft ? dictName : undefined,
|
||||
draft && !isCreateDraft ? draft.businessKey : undefined,
|
||||
)
|
||||
const approveMut = useApproveDraft()
|
||||
@@ -773,6 +783,20 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
|
||||
<div className="space-y-1 text-cell">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={operationVariant(draft.operation)}>{draft.operation}</Badge>
|
||||
{dictName ? (
|
||||
<Link
|
||||
to="/dictionaries/$name"
|
||||
params={{ name: dictName }}
|
||||
className="text-mono text-accent hover:underline"
|
||||
>
|
||||
{dictName}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-mono text-mute" title={draft.dictionaryId}>
|
||||
{draft.dictionaryId.slice(0, 8)}…
|
||||
</span>
|
||||
)}
|
||||
<span className="text-mute">/</span>
|
||||
<span className="font-mono">{draft.businessKey}</span>
|
||||
<DrawerStatusBadge status={draft.status} />
|
||||
</div>
|
||||
@@ -832,38 +856,63 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
|
||||
operation={draft.operation}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p className="text-cap text-mute mb-1">
|
||||
{t('reviews.drawer.live')}
|
||||
</p>
|
||||
{liveQ.isLoading && <LoadingBlock size="sm" label={t('loading')} />}
|
||||
{(liveQ.data === null || isCreateDraft) && (
|
||||
<p className="text-cell text-mute italic">
|
||||
{t('reviews.drawer.noLive')}
|
||||
</p>
|
||||
)}
|
||||
{liveQ.data && (
|
||||
<pre className="text-mono bg-line/30 rounded p-2 overflow-x-auto whitespace-pre-wrap">
|
||||
{JSON.stringify(liveQ.data.data, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-cap text-mute mb-1">
|
||||
{t('reviews.drawer.proposed')}
|
||||
</p>
|
||||
{draft.operation === 'CLOSE' ? (
|
||||
<p className="text-cell text-aurora italic">
|
||||
{t('reviews.drawer.closeNote')}
|
||||
</p>
|
||||
) : (
|
||||
<pre className="text-mono bg-accent/4 rounded p-2 overflow-x-auto whitespace-pre-wrap">
|
||||
{JSON.stringify(draft.data, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
// Считаем diff один раз для обеих колонок: changed/added/removed
|
||||
// keys (top-level) подсветим через JsonView в обоих side-by-side
|
||||
// блоках — reviewer видит точку изменения визуально, не сравнивая
|
||||
// глазами длинные JSON-объекты.
|
||||
const liveData =
|
||||
(liveQ.data?.data as Record<string, unknown> | null | undefined) ?? null
|
||||
const proposedData =
|
||||
(draft.data as Record<string, unknown> | null | undefined) ?? null
|
||||
const diff =
|
||||
draft.operation === 'CLOSE'
|
||||
? null
|
||||
: shallowDiffSummary(liveData, proposedData)
|
||||
const liveDiff = diff
|
||||
? { removed: diff.removed, changed: diff.changed }
|
||||
: undefined
|
||||
const proposedDiff = diff
|
||||
? { added: diff.added, changed: diff.changed }
|
||||
: undefined
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p className="text-cap text-mute mb-1">
|
||||
{t('reviews.drawer.live')}
|
||||
</p>
|
||||
{liveQ.isLoading && <LoadingBlock size="sm" label={t('loading')} />}
|
||||
{liveQ.error && (
|
||||
<p className="text-cell text-danger italic">
|
||||
{t('reviews.drawer.liveError', {
|
||||
defaultValue: 'Не удалось загрузить текущую запись',
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{!liveQ.isLoading && !liveQ.error && (liveQ.data === null || isCreateDraft) && (
|
||||
<p className="text-cell text-mute italic">
|
||||
{t('reviews.drawer.noLive')}
|
||||
</p>
|
||||
)}
|
||||
{liveQ.data && (
|
||||
<JsonView value={liveQ.data.data} diff={liveDiff} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-cap text-mute mb-1">
|
||||
{t('reviews.drawer.proposed')}
|
||||
</p>
|
||||
{draft.operation === 'CLOSE' ? (
|
||||
<p className="text-cell text-aurora italic">
|
||||
{t('reviews.drawer.closeNote')}
|
||||
</p>
|
||||
) : (
|
||||
<JsonView value={draft.data} diff={proposedDiff} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{(approveMut.error || rejectMut.error || withdrawMut.error) && (() => {
|
||||
const err = approveMut.error || rejectMut.error || withdrawMut.error
|
||||
|
||||
Reference in New Issue
Block a user