90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import { AlertTriangle, CircleAlert } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { qk } from '@/shared/api/query-keys'
|
|
import type { SlotDto, TemplateIssueDto } from '@/shared/api/types'
|
|
import { cn } from '@/shared/lib/cn'
|
|
import { getTemplateIssues } from '../api'
|
|
|
|
/**
|
|
* Проверки по правилам (см. 5.1). Считаются на сервере по шаблону, без прогона генератора, поэтому
|
|
* показываются прямо в редакторе и обновляются вместе с сеткой.
|
|
*/
|
|
export function TemplateIssues({
|
|
channelId,
|
|
slotsById,
|
|
onGoToSlot,
|
|
}: Readonly<{
|
|
channelId: string
|
|
slotsById: Map<string, SlotDto>
|
|
onGoToSlot: (slot: SlotDto) => void
|
|
}>) {
|
|
const { t } = useTranslation()
|
|
const { data: issues } = useQuery({
|
|
queryKey: qk.channels.issues(channelId),
|
|
queryFn: () => getTemplateIssues(channelId),
|
|
})
|
|
|
|
if (!issues || issues.length === 0) return null
|
|
|
|
const errors = issues.filter((i) => i.severity === 'Error')
|
|
const warnings = issues.filter((i) => i.severity === 'Warning')
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1 rounded-md border border-border px-3 py-2 text-xs">
|
|
<span className="font-semibold uppercase tracking-wide text-muted-foreground">
|
|
{t('admin.channels.issues', { errors: errors.length, warnings: warnings.length })}
|
|
</span>
|
|
<ul className="flex flex-col gap-0.5">
|
|
{[...errors, ...warnings].map((issue, index) => (
|
|
<IssueRow
|
|
key={`${issue.kind}-${index}`}
|
|
issue={issue}
|
|
slot={issue.slotId ? slotsById.get(issue.slotId) : undefined}
|
|
onGoToSlot={onGoToSlot}
|
|
/>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function IssueRow({
|
|
issue,
|
|
slot,
|
|
onGoToSlot,
|
|
}: Readonly<{
|
|
issue: TemplateIssueDto
|
|
slot: SlotDto | undefined
|
|
onGoToSlot: (slot: SlotDto) => void
|
|
}>) {
|
|
const { t } = useTranslation()
|
|
const Icon = issue.severity === 'Error' ? CircleAlert : AlertTriangle
|
|
|
|
return (
|
|
<li className="flex items-start gap-1.5">
|
|
<Icon
|
|
className={cn(
|
|
'mt-0.5 h-3 w-3 shrink-0',
|
|
issue.severity === 'Error' ? 'text-red-500' : 'text-amber-500',
|
|
)}
|
|
/>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="text-muted-foreground">
|
|
{t(`admin.channels.issueKinds.${issue.kind}`)}:{' '}
|
|
</span>
|
|
{issue.details}
|
|
</span>
|
|
{slot && (
|
|
<button
|
|
type="button"
|
|
className="shrink-0 text-primary hover:underline"
|
|
onClick={() => onGoToSlot(slot)}
|
|
>
|
|
{t('admin.channels.goToSlot')}
|
|
</button>
|
|
)}
|
|
</li>
|
|
)
|
|
}
|