Refactor ChannelDetail component to implement tabbed navigation for channel settings, enhancing user experience by organizing content into distinct sections. Introduce a new TABS constant for better maintainability and update related components (BumperCard, RulesCard, ViewerCard, etc.) to support a 'bare' prop for streamlined rendering. Improve error handling for template loading and update translations for better clarity.
build / backend (push) Successful in 1m10s
build / frontend (push) Successful in 35s
tests / backend-tests (push) Successful in 1m31s

This commit is contained in:
Leonid Pershin
2026-07-26 20:03:14 +03:00
parent 2387223f0b
commit 69c236d8cc
8 changed files with 593 additions and 494 deletions
@@ -8,7 +8,9 @@ import { HttpError } from '@/shared/api/client'
import type { GridLayerDto, SlotDto } from '@/shared/api/types'
import { Badge } from '@/shared/ui/badge'
import { Button } from '@/shared/ui/button'
import { Card, CardContent } from '@/shared/ui/card'
import { Input } from '@/shared/ui/input'
import { cn } from '@/shared/lib/cn'
import { toast } from '@/shared/ui/toast-store'
import {
applyChannelTemplate,
@@ -26,7 +28,6 @@ import {
} from './api'
import { ApplyDialog } from './components/ApplyDialog'
import { BumperCard } from './components/BumperCard'
import { CollapsibleCard } from './components/CollapsibleCard'
import { EntryTraceDialog } from './components/EntryTraceDialog'
import { JunctionsCard } from './components/JunctionsCard'
import { LayerApplicabilityDialog } from './components/LayerApplicabilityDialog'
@@ -40,6 +41,10 @@ import { ViewerCard } from './components/ViewerCard'
import { SlotInspector, type SlotDraft } from './components/SlotInspector'
import { toTime } from './lib/format'
/** Вкладки экрана канала — в порядке частоты правки. */
const TABS = ['grid', 'rules', 'junctions', 'bumpers', 'viewer', 'settings', 'air'] as const
type ChannelTab = (typeof TABS)[number]
export function ChannelDetail({ channelId }: { channelId: string }) {
const { t } = useTranslation()
const queryClient = useQueryClient()
@@ -53,12 +58,13 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
const [applyOpen, setApplyOpen] = useState(false)
const [traceEntryId, setTraceEntryId] = useState<string | null>(null)
const [copyToChannel, setCopyToChannel] = useState('')
const [tab, setTab] = useState<ChannelTab>('grid')
const { data: channel, isLoading } = useQuery({
queryKey: ['admin', 'channels', channelId],
queryFn: () => getChannel(channelId),
})
const { data: template } = useQuery({
const { data: template, error: templateError } = useQuery({
queryKey: ['admin', 'channels', channelId, 'template'],
queryFn: () => getChannelTemplate(channelId),
})
@@ -248,16 +254,46 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
</div>
)}
<SettingsCard
channel={channel}
readyAssets={ready?.items ?? []}
onSaved={invalidate}
onError={onError}
/>
{/* Вкладки вместо колонки карточек: экран канала перестал помещаться в один свиток. */}
<nav className="flex flex-wrap gap-4 border-b border-border text-xs uppercase tracking-wide">
{TABS.map((value) => (
<button
key={value}
type="button"
onClick={() => setTab(value)}
className={cn(
'pb-2 text-muted-foreground hover:text-foreground',
tab === value && 'border-b-2 border-primary text-primary',
)}
>
{t(`admin.channels.tabs.${value}`)}
</button>
))}
</nav>
{template && (
<CollapsibleCard title={t('admin.channels.grid')} defaultOpen>
<div className="grid gap-4 lg:grid-cols-[220px_1fr]">
{tab === 'settings' && (
<SettingsCard
channel={channel}
readyAssets={ready?.items ?? []}
bare
onSaved={invalidate}
onError={onError}
/>
)}
{/* Шаблон мог не загрузиться — раньше вкладка сетки просто оказывалась пустой. */}
{tab === 'grid' && !template && (
<p className="text-sm text-muted-foreground">
{templateError instanceof HttpError
? templateError.detail
: t('admin.channels.noTemplate')}
</p>
)}
{tab === 'grid' && template && (
<Card>
<CardContent>
<div className="grid gap-4 lg:grid-cols-[220px_1fr]">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
@@ -408,24 +444,43 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
/>
)}
</div>
</div>
</CollapsibleCard>
</div>
</CardContent>
</Card>
)}
{template && (
<RulesCard template={template} onChanged={invalidate} onError={onError} />
{tab === 'rules' &&
(template ? (
<RulesCard template={template} bare onChanged={invalidate} onError={onError} />
) : (
<p className="text-sm text-muted-foreground">{t('admin.channels.noTemplate')}</p>
))}
{tab === 'junctions' && (
<JunctionsCard
channel={channel}
template={template}
bare
onChanged={invalidate}
onError={onError}
/>
)}
<JunctionsCard
channel={channel}
template={template}
onChanged={invalidate}
onError={onError}
/>
{tab === 'bumpers' && (
<BumperCard channel={channel} bare onSaved={invalidate} onError={onError} />
)}
<BumperCard channel={channel} onSaved={invalidate} onError={onError} />
{tab === 'viewer' && (
<ViewerCard channel={channel} bare onSaved={invalidate} onError={onError} />
)}
<ViewerCard channel={channel} onSaved={invalidate} onError={onError} />
{tab === 'air' && (
<Card>
<CardContent>
<SchedulePreview entries={schedule ?? []} onShowTrace={setTraceEntryId} />
</CardContent>
</Card>
)}
{applicabilityLayer && (
<LayerApplicabilityDialog
@@ -436,8 +491,6 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
/>
)}
<SchedulePreview entries={schedule ?? []} onShowTrace={setTraceEntryId} />
{applyOpen && (
<ApplyDialog
channelId={channelId}