From 6fe6404e611795d3a37d84cb7b68baa425c02628 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Mon, 27 Jul 2026 06:57:27 +0300 Subject: [PATCH] Refactor GridTab and SlotInspector components for improved UI and functionality Updated the GridTab component to reorganize the layout, integrating the TemplateIssues component into a more user-friendly format. Enhanced the SlotInspector component by transitioning it from a panel to a dialog for better visibility and usability, especially for lengthy forms. Added new button functionality for creating slots and improved the handling of template issues with collapsible sections. These changes enhance the overall user experience in managing channels and slots. --- .../admin/channels/components/GridTab.tsx | 26 +- .../channels/components/SlotInspector.tsx | 628 +++++++++--------- .../channels/components/TemplateIssues.tsx | 52 +- 3 files changed, 379 insertions(+), 327 deletions(-) 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) => ( + + ))} +
+ )}
) }