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.
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user