fix(settings): вернуть в админку категории, которых не было в дереве

Дерево разделов настроек — захардкоженный список категорий, а на бэкенде категория
выводится из имени ключа. Любая настройка, создавшая новую категорию, просто
исчезала из админки: настройки были в 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 тестов на само дерево.
This commit is contained in:
Fringg
2026-07-30 01:58:05 +03:00
parent 0f21629ba5
commit 653d683835
7 changed files with 118 additions and 8 deletions

View File

@@ -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);
});
});

View File

@@ -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<string> {
const mapped = new Set<string>();
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,