From 653d683835e2c10f7690f1f71df3552c3ba9ab2f Mon Sep 17 00:00:00 2001 From: Fringg Date: Thu, 30 Jul 2026 01:58:05 +0300 Subject: [PATCH] =?UTF-8?q?fix(settings):=20=D0=B2=D0=B5=D1=80=D0=BD=D1=83?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D1=83=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D0=B8?= =?UTF-8?q?,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D1=85=20=D0=BD=D0=B5?= =?UTF-8?q?=20=D0=B1=D1=8B=D0=BB=D0=BE=20=D0=B2=20=D0=B4=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дерево разделов настроек — захардкоженный список категорий, а на бэкенде категория выводится из имени ключа. Любая настройка, создавшая новую категорию, просто исчезала из админки: настройки были в API, но ни один раздел их не показывал. Так пропали 33 категории из 102, в том числе: - CABINET целиком (13 настроек, включая CABINET_ENABLED, CABINET_URL, CABINET_REQUIRE_LEGAL_CONSENT и CABINET_LEGAL_CONSENT_PRECHECKED); - INFO_PAGES — режимы показа оферты, политики, правил и FAQ; - MENU — кнопка меню Telegram с открытием кабинета. Эти три разложены по дереву явно. Остальное собирает новый пункт «Прочие настройки» — по образцу админки бота, где такой раздел уже есть. Это и есть главная часть фикса: теперь новая категория на бэкенде не приводит к молчаливому исчезновению настроек, максимум — к неудачному разделу. Локали ru/en/zh/fa, +5 тестов на само дерево. --- src/components/admin/constants.test.ts | 44 ++++++++++++++++++++++++++ src/components/admin/constants.ts | 26 ++++++++++++++- src/locales/en.json | 9 +++++- src/locales/fa.json | 9 +++++- src/locales/ru.json | 9 +++++- src/locales/zh.json | 9 +++++- src/pages/AdminSettings.tsx | 20 ++++++++++-- 7 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 src/components/admin/constants.test.ts diff --git a/src/components/admin/constants.test.ts b/src/components/admin/constants.test.ts new file mode 100644 index 0000000..a8699b5 --- /dev/null +++ b/src/components/admin/constants.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { OTHER_CATEGORIES, SETTINGS_TREE, getMappedCategoryKeys } from './constants'; + +// Категории настроек на бэкенде выводятся из имени ключа, поэтому каждая новая +// настройка может создать категорию, которой в дереве нет. Раньше такие настройки +// просто не отображались: так исчезла вся категория CABINET (13 настроек, включая +// CABINET_ENABLED и CABINET_URL) и правовые документы из INFO_PAGES. + +describe('SETTINGS_TREE', () => { + it('содержит пункт-сборник для категорий без своего места', () => { + const children = SETTINGS_TREE.groups.flatMap((group) => group.children); + const catchAll = children.filter((child) => child.categories.includes(OTHER_CATEGORIES)); + + expect(catchAll).toHaveLength(1); + expect(catchAll[0].id).toBe('sys_other'); + }); + + it('не считает маркер сборника обычной категорией', () => { + expect(getMappedCategoryKeys().has(OTHER_CATEGORIES)).toBe(false); + }); + + it('раскладывает по дереву категории, которые раньше терялись', () => { + const mapped = getMappedCategoryKeys(); + + for (const category of ['CABINET', 'INFO_PAGES', 'MENU']) { + expect(mapped.has(category)).toBe(true); + } + }); + + it('незнакомая категория попадает в сборник, а не пропадает', () => { + const mapped = getMappedCategoryKeys(); + // Так выглядит любая категория, появившаяся на бэкенде после этого релиза. + expect(mapped.has('SOME_FUTURE_CATEGORY')).toBe(false); + }); + + it('не содержит категорию дважды', () => { + const all = SETTINGS_TREE.groups + .flatMap((group) => group.children) + .flatMap((child) => child.categories) + .filter((category) => category !== OTHER_CATEGORIES); + + expect(new Set(all).size).toBe(all.length); + }); +}); diff --git a/src/components/admin/constants.ts b/src/components/admin/constants.ts index 207b361..2381c9f 100644 --- a/src/components/admin/constants.ts +++ b/src/components/admin/constants.ts @@ -1,4 +1,4 @@ -import { ThemeColors, DEFAULT_THEME_COLORS } from '../../types/theme'; +import { type ThemeColors, DEFAULT_THEME_COLORS } from '../../types/theme'; // Tree sidebar types export interface TreeSubItem { @@ -23,6 +23,9 @@ export interface SettingsTreeConfig { groups: TreeGroup[]; } +// Маркер пункта-сборника: подставляется вместо списка категорий. +export const OTHER_CATEGORIES = '*'; + // Hierarchical settings tree — all 61 backend category keys mapped into 7 groups export const SETTINGS_TREE: SettingsTreeConfig = { specialItems: [ @@ -91,6 +94,8 @@ export const SETTINGS_TREE: SettingsTreeConfig = { { id: 'iface_widget', categories: ['TELEGRAM_WIDGET'] }, { id: 'iface_oidc', categories: ['TELEGRAM_OIDC'] }, { id: 'iface_skip', categories: ['SKIP'] }, + { id: 'iface_info', categories: ['INFO_PAGES'] }, + { id: 'iface_menu', categories: ['MENU'] }, { id: 'iface_additional', categories: ['ADDITIONAL'] }, ], }, @@ -131,6 +136,7 @@ export const SETTINGS_TREE: SettingsTreeConfig = { { id: 'sys_core', categories: ['CORE', 'DEBUG'] }, { id: 'sys_remnawave', categories: ['REMNAWAVE'] }, { id: 'sys_webapi', categories: ['WEB_API', 'EXTERNAL_ADMIN'] }, + { id: 'sys_cabinet', categories: ['CABINET'] }, { id: 'sys_webhook', categories: ['WEBHOOK'] }, { id: 'sys_server', categories: ['SERVER_STATUS'] }, { id: 'sys_monitoring', categories: ['MONITORING'] }, @@ -138,11 +144,29 @@ export const SETTINGS_TREE: SettingsTreeConfig = { { id: 'sys_backup', categories: ['BACKUP'] }, { id: 'sys_version', categories: ['VERSION'] }, { id: 'sys_logging', categories: ['LOG'] }, + // Категории на бэкенде выводятся из имени ключа, поэтому каждая новая + // настройка может создать категорию, которой в дереве нет — и тогда она + // просто исчезала из админки (так пропала вся категория CABINET). + // Этот пункт собирает всё неразложенное, как «Прочее» в админке бота. + { id: 'sys_other', categories: [OTHER_CATEGORIES] }, ], }, ], }; +// Категории, у которых есть своё место в дереве (маркер сборника не считается). +export function getMappedCategoryKeys(): Set { + const mapped = new Set(); + for (const group of SETTINGS_TREE.groups) { + for (const child of group.children) { + for (const category of child.categories) { + if (category !== OTHER_CATEGORIES) mapped.add(category); + } + } + } + return mapped; +} + // Helper: find which group and sub-item a backend category key belongs to export function findTreeLocation( categoryKey: string, diff --git a/src/locales/en.json b/src/locales/en.json index d29d33f..b07b36d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2306,9 +2306,16 @@ "sys_maintenance": "Maintenance", "sys_backup": "Backup", "sys_version": "Version", - "sys_logging": "Logging" + "sys_logging": "Logging", + "iface_info": "Legal documents", + "iface_menu": "Menu button", + "sys_cabinet": "Cabinet", + "sys_other": "Other settings" }, "categories": { + "CABINET": "Cabinet", + "INFO_PAGES": "Legal documents and FAQ", + "MENU": "Menu button", "TELEGRAM_WIDGET": "Telegram Login Widget", "TELEGRAM_OIDC": "Telegram Login (OIDC)" }, diff --git a/src/locales/fa.json b/src/locales/fa.json index eebca41..cbbb785 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1969,9 +1969,16 @@ "sys_maintenance": "نگهداری", "sys_backup": "پشتیبان‌گیری", "sys_version": "نسخه", - "sys_logging": "لاگ‌ها" + "sys_logging": "لاگ‌ها", + "iface_info": "اسناد حقوقی", + "iface_menu": "دکمه منو", + "sys_cabinet": "کابین", + "sys_other": "سایر تنظیمات" }, "categories": { + "CABINET": "کابین", + "INFO_PAGES": "اسناد حقوقی و پرسش‌های متداول", + "MENU": "دکمه منو", "TELEGRAM_WIDGET": "Telegram Login Widget", "TELEGRAM_OIDC": "Telegram Login (OIDC)" }, diff --git a/src/locales/ru.json b/src/locales/ru.json index 38614c1..0e1e6a1 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2313,7 +2313,11 @@ "sys_maintenance": "Обслуживание", "sys_backup": "Бэкап", "sys_version": "Версия", - "sys_logging": "Логи" + "sys_logging": "Логи", + "iface_info": "Правовые документы", + "iface_menu": "Кнопка меню", + "sys_cabinet": "Кабинет", + "sys_other": "Прочие настройки" }, "presets": { "standard": "Стандарт", @@ -2769,6 +2773,9 @@ "telegramOidcClientSecret": "Client Secret" }, "categories": { + "CABINET": "Кабинет", + "INFO_PAGES": "Правовые документы и FAQ", + "MENU": "Кнопка меню", "PAYMENT": "Платежи", "PAYMENT_VERIFICATION": "Верификация платежей", "YOOKASSA": "YooKassa", diff --git a/src/locales/zh.json b/src/locales/zh.json index 09f525e..49b202d 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -2036,9 +2036,16 @@ "sys_maintenance": "维护", "sys_backup": "备份", "sys_version": "版本", - "sys_logging": "日志" + "sys_logging": "日志", + "iface_info": "法律文件", + "iface_menu": "菜单按钮", + "sys_cabinet": "用户中心", + "sys_other": "其他设置" }, "categories": { + "CABINET": "用户中心", + "INFO_PAGES": "法律文件与 FAQ", + "MENU": "菜单按钮", "TELEGRAM_WIDGET": "Telegram Login Widget", "TELEGRAM_OIDC": "Telegram Login (OIDC)" }, diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index db1b127..438c353 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -2,10 +2,16 @@ import { useState, useMemo, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; -import { adminSettingsApi, SettingDefinition } from '../api/adminSettings'; +import { adminSettingsApi, type SettingDefinition } from '../api/adminSettings'; import { themeColorsApi } from '../api/themeColors'; import { useFavoriteSettings } from '../hooks/useFavoriteSettings'; -import { SETTINGS_TREE, findTreeLocation, formatSettingKey } from '../components/admin'; +import { + OTHER_CATEGORIES, + SETTINGS_TREE, + findTreeLocation, + formatSettingKey, + getMappedCategoryKeys, +} from '../components/admin'; import { usePlatform } from '../platform/hooks/usePlatform'; import { AnalyticsTab } from '../components/admin/AnalyticsTab'; import { BrandingTab } from '../components/admin/BrandingTab'; @@ -75,10 +81,18 @@ export default function AdminSettings() { if (!activeTreeInfo || !allSettings || !Array.isArray(allSettings)) return []; const categoryKeys = activeTreeInfo.child.categories; + // Пункт-сборник показывает всё, чему не нашлось места в дереве: категории на + // бэкенде выводятся из имени ключа, поэтому новая настройка легко создаёт + // категорию, которой здесь нет — раньше такие настройки просто исчезали. + const isOtherSection = categoryKeys.includes(OTHER_CATEGORIES); + const mappedCategories = isOtherSection ? getMappedCategoryKeys() : null; const categoryMap = new Map(); for (const setting of allSettings) { - if (categoryKeys.includes(setting.category.key)) { + const belongsHere = mappedCategories + ? !mappedCategories.has(setting.category.key) + : categoryKeys.includes(setting.category.key); + if (belongsHere) { // Hide tariff-dependent settings when not in tariffs mode if (!isTariffsMode && TARIFF_MODE_SETTINGS.includes(setting.key)) continue;