diff --git a/frontend/src/features/admin/channels/components/GridTab.tsx b/frontend/src/features/admin/channels/components/GridTab.tsx index 78af3a7..a1ce7bb 100644 --- a/frontend/src/features/admin/channels/components/GridTab.tsx +++ b/frontend/src/features/admin/channels/components/GridTab.tsx @@ -281,17 +281,27 @@ export function GridTab({
- l.slots).map((slot) => [slot.id, slot])) - } - onGoToSlot={openSlot} - /> - {/* Сетка на конкретную дату: видно, какие слои в этот день действительно действуют. */} + {/* Один ряд служебных элементов: проверки, дата просмотра и добавление слота. Всё, + что не сама сетка, живёт в одной строке — вертикаль экрана нужна сетке. */}
+ l.slots).map((slot) => [slot.id, slot])) + } + onGoToSlot={openSlot} + /> + {t('admin.channels.showForDate')} -
-

- {draft.slot ? t('admin.channels.editSlot') : t('admin.channels.newSlot')} -

- -
+ // Отдельным окном, а не панелью под сеткой: форма длинная, и раньше она уезжала за нижний край, + // а сетка — то, с чем её сверяют, — уходила из виду. + !next && onClose()}> + + + + {draft.slot ? t('admin.channels.editSlot') : t('admin.channels.newSlot')} + + -
- - patch({ title: e.target.value })} /> -
- -
-
- - patch({ targetStart: `${e.target.value}:00` })} - /> -
-
- - patch({ targetDurationMinutes: Number(e.target.value) })} - /> -
-
- -
- - -
- -
-
- - -
-
- - -
-
- - {isContent && ( - <> +
- - + + patch({ title: e.target.value })} />
-
- - -
- - {body.strategy?.type === 'RandomWithCooldown' && ( -
- - - patch({ - strategy: { ...body.strategy!, cooldownDays: Number(e.target.value) }, - }) - } - /> -

{t('admin.channels.cooldownHint')}

-
- )} -
- + + patch({ targetStart: `${e.target.value}:00` })} + /> +
+
+ + patch({ targetDurationMinutes: Number(e.target.value) })} + /> +
+
+ +
+ + +
+ +
+
+
- {body.blockMode !== 'FillSlot' && ( +
+ + +
+
+ + {isContent && ( + <>
- + + +
+ +
+ + +
+ + {body.strategy?.type === 'RandomWithCooldown' && ( +
+ + + patch({ + strategy: { ...body.strategy!, cooldownDays: Number(e.target.value) }, + }) + } + /> +

+ {t('admin.channels.cooldownHint')} +

+
+ )} + +
+
+ + +
+ {body.blockMode !== 'FillSlot' && ( +
+ + patch({ blockValue: Number(e.target.value) })} + /> +
+ )} +
+ +
+ + +

{t('admin.channels.overflowHint')}

+
+ + )} + + {body.slotKind === 'Repeat' && body.repeatSource && ( +
+
+ patch({ blockValue: Number(e.target.value) })} + value={body.repeatSource.daysAgo} + onChange={(e) => + patch({ + repeatSource: { ...body.repeatSource!, daysAgo: Number(e.target.value) }, + }) + } />
- )} -
+
+ + + patch({ repeatSource: { ...body.repeatSource!, time: `${e.target.value}:00` } }) + } + /> +
+
+ + + patch({ + repeatSource: { + ...body.repeatSource!, + durationMinutes: Number(e.target.value), + }, + }) + } + /> +
+
+ )} -
- - -

{t('admin.channels.overflowHint')}

+
+ +
+ + patch({ maxDriftMinutes: Number(e.target.value) })} + /> +
+
+ + +
- - )} +

{t('admin.channels.anchorHint')}

- {body.slotKind === 'Repeat' && body.repeatSource && ( -
-
- - - patch({ - repeatSource: { ...body.repeatSource!, daysAgo: Number(e.target.value) }, - }) - } - /> -
-
- - - patch({ repeatSource: { ...body.repeatSource!, time: `${e.target.value}:00` } }) - } - /> -
-
- - - patch({ - repeatSource: { - ...body.repeatSource!, - durationMinutes: Number(e.target.value), - }, - }) - } - /> + {/* Стыки: внутри слота — между единицами, после слота — на переходе к следующему. */} +
+
+ + +
+
+ + +
- )} -
- -
- - patch({ maxDriftMinutes: Number(e.target.value) })} - /> -
-
- - -
-
-

{t('admin.channels.anchorHint')}

- - {/* Стыки: внутри слота — между единицами, после слота — на переходе к следующему. */} -
-
- - -
-
- - -
-
- -
- {draft.slot ? ( - - ) : ( - - )} - -
-
+ + {draft.slot ? ( + + ) : ( + + )} +
+ + +
+
+ +
) } diff --git a/frontend/src/features/admin/channels/components/TemplateIssues.tsx b/frontend/src/features/admin/channels/components/TemplateIssues.tsx index 34eb50f..889bff8 100644 --- a/frontend/src/features/admin/channels/components/TemplateIssues.tsx +++ b/frontend/src/features/admin/channels/components/TemplateIssues.tsx @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query' -import { AlertTriangle, CircleAlert } from 'lucide-react' +import { AlertTriangle, ChevronDown, ChevronRight, CircleAlert } from 'lucide-react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { qk } from '@/shared/api/query-keys' import type { SlotDto, TemplateIssueDto } from '@/shared/api/types' @@ -9,6 +10,10 @@ import { getTemplateIssues } from '../api' /** * Проверки по правилам (см. 5.1). Считаются на сервере по шаблону, без прогона генератора, поэтому * показываются прямо в редакторе и обновляются вместе с сеткой. + * + * По умолчанию свёрнуты в одну строку со счётчиками: семь дыр в сетке — обычное состояние + * недособранного канала, и занимать ими пол-экрана над самой сеткой значит прятать то, ради чего + * экран открывали. Разворачиваются по клику и скроллятся, не растягивая страницу. */ export function TemplateIssues({ channelId, @@ -20,6 +25,7 @@ export function TemplateIssues({ onGoToSlot: (slot: SlotDto) => void }>) { const { t } = useTranslation() + const [open, setOpen] = useState(false) const { data: issues } = useQuery({ queryKey: qk.channels.issues(channelId), queryFn: () => getTemplateIssues(channelId), @@ -31,20 +37,38 @@ export function TemplateIssues({ const warnings = issues.filter((i) => i.severity === 'Warning') return ( -
- +
+ + + {open && ( +
    + {[...errors, ...warnings].map((issue, index) => ( + + ))} +
+ )}
) }