Files
mdm-ordinis/ordinis-admin-ui/src/components/JsonView.tsx
T
Zimin A.N. ac4f554e34 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 "не удалось загрузить").
2026-05-25 14:55:51 +03:00

135 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, type ReactNode } from 'react'
/**
* Лёгкий JSON viewer с token-based syntax highlight + diff-подсветкой
* top-level ключей. Никаких внешних зависимостей: regex-проход по
* `JSON.stringify(..., null, 2)`.
*
* <p>Syntax: ключи (aurora), строки (pink), числа (warn), true/false/null
* (accent), пунктуация (mute).
*
* <p>Diff (опционально): передай diff — массив { kind, keys }. Строки
* JSON где key в added/removed/changed получают bg-цвет (зелёный /
* красный / жёлтый). Подсветка — top-level only (для глубокого diff'а
* есть DraftDiffSummary).
*/
export type JsonDiff = {
added?: Set<string> | readonly string[]
removed?: Set<string> | readonly string[]
changed?: Set<string> | readonly string[]
}
type JsonViewProps = {
value: unknown
diff?: JsonDiff
className?: string
}
const TOKEN_RE =
/("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(true|false|null)|([{}[\],])/g
const toSet = (v: Set<string> | readonly string[] | undefined): Set<string> =>
v instanceof Set ? v : new Set(v ?? [])
const tokenize = (line: string): ReactNode[] => {
const out: ReactNode[] = []
let last = 0
let i = 0
for (const m of line.matchAll(TOKEN_RE)) {
const idx = m.index ?? 0
if (idx > last) out.push(line.slice(last, idx))
const [whole, str, colon, num, lit, punct] = m
if (str && colon) {
out.push(
<span key={`k${i}`} className="text-aurora">
{str}
</span>,
)
out.push(
<span key={`c${i}`} className="text-mute">
{colon}
</span>,
)
} else if (str) {
out.push(
<span key={`s${i}`} className="text-pink">
{str}
</span>,
)
} else if (num) {
out.push(
<span key={`n${i}`} className="text-warn">
{num}
</span>,
)
} else if (lit) {
out.push(
<span key={`l${i}`} className="text-accent">
{lit}
</span>,
)
} else if (punct) {
out.push(
<span key={`p${i}`} className="text-mute">
{punct}
</span>,
)
} else {
out.push(whole)
}
last = idx + whole.length
i++
}
if (last < line.length) out.push(line.slice(last))
return out
}
const LINE_KEY_RE = /^ {2}"((?:\\.|[^"\\])*)":/
const lineDiffClass = (
line: string,
added: Set<string>,
removed: Set<string>,
changed: Set<string>,
): string => {
const m = LINE_KEY_RE.exec(line)
if (!m) return ''
const k = m[1]
if (added.has(k)) return 'bg-aurora/15 -mx-2 px-2 border-l-2 border-aurora'
if (removed.has(k)) return 'bg-danger/15 -mx-2 px-2 border-l-2 border-danger'
if (changed.has(k)) return 'bg-warn/15 -mx-2 px-2 border-l-2 border-warn'
return ''
}
export const JsonView = ({ value, diff, className }: JsonViewProps) => {
const added = useMemo(() => toSet(diff?.added), [diff?.added])
const removed = useMemo(() => toSet(diff?.removed), [diff?.removed])
const changed = useMemo(() => toSet(diff?.changed), [diff?.changed])
const lines = useMemo(() => {
try {
return JSON.stringify(value, null, 2).split('\n')
} catch {
return [String(value)]
}
}, [value])
return (
<pre
className={
'text-mono text-cell bg-line/20 rounded p-2 overflow-x-auto whitespace-pre ' +
(className ?? '')
}
>
{lines.map((line, i) => {
const dc = lineDiffClass(line, added, removed, changed)
return (
<div key={i} className={dc}>
{tokenize(line)}
</div>
)
})}
</pre>
)
}