Files
TeleWave/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx
T
Leonid PershinandClaude Opus 5 0606ea3e6e Add Prettier to the frontend and gate formatting in CI
Форматтера у фронтенда не было: стиль держался вручную и успел разъехаться в 50 файлах.
Ставим Prettier с настройками под уже сложившийся стиль (без точек с запятой, одинарные кавычки,
ширина 100 — подобрана замером: при 100 расходится меньше файлов, чем при 96 или 110) и
прогоняем его по коду. `src/routeTree.gen.ts` исключён — его переписывает плагин роутера.

Чтобы форматирование больше не расходилось незаметно, добавлены проверки в CI: `csharpier check`
для бэкенда (его отсутствие и позволило накопиться 79 неотформатированным файлам) и
`prettier --check` для фронтенда. Версии форматтеров прибиты точно, без кареток: минорка меняет
вывод и красит CI на файлах, которых никто не трогал.

`.editorconfig` задаёт редакторам те же отступы и LF ещё до форматтера; значения совпадают с
настройками csharpier и Prettier намеренно — оба его читают.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:38:12 +03:00

106 lines
3.9 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { qk } from '@/shared/api/query-keys'
import { useTranslation } from 'react-i18next'
import type { EntryTraceDto } from '@/shared/api/types'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/ui/dialog'
import { getEntryTrace } from '../api'
import { formatChannelTime } from '../lib/format'
/**
* «Почему это здесь» (см. 6.5): цепочка происхождения записи. Трейс пишется в момент генерации —
* восстановить его потом нельзя, поэтому у старых записей часть строк будет пустой.
*/
export function EntryTraceDialog({
entryId,
utcOffsetMinutes,
onClose,
}: Readonly<{
entryId: string
utcOffsetMinutes: number
onClose: () => void
}>) {
const { t } = useTranslation()
const { data } = useQuery({
queryKey: qk.entries.trace(entryId),
queryFn: () => getEntryTrace(entryId),
})
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{data
? `${data.showName ?? '—'} · ${formatChannelTime(data.startsAtUtc, utcOffsetMinutes)}`
: t('common.loading')}
</DialogTitle>
</DialogHeader>
{data && (
<dl className="grid grid-cols-[110px_1fr] gap-x-3 gap-y-1.5 text-sm">
<Row label={t('admin.channels.traceLayer')}>{layerSummary(data, t)}</Row>
<Row label={t('admin.channels.traceSlot')}>{slotSummary(data, t)}</Row>
<Row label={t('admin.channels.traceGroup')}>{groupSummary(data)}</Row>
<Row label={t('admin.channels.traceCollection')}>{data.collectionName}</Row>
<Row label={t('admin.channels.traceStrategy')}>{strategySummary(data, t)}</Row>
<Row label={t('admin.channels.traceJunction')}>{data.junctionName}</Row>
</dl>
)}
</DialogContent>
</Dialog>
)
}
type Translate = ReturnType<typeof useTranslation>['t']
/** Склейка непустых частей строки трейса; пусто — значит строка не заполнена (покажем «—»). */
const joinParts = (parts: (string | null | undefined)[]) =>
parts.filter(Boolean).join(' · ') || null
function layerSummary(data: EntryTraceDto, t: Translate) {
if (!data.layerName) return null
const priority =
data.layerPriority !== null ? ` (${t('admin.channels.priority')} ${data.layerPriority})` : ''
return `${data.layerName}${priority}`
}
function slotSummary(data: EntryTraceDto, t: Translate) {
if (!data.slotTitle) return null
return joinParts([
data.slotTitle,
data.slotWeekday === null
? t('admin.channels.everyDay')
: t(`admin.channels.weekdays.${data.slotWeekday}`),
data.slotTargetStart?.slice(0, 5),
data.slotDurationMinutes ? `${data.slotDurationMinutes} мин` : null,
data.driftMinutes !== 0 ? t('admin.channels.traceDrift', { minutes: data.driftMinutes }) : null,
data.snapped ? t('admin.channels.traceSnapped') : null,
])
}
function groupSummary(data: EntryTraceDto) {
if (!data.groupName) return null
const count = data.groupItemCount !== null ? ` (${data.groupItemCount})` : ''
return `${data.groupName}${count}`
}
function strategySummary(data: EntryTraceDto, t: Translate) {
if (!data.strategy) return null
return joinParts([
t(`admin.channels.strategies.${data.strategy}`),
data.cooldownDays ? t('admin.channels.traceCooldown', { days: data.cooldownDays }) : null,
data.candidatesAfterCooldown !== null
? t('admin.channels.traceCandidates', { count: data.candidatesAfterCooldown })
: null,
])
}
function Row({ label, children }: Readonly<{ label: string; children: React.ReactNode }>) {
return (
<>
<dt className="text-muted-foreground">{label}</dt>
<dd>{children || '—'}</dd>
</>
)
}