refactor(settings): Add base structures for settings tab architecture

This commit is contained in:
Grol Grol 2025-04-26 21:35:26 +03:00
parent d1a76ae87c
commit 5074bf6464
5 changed files with 189 additions and 0 deletions

56
src/ui/SettingGroup.ts Normal file
View file

@ -0,0 +1,56 @@
import { CheckboxSyncPluginSettings } from "src/types";
import { ISettingComponent } from "./interfaces/ISettingComponent";
/**
* Представляет группу связанных настроек в UI.
*/
export class SettingGroup {
title: string;
description?: string;
components: ISettingComponent[];
/**
* Создает экземпляр группы настроек.
* @param title - Заголовок группы (отображается как h3).
* @param components - Массив компонентов настроек, входящих в эту группу.
* @param description - Опциональное описание группы (отображается как p).
*/
constructor(title: string, components: ISettingComponent[], description?: string) {
this.title = title;
this.components = components;
this.description = description;
}
/**
* Рендерит заголовок группы и все ее компоненты.
* @param container - HTML-элемент, куда рендерить группу.
* @param currentSettings - Объект текущих настроек плагина.
*/
render(container: HTMLElement, currentSettings: CheckboxSyncPluginSettings): void {
container.createEl('h3', { text: this.title });
if (this.description) {
container.createEl('p', { text: this.description, cls: 'setting-item-description' });
}
// Рендерим каждый компонент, передавая ему текущее значение
for (const component of this.components) {
try {
const key = component.getSettingKey();
// Проверяем, существует ли ключ в настройках, чтобы избежать ошибок
if (key in currentSettings) {
component.render(container, currentSettings[key]);
} else {
console.warn(`Setting key "${key}" not found in current settings for component in group "${this.title}". Rendering with default or undefined value might occur.`);
// Можно передать undefined или значение по умолчанию, если getSettingKey гарантированно работает
// component.render(container, component.getDefaultValue());
component.render(container, undefined); // Или рендерить с undefined
}
} catch (error) {
console.error(`Error rendering component for key "${component.getSettingKey()}" in group "${this.title}":`, error);
// Можно добавить placeholder или сообщение об ошибке в UI
container.createDiv({ text: `Error rendering setting: ${component.getSettingKey()}`, cls: 'checkbox-sync-settings-error' });
}
}
}
}

View file

@ -0,0 +1,27 @@
import { Setting } from 'obsidian';
import { CheckboxSyncPluginSettings } from '../../types';
import { ISettingComponent } from '../interfaces/ISettingComponent';
import { ValidationError } from '../validation/types';
// Убрали App и CheckboxSyncPlugin из импортов, если они не нужны ВСЕМ наследникам
export abstract class BaseSettingComponent implements ISettingComponent {
protected setting: Setting; // Инициализируется в render конкретного компонента
protected onChangeCallback: () => void = () => {};
// Конструктор теперь пустой или с минимальными общими зависимостями
constructor() {}
// --- Методы для реализации наследниками ---
abstract getSettingKey(): keyof CheckboxSyncPluginSettings;
abstract getDefaultValue(): any;
// render теперь должен сам создавать Setting и сохранять ссылку в this.setting
abstract render(container: HTMLElement, currentValue: any): void;
abstract getValueFromUi(): any;
abstract setValueInUi(value: any): void;
abstract validate(value: any): ValidationError | null;
// --- Общая реализация ---
public setChangeListener(listener: () => void): void {
this.onChangeCallback = listener;
}
}

View file

@ -0,0 +1,58 @@
import { CheckboxSyncPluginSettings } from "src/types";
import { ValidationError } from "../validation/types";
/**
* Интерфейс для компонента, отвечающего за одну настройку в UI.
*/
export interface ISettingComponent {
/**
* Получает ключ настройки, за которую отвечает этот компонент.
* @returns Ключ из CheckboxSyncPluginSettings.
*/
getSettingKey(): keyof CheckboxSyncPluginSettings;
/**
* Получает значение по умолчанию для этой настройки.
* @returns Значение по умолчанию.
*/
getDefaultValue(): any;
/**
* Рендерит UI-элементы для этой настройки в указанный контейнер.
* @param container - HTML-элемент, куда добавлять настройку.
* @param currentValue - Текущее сохраненное значение настройки.
*/
render(container: HTMLElement, currentValue: any): void;
/**
* Считывает значение из UI-элементов компонента.
* Может включать парсинг (например, JSON).
* @returns Текущее значение из UI.
* @throws Error если значение в UI некорректно для извлечения (например, невалидный JSON).
*/
getValueFromUi(): any;
/**
* Устанавливает значение в UI-элементы компонента.
* Может включать форматирование (например, JSON.stringify).
* @param value - Значение для установки в UI.
*/
setValueInUi(value: any): void;
/**
* Выполняет *индивидуальную* валидацию значения.
* **Важно:** Этот метод должен быть тестируемым и не зависеть от Obsidian API.
* Он принимает чистое значение и возвращает ошибку или null.
* @param value - Чистое значение для валидации (обычно результат getValueFromUi).
* @returns Объект ValidationError, если значение невалидно, иначе null.
*/
validate(value: any): ValidationError | null;
/**
* Устанавливает колбэк, который будет вызван при изменении значения в UI.
* Используется для управления флагом isDirty.
* @param listener - Функция обратного вызова.
*/
setChangeListener(listener: () => void): void;
}

View file

@ -0,0 +1,40 @@
// src/settings/validation/SettingsValidator.ts
import { CheckboxSyncPluginSettings } from '../../types'; // Путь к типам
import { ValidationError } from './types'; // Путь к типам ошибок
/**
* Отвечает за валидацию настроек, требующую проверки нескольких полей (перекрестная валидация).
* **Важно:** Логика внутри методов не должна зависеть от Obsidian API для тестируемости.
*/
export class SettingsValidator {
/**
* Выполняет перекрестную валидацию предоставленных данных настроек.
* @param settingsData - Объект с текущими значениями настроек (или их частью).
* @returns Массив объектов ValidationError, если найдены ошибки, иначе пустой массив.
*/
public validate(settingsData: Partial<CheckboxSyncPluginSettings>): ValidationError[] {
const errors: ValidationError[] = [];
// Пример: Проверка пересечения списков символов (будет реализована позже)
// if (settingsData.checkedSymbols && settingsData.uncheckedSymbols) {
// const intersection = settingsData.checkedSymbols.filter(symbol => settingsData.uncheckedSymbols!.includes(symbol));
// if (intersection.length > 0) {
// errors.push({
// // Можно не указывать поле, т.к. ошибка затрагивает несколько
// message: `Symbols found in both Checked and Unchecked lists: ${JSON.stringify(intersection)}`
// });
// }
// }
// ... другие перекрестные проверки ...
// Пока возвращаем пустой массив, логика будет добавлена на Шаге 6
return errors;
}
// Сюда можно добавить статические вспомогательные методы для валидации, если нужно
// public static validateSymbolListsIntersection(listA: string[], listB: string[]): string[] {
// return listA.filter(symbol => listB.includes(symbol));
// }
}

View file

@ -0,0 +1,8 @@
import { CheckboxSyncPluginSettings } from "src/types";
export interface ValidationError {
/** Ключ поля настройки, с которым связана ошибка (опционально). */
field?: keyof CheckboxSyncPluginSettings;
/** Сообщение об ошибке, понятное пользователю. */
message: string;
}