mirror of
https://github.com/groldsf/obsidian_check_plugin.git
synced 2026-07-22 05:37:48 +00:00
refactor(settings): Migrate all remaining settings to component architecture
This commit is contained in:
parent
400ce20193
commit
cdcf3a9db3
10 changed files with 738 additions and 201 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import { validateValueIsBoolean } from "src/ui/validation/validators";
|
import { CheckboxState } from "src/types";
|
||||||
|
import { validateIsCheckboxState, validateJsonStringArray, validateNotEmptyArray, validateValueIsBoolean } from "src/ui/validation/validators";
|
||||||
|
|
||||||
|
|
||||||
// Группа тестов для всего файла валидаторов
|
// Группа тестов для всего файла валидаторов
|
||||||
|
|
@ -56,15 +57,117 @@ describe('Validation Utility Functions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Сюда можно будет добавлять describe-блоки для других функций валидации ---
|
describe('validateIsCheckboxState', () => {
|
||||||
/*
|
it('should return null for valid CheckboxState values', () => {
|
||||||
|
expect(validateIsCheckboxState(CheckboxState.Checked)).toBeNull();
|
||||||
|
expect(validateIsCheckboxState(CheckboxState.Unchecked)).toBeNull();
|
||||||
|
expect(validateIsCheckboxState(CheckboxState.Ignore)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error object for invalid values', () => {
|
||||||
|
const expectedError = { message: `Invalid checkbox state selected. Must be one of: ${Object.values(CheckboxState).join(', ')}.` };
|
||||||
|
expect(validateIsCheckboxState('checked ')).toEqual(expectedError); // Лишний пробел
|
||||||
|
expect(validateIsCheckboxState('CHECKED')).toEqual(expectedError); // Не тот регистр
|
||||||
|
expect(validateIsCheckboxState(null)).toEqual(expectedError);
|
||||||
|
expect(validateIsCheckboxState(undefined)).toEqual(expectedError);
|
||||||
|
expect(validateIsCheckboxState(1)).toEqual(expectedError);
|
||||||
|
expect(validateIsCheckboxState({})).toEqual(expectedError);
|
||||||
|
expect(validateIsCheckboxState('')).toEqual(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('validateJsonStringArray', () => {
|
describe('validateJsonStringArray', () => {
|
||||||
// ... тесты для валидации JSON ...
|
// Валидные случаи
|
||||||
|
test('should return null for valid single-element array', () => {
|
||||||
|
expect(validateJsonStringArray('["x"]')).toBeNull();
|
||||||
|
expect(validateJsonStringArray('[" "]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null for valid multi-element array', () => {
|
||||||
|
expect(validateJsonStringArray('["x", " ", "✓"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null for empty array string "[]"', () => {
|
||||||
|
expect(validateJsonStringArray('[]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null for empty string ""', () => {
|
||||||
|
// Мы решили, что пустая строка - валидный пустой массив
|
||||||
|
expect(validateJsonStringArray('')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null for string with whitespace around valid JSON', () => {
|
||||||
|
expect(validateJsonStringArray(' ["x", "o"] ')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Невалидные случаи: Не JSON
|
||||||
|
test('should return error for non-JSON string', () => {
|
||||||
|
expect(validateJsonStringArray('["x"')).toEqual({ message: expect.stringContaining('JSON format') });
|
||||||
|
expect(validateJsonStringArray('abc')).toEqual({ message: expect.stringContaining('JSON format') });
|
||||||
|
expect(validateJsonStringArray('[x]')).toEqual({ message: expect.stringContaining('JSON format') }); // 'x' должен быть в кавычках
|
||||||
|
});
|
||||||
|
|
||||||
|
// Невалидные случаи: Не массив
|
||||||
|
test('should return error for valid JSON that is not an array', () => {
|
||||||
|
expect(validateJsonStringArray('{"a": 1}')).toEqual({ message: expect.stringContaining('must be a valid JSON array') });
|
||||||
|
expect(validateJsonStringArray('"string"')).toEqual({ message: expect.stringContaining('must be a valid JSON array') });
|
||||||
|
expect(validateJsonStringArray('123')).toEqual({ message: expect.stringContaining('must be a valid JSON array') });
|
||||||
|
expect(validateJsonStringArray('null')).toEqual({ message: expect.stringContaining('must be a valid JSON array') });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Невалидные случаи: Содержимое массива - не строки
|
||||||
|
test('should return error if array contains non-string elements', () => {
|
||||||
|
expect(validateJsonStringArray('[1]')).toEqual({ message: expect.stringContaining('not a string') });
|
||||||
|
expect(validateJsonStringArray('["x", 2]')).toEqual({ message: expect.stringContaining('not a string') });
|
||||||
|
expect(validateJsonStringArray('[null]')).toEqual({ message: expect.stringContaining('not a string') });
|
||||||
|
expect(validateJsonStringArray('[[]]')).toEqual({ message: expect.stringContaining('not a string') });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Невалидные случаи: Строки не из одного символа
|
||||||
|
test('should return error if array contains strings of incorrect length', () => {
|
||||||
|
expect(validateJsonStringArray('["xx"]')).toEqual({ message: expect.stringContaining('must be a single character') });
|
||||||
|
expect(validateJsonStringArray('["x", "ab"]')).toEqual({ message: expect.stringContaining('must be a single character') });
|
||||||
|
expect(validateJsonStringArray('[""]')).toEqual({ message: expect.stringContaining('must be a single character') }); // Пустая строка тоже не 1 символ
|
||||||
|
});
|
||||||
|
|
||||||
|
// Невалидные случаи: Дубликаты символов
|
||||||
|
test('should return error if array contains duplicate symbols', () => {
|
||||||
|
expect(validateJsonStringArray('["x", "x"]')).toEqual({ message: expect.stringContaining('Duplicate symbol "x"') });
|
||||||
|
expect(validateJsonStringArray('["a", "b", "a"]')).toEqual({ message: expect.stringContaining('Duplicate symbol "a"') });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Невалидные случаи: Null или undefined на входе
|
||||||
|
test('should return error for null or undefined input', () => {
|
||||||
|
expect(validateJsonStringArray(null)).toEqual({ message: "Input must be a string." });
|
||||||
|
expect(validateJsonStringArray(undefined)).toEqual({ message: "Input must be a string." });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateNotEmptyArray', () => {
|
describe('validateNotEmptyArray', () => {
|
||||||
// ... тесты для проверки на пустой массив ...
|
// Валидные случаи (не пустой массив)
|
||||||
|
test('should return null for non-empty arrays', () => {
|
||||||
|
expect(validateNotEmptyArray(['a'])).toBeNull();
|
||||||
|
expect(validateNotEmptyArray([1, 2])).toBeNull();
|
||||||
|
expect(validateNotEmptyArray([null])).toBeNull();
|
||||||
|
expect(validateNotEmptyArray([undefined])).toBeNull();
|
||||||
|
expect(validateNotEmptyArray([[]])).toBeNull(); // Массив, содержащий пустой массив
|
||||||
|
});
|
||||||
|
|
||||||
|
// Невалидные случаи: Пустой массив
|
||||||
|
test('should return error object for an empty array', () => {
|
||||||
|
expect(validateNotEmptyArray([])).toEqual({ message: 'The list cannot be empty.' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Невалидные случаи: Не массив
|
||||||
|
test('should return error object for non-array values', () => {
|
||||||
|
const expectedError = { message: 'Value must be an array.' };
|
||||||
|
expect(validateNotEmptyArray(null)).toEqual(expectedError);
|
||||||
|
expect(validateNotEmptyArray(undefined)).toEqual(expectedError);
|
||||||
|
expect(validateNotEmptyArray('abc')).toEqual(expectedError);
|
||||||
|
expect(validateNotEmptyArray(123)).toEqual(expectedError);
|
||||||
|
expect(validateNotEmptyArray({})).toEqual(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
*/
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
import { App, ButtonComponent, DropdownComponent, Notice, PluginSettingTab, Setting, TextComponent, ToggleComponent } from "obsidian";
|
|
||||||
import CheckboxSyncPlugin from "../main";
|
|
||||||
import { CheckboxState, CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "../types";
|
|
||||||
import { ConfirmModal, InfoModal, SaveConfirmModal } from "./modals";
|
|
||||||
import { Mutex } from "async-mutex";
|
import { Mutex } from "async-mutex";
|
||||||
|
import { App, ButtonComponent, Notice, PluginSettingTab, Setting, TextComponent } from "obsidian";
|
||||||
|
import CheckboxSyncPlugin from "../main";
|
||||||
|
import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "../types";
|
||||||
import { SettingGroup } from "./SettingGroup";
|
import { SettingGroup } from "./SettingGroup";
|
||||||
|
import { EnableChildSyncSettingComponent } from "./components/EnableChildSyncSettingComponent";
|
||||||
|
import { EnableFileSyncSettingComponent } from "./components/EnableFileSyncSettingComponent";
|
||||||
import { EnableParentSyncSettingComponent } from "./components/EnableParentSyncSettingComponent";
|
import { EnableParentSyncSettingComponent } from "./components/EnableParentSyncSettingComponent";
|
||||||
|
import { UnknownPolicySettingComponent } from "./components/UnknownPolicySettingComponent";
|
||||||
|
import { ConfirmModal, InfoModal, SaveConfirmModal } from "./modals";
|
||||||
import { ValidationError } from "./validation/types";
|
import { ValidationError } from "./validation/types";
|
||||||
|
import { CheckedSymbolsSettingComponent } from "./components/CheckedSymbolsSettingComponent";
|
||||||
|
import { UncheckedSymbolsSettingComponent } from "./components/UncheckedSymbolsSettingComponent";
|
||||||
|
import { IgnoreSymbolsSettingComponent } from "./components/IgnoreSymbolsSettingComponent";
|
||||||
|
|
||||||
export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
||||||
plugin: CheckboxSyncPlugin;
|
plugin: CheckboxSyncPlugin;
|
||||||
private checkedSymbolsInput: TextComponent;
|
|
||||||
private uncheckedSymbolsInput: TextComponent;
|
|
||||||
private ignoreSymbolsInput: TextComponent;
|
|
||||||
private unknownPolicyDropdown: DropdownComponent;
|
|
||||||
// private parentToggle: ToggleComponent;
|
|
||||||
private childToggle: ToggleComponent;
|
|
||||||
private enableAutomaticFileSyncToggle: ToggleComponent;
|
|
||||||
|
|
||||||
private settingGroups: SettingGroup[] = []
|
private settingGroups: SettingGroup[] = []
|
||||||
|
|
||||||
|
|
@ -35,22 +34,45 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
||||||
|
|
||||||
// Метод для создания и конфигурации групп и компонентов
|
// Метод для создания и конфигурации групп и компонентов
|
||||||
private initializeSettingGroups(): void {
|
private initializeSettingGroups(): void {
|
||||||
// 1. Создаем экземпляр нашего компонента
|
// 1. Создаем экземпляры наших компонентов
|
||||||
const parentToggleComp = new EnableParentSyncSettingComponent();
|
|
||||||
|
|
||||||
// 2. Устанавливаем ему обработчик изменений, чтобы он мог обновить флаг isDirty
|
const checkedSymbolsComp = new CheckedSymbolsSettingComponent();
|
||||||
// Мы передаем лямбда-функцию, которая вызовет наш метод settingChanged
|
checkedSymbolsComp.setChangeListener(() => this.settingChanged());
|
||||||
|
|
||||||
|
const uncheckedSymbolsComp = new UncheckedSymbolsSettingComponent();
|
||||||
|
uncheckedSymbolsComp.setChangeListener(() => this.settingChanged());
|
||||||
|
|
||||||
|
const unknownPolicyComp = new UnknownPolicySettingComponent();
|
||||||
|
unknownPolicyComp.setChangeListener(() => this.settingChanged());
|
||||||
|
|
||||||
|
const ignoreSymbolsComp = new IgnoreSymbolsSettingComponent();
|
||||||
|
ignoreSymbolsComp.setChangeListener(() => this.settingChanged());
|
||||||
|
|
||||||
|
const symbolGroup = new SettingGroup(
|
||||||
|
// Используем старый заголовок/описание
|
||||||
|
"Checkbox Symbol Configuration (Advanced: JSON)",
|
||||||
|
[checkedSymbolsComp, uncheckedSymbolsComp, ignoreSymbolsComp, unknownPolicyComp], // Пока только один компонент
|
||||||
|
"Warning: Requires valid JSON format. Use double quotes for strings."
|
||||||
|
);
|
||||||
|
|
||||||
|
const parentToggleComp = new EnableParentSyncSettingComponent();
|
||||||
parentToggleComp.setChangeListener(() => this.settingChanged());
|
parentToggleComp.setChangeListener(() => this.settingChanged());
|
||||||
|
|
||||||
// 3. Создаем группу настроек
|
const childrenToggleComp = new EnableChildSyncSettingComponent();
|
||||||
|
childrenToggleComp.setChangeListener(() => this.settingChanged());
|
||||||
|
|
||||||
|
const automaticFileSyncToggleComp = new EnableFileSyncSettingComponent();
|
||||||
|
automaticFileSyncToggleComp.setChangeListener(() => this.settingChanged());
|
||||||
|
|
||||||
const behaviorGroup = new SettingGroup(
|
const behaviorGroup = new SettingGroup(
|
||||||
"Synchronization Behavior", // Заголовок группы
|
"Synchronization Behavior", // Заголовок группы
|
||||||
[parentToggleComp] // Массив компонентов для этой группы (пока один)
|
[parentToggleComp, childrenToggleComp, automaticFileSyncToggleComp] // Массив компонентов для этой группы
|
||||||
// Можно добавить описание группы третьим аргументом, если нужно
|
// Можно добавить описание группы третьим аргументом, если нужно
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Сохраняем созданную группу (или группы) в поле класса
|
// 3. Сохраняем созданную группу (или группы) в поле класса
|
||||||
this.settingGroups = [
|
this.settingGroups = [
|
||||||
|
symbolGroup,
|
||||||
behaviorGroup
|
behaviorGroup
|
||||||
// Сюда позже добавятся другие группы (например, для символов)
|
// Сюда позже добавятся другие группы (например, для символов)
|
||||||
];
|
];
|
||||||
|
|
@ -133,100 +155,10 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
||||||
const containerEl = this.containerEl;
|
const containerEl = this.containerEl;
|
||||||
containerEl.empty();
|
containerEl.empty();
|
||||||
containerEl.createEl('h2', { text: 'Checkbox Sync Settings' });
|
containerEl.createEl('h2', { text: 'Checkbox Sync Settings' });
|
||||||
containerEl.createEl('h3', { text: 'Checkbox Symbol Configuration (Advanced: JSON)' });
|
|
||||||
containerEl.createEl('p', { text: 'Warning: Requires valid JSON format. Use double quotes for strings.' });
|
|
||||||
|
|
||||||
// --- Checked Symbols ---
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Checked Symbols")
|
|
||||||
.setDesc("Enter symbols as a JSON array of single-character strings. Example: [\"x\", \"✓\", \" \"]")
|
|
||||||
.addText((text) => {
|
|
||||||
this.checkedSymbolsInput = text;
|
|
||||||
text
|
|
||||||
.setPlaceholder("e.g., [\"x\", \" \"]")
|
|
||||||
.setValue(this.arrayToJsonString(this.plugin.settings.checkedSymbols))
|
|
||||||
.onChange(() => this.settingChanged())
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Unchecked Symbols ---
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Unchecked Symbols")
|
|
||||||
.setDesc("Enter symbols as a JSON array of single-character strings. Example: [\" \", \"?\", \",\"]")
|
|
||||||
.addText((text) => {
|
|
||||||
this.uncheckedSymbolsInput = text;
|
|
||||||
text
|
|
||||||
.setPlaceholder("e.g., [\" \", \"?\", \",\"]")
|
|
||||||
.setValue(this.arrayToJsonString(this.plugin.settings.uncheckedSymbols))
|
|
||||||
.onChange(() => this.settingChanged())
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Ignore Symbols ---
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Ignore Symbols")
|
|
||||||
.setDesc("Checkboxes with these symbols will be ignored during automatic parent/child state updates. Example: [\"-\", \"~\"]")
|
|
||||||
.addText((text) => {
|
|
||||||
this.ignoreSymbolsInput = text; // Сохраняем ссылку
|
|
||||||
text
|
|
||||||
.setPlaceholder("e.g., [\"-\", \"~\"]")
|
|
||||||
.setValue(this.arrayToJsonString(this.plugin.settings.ignoreSymbols))
|
|
||||||
.onChange(() => this.settingChanged())
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Unknown Symbol Policy ---
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Unknown Symbol Policy')
|
|
||||||
.setDesc('How to treat symbols not in Checked or Unchecked lists.')
|
|
||||||
.addDropdown(dropdown => {
|
|
||||||
this.unknownPolicyDropdown = dropdown;
|
|
||||||
dropdown
|
|
||||||
.addOption(CheckboxState.Checked, 'Treat as Checked')
|
|
||||||
.addOption(CheckboxState.Unchecked, 'Treat as Unchecked')
|
|
||||||
.addOption(CheckboxState.Ignore, 'Ignore')
|
|
||||||
.setValue(this.plugin.settings.unknownSymbolPolicy)
|
|
||||||
.onChange(() => this.settingChanged())
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
for (const group of this.settingGroups) {
|
for (const group of this.settingGroups) {
|
||||||
// Вызываем метод render группы, передавая контейнер и текущие настройки
|
|
||||||
group.render(containerEl, this.plugin.settings);
|
group.render(containerEl, this.plugin.settings);
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
containerEl.createEl('h3', { text: 'Synchronization Behavior' });
|
|
||||||
// --- Parent State Toggle ---
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Update parent checkbox state automatically')
|
|
||||||
.setDesc('If enabled, the state of a parent checkbox will be automatically updated based on the state of its children (all children checked = parent checked). If disabled, only manually changing a parent will affect its children.')
|
|
||||||
.addToggle(toggle => {
|
|
||||||
this.parentToggle = toggle;
|
|
||||||
toggle
|
|
||||||
.setValue(this.plugin.settings.enableAutomaticParentState)
|
|
||||||
.onChange(() => this.settingChanged())
|
|
||||||
});
|
|
||||||
|
|
||||||
*/
|
|
||||||
// --- Child State Toggle ---
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Update child checkbox state automatically')
|
|
||||||
.setDesc('If enabled, changing the state of a parent checkbox will automatically update the state of all its direct and nested children. If disabled, changing a parent checkbox will not affect its children.')
|
|
||||||
.addToggle(toggle => {
|
|
||||||
this.childToggle = toggle;
|
|
||||||
toggle
|
|
||||||
.setValue(this.plugin.settings.enableAutomaticChildState)
|
|
||||||
.onChange(() => this.settingChanged())
|
|
||||||
});
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("Enable automatic file synchronization")
|
|
||||||
.setDesc("If enabled (requires restart or settings reload), automatically syncs checkbox states when files are loaded/opened and after settings changes. If disabled (default), sync only occurs when you manually change a checkbox.")
|
|
||||||
.addToggle(toggle => {
|
|
||||||
this.enableAutomaticFileSyncToggle = toggle;
|
|
||||||
toggle
|
|
||||||
.setValue(this.plugin.settings.enableAutomaticFileSync)
|
|
||||||
.onChange(() => this.settingChanged());
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Область для вывода ошибок ---
|
// --- Область для вывода ошибок ---
|
||||||
this.errorDisplayEl = containerEl.createDiv({ cls: 'checkbox-sync-settings-error' });
|
this.errorDisplayEl = containerEl.createDiv({ cls: 'checkbox-sync-settings-error' });
|
||||||
|
|
@ -283,16 +215,10 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.validateAndSaveSettings();
|
await this.validateAndSaveSettings();
|
||||||
|
|
||||||
this.checkedSymbolsInput.setValue(this.arrayToJsonString(this.plugin.settings.checkedSymbols));
|
|
||||||
this.uncheckedSymbolsInput.setValue(this.arrayToJsonString(this.plugin.settings.uncheckedSymbols));
|
|
||||||
this.ignoreSymbolsInput.setValue(this.arrayToJsonString(this.plugin.settings.ignoreSymbols));
|
|
||||||
|
|
||||||
this.settingSaved();
|
this.settingSaved();
|
||||||
this.errorDisplayEl.setText('');
|
this.errorDisplayEl.setText('');
|
||||||
new Notice("Checkbox Sync settings applied!", 3000);
|
new Notice("Checkbox Sync settings applied!", 3000);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// --- Ошибка ---
|
|
||||||
console.error("Error applying checkbox sync settings:", error);
|
console.error("Error applying checkbox sync settings:", error);
|
||||||
// Выводим ошибку в специальный div
|
// Выводим ошибку в специальный div
|
||||||
this.errorDisplayEl.setText(`❌ ${error.message || "An unknown error occurred."}`);
|
this.errorDisplayEl.setText(`❌ ${error.message || "An unknown error occurred."}`);
|
||||||
|
|
@ -321,15 +247,6 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем значения всех полей ввода
|
|
||||||
this.checkedSymbolsInput.setValue(this.arrayToJsonString(settings.checkedSymbols));
|
|
||||||
this.uncheckedSymbolsInput.setValue(this.arrayToJsonString(settings.uncheckedSymbols));
|
|
||||||
this.ignoreSymbolsInput.setValue(this.arrayToJsonString(settings.ignoreSymbols));
|
|
||||||
this.unknownPolicyDropdown.setValue(settings.unknownSymbolPolicy);
|
|
||||||
// this.parentToggle.setValue(settings.enableAutomaticParentState);
|
|
||||||
this.childToggle.setValue(settings.enableAutomaticChildState);
|
|
||||||
this.enableAutomaticFileSyncToggle.setValue(settings.enableAutomaticFileSync);
|
|
||||||
|
|
||||||
// Очищаем ошибку и сбрасываем состояние "грязный"
|
// Очищаем ошибку и сбрасываем состояние "грязный"
|
||||||
this.errorDisplayEl?.setText('');
|
this.errorDisplayEl?.setText('');
|
||||||
this.settingSaved(); // Используем существующий метод для сброса isDirty и состояния кнопки Apply
|
this.settingSaved(); // Используем существующий метод для сброса isDirty и состояния кнопки Apply
|
||||||
|
|
@ -434,64 +351,25 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Считываем значения из UI
|
const checkedSymbolsFromComponent = newSettingsData.checkedSymbols || [];
|
||||||
const checkedValue = this.checkedSymbolsInput.getValue();
|
const uncheckedSymbolsFromComp = newSettingsData.uncheckedSymbols || [];
|
||||||
const uncheckedValue = this.uncheckedSymbolsInput.getValue();
|
const ignoreSymbolsFromComp = newSettingsData.ignoreSymbols || [];
|
||||||
const ignoreValue = this.ignoreSymbolsInput.getValue();
|
|
||||||
const policyValue = this.unknownPolicyDropdown.getValue() as CheckboxState;
|
|
||||||
// const parentValue = this.parentToggle.getValue();
|
|
||||||
const childValue = this.childToggle.getValue();
|
|
||||||
const automaticFileSyncValue = this.enableAutomaticFileSyncToggle.getValue();
|
|
||||||
|
|
||||||
// 2. Парсим JSON
|
|
||||||
const parsedChecked = this.parseJsonStringArray(checkedValue);
|
|
||||||
const parsedUnchecked = this.parseJsonStringArray(uncheckedValue);
|
|
||||||
const parsedIgnore = this.parseJsonStringArray(ignoreValue);
|
|
||||||
|
|
||||||
// 3. Проверяем ошибки парсинга -> throw
|
|
||||||
if (parsedChecked.error) {
|
|
||||||
throw new Error(`Checked Symbols Error: ${parsedChecked.error}`);
|
|
||||||
}
|
|
||||||
if (parsedUnchecked.error) {
|
|
||||||
throw new Error(`Unchecked Symbols Error: ${parsedUnchecked.error}`);
|
|
||||||
}
|
|
||||||
if (parsedIgnore.error) {
|
|
||||||
throw new Error(`Ignore Symbols Error: ${parsedIgnore.error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkedSymbolsArray = parsedChecked.result;
|
|
||||||
const uncheckedSymbolsArray = parsedUnchecked.result;
|
|
||||||
const ignoreSymbolsArray = parsedIgnore.result;
|
|
||||||
|
|
||||||
// 4. Валидируем пересечение -> throw
|
// 4. Валидируем пересечение -> throw
|
||||||
const intersectionValidation1 = this.validateSettingsArraysIntersection(checkedSymbolsArray, uncheckedSymbolsArray);
|
const intersectionValidation1 = this.validateSettingsArraysIntersection(checkedSymbolsFromComponent, uncheckedSymbolsFromComp);
|
||||||
if (!intersectionValidation1.isValid) {
|
if (!intersectionValidation1.isValid) {
|
||||||
throw new Error(`Lists: checked and unchecked. ${intersectionValidation1.error!}`);
|
throw new Error(`Lists: checked and unchecked. ${intersectionValidation1.error!}`);
|
||||||
}
|
}
|
||||||
const intersectionValidation2 = this.validateSettingsArraysIntersection(checkedSymbolsArray, ignoreSymbolsArray);
|
const intersectionValidation2 = this.validateSettingsArraysIntersection(checkedSymbolsFromComponent, ignoreSymbolsFromComp);
|
||||||
if (!intersectionValidation2.isValid) {
|
if (!intersectionValidation2.isValid) {
|
||||||
throw new Error(`Lists: checked and ignore. ${intersectionValidation2.error!}`);
|
throw new Error(`Lists: checked and ignore. ${intersectionValidation2.error!}`);
|
||||||
}
|
}
|
||||||
const intersectionValidation3 = this.validateSettingsArraysIntersection(uncheckedSymbolsArray, ignoreSymbolsArray);
|
const intersectionValidation3 = this.validateSettingsArraysIntersection(uncheckedSymbolsFromComp, ignoreSymbolsFromComp);
|
||||||
if (!intersectionValidation3.isValid) {
|
if (!intersectionValidation3.isValid) {
|
||||||
throw new Error(`Lists: unchecked and ignore. ${intersectionValidation3.error!}`);
|
throw new Error(`Lists: unchecked and ignore. ${intersectionValidation3.error!}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Валидируем на пустые списки -> throw
|
|
||||||
if (checkedSymbolsArray.length === 0) {
|
|
||||||
throw new Error("Checked symbols list cannot be empty.");
|
|
||||||
}
|
|
||||||
if (uncheckedSymbolsArray.length === 0) {
|
|
||||||
throw new Error("Unchecked symbols list cannot be empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
newSettingsData.checkedSymbols = checkedSymbolsArray;
|
|
||||||
newSettingsData.uncheckedSymbols = uncheckedSymbolsArray;
|
|
||||||
newSettingsData.ignoreSymbols = ignoreSymbolsArray;
|
|
||||||
newSettingsData.unknownSymbolPolicy = policyValue;
|
|
||||||
newSettingsData.enableAutomaticChildState = childValue;
|
|
||||||
newSettingsData.enableAutomaticFileSync = automaticFileSyncValue;
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
// Формируем сообщение об ошибке
|
// Формируем сообщение об ошибке
|
||||||
const errorMessage = errors.map(e => `❌ ${e.field ? `[${e.field}]: ` : ''}${e.message}`).join('\n');
|
const errorMessage = errors.map(e => `❌ ${e.field ? `[${e.field}]: ` : ''}${e.message}`).join('\n');
|
||||||
|
|
|
||||||
130
src/ui/components/BaseTextArraySettingComponent.ts
Normal file
130
src/ui/components/BaseTextArraySettingComponent.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { TextComponent } from "obsidian";
|
||||||
|
import { ValidationError } from "../validation/types";
|
||||||
|
import { validateJsonStringArray } from "../validation/validators";
|
||||||
|
import { BaseSettingComponent } from "./BaseSettingComponent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Абстрактный базовый класс для компонентов настроек,
|
||||||
|
* которые управляют массивом строк через текстовое поле с JSON.
|
||||||
|
*/
|
||||||
|
export abstract class BaseTextArraySettingComponent extends BaseSettingComponent {
|
||||||
|
/** Ссылка на текстовое поле ввода */
|
||||||
|
protected textInput: TextComponent;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Форматирует массив строк в JSON строку для отображения в UI.
|
||||||
|
* @param symbols Массив строк.
|
||||||
|
* @returns Форматированная JSON строка.
|
||||||
|
*/
|
||||||
|
protected arrayToJsonString(symbols: string[] | undefined): string {
|
||||||
|
if (!symbols) {
|
||||||
|
return '[]';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(symbols);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[${this.getSettingKey()}] Error stringifying array to JSON:`, e);
|
||||||
|
return '[]'; // Возвращаем безопасное значение
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает JSON строку из UI и парсит ее в массив строк.
|
||||||
|
* @returns Распарсенный массив строк.
|
||||||
|
* @throws Error если компонент не отрисован или если строка не является валидным JSON.
|
||||||
|
*/
|
||||||
|
getValueFromUi(): string[] {
|
||||||
|
if (this.textInput) {
|
||||||
|
const rawJson = this.textInput.getValue();
|
||||||
|
const trimmedValue = rawJson.trim();
|
||||||
|
|
||||||
|
// Считаем пустую строку или '[]' валидным представлением пустого массива
|
||||||
|
if (trimmedValue === '' || trimmedValue === '[]') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmedValue);
|
||||||
|
// Дополнительная проверка, что результат парсинга - массив
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
// Эта ошибка также будет поймана validateJsonStringArray,
|
||||||
|
// но для надежности можно оставить.
|
||||||
|
throw new Error("Input must be a valid JSON array.");
|
||||||
|
}
|
||||||
|
// Предполагаем, что содержимое массива будет проверено валидатором.
|
||||||
|
// Возвращаем как string[] для TypeScript.
|
||||||
|
return parsed as string[];
|
||||||
|
} catch (e: any) {
|
||||||
|
// Оборачиваем ошибку парсинга
|
||||||
|
throw new Error(`Invalid JSON format: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Если компонент не был отрисован
|
||||||
|
throw new Error(`[${this.getSettingKey()}] Cannot get value from UI before component is rendered.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает значение (массив строк) в UI, предварительно форматируя его в JSON.
|
||||||
|
* @param value Массив строк для установки.
|
||||||
|
* @throws Error если компонент не отрисован.
|
||||||
|
*/
|
||||||
|
setValueInUi(value: any): void {
|
||||||
|
if (this.textInput) {
|
||||||
|
// Убедимся, что на вход пришел массив, прежде чем его форматировать
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
console.warn(`[${this.getSettingKey()}] Attempted to set non-array value in TextArray component. Value:`, value);
|
||||||
|
// Можно установить пустой массив или выбросить ошибку?
|
||||||
|
// Установим пустой массив для безопасности UI.
|
||||||
|
this.textInput.setValue('[]');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const jsonStringValue = this.arrayToJsonString(value as string[]);
|
||||||
|
this.textInput.setValue(jsonStringValue);
|
||||||
|
} else {
|
||||||
|
// Если компонент не был отрисован
|
||||||
|
throw new Error(`[${this.getSettingKey()}] Cannot set value before component is rendered.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет базовую валидацию: проверяет, что строка в UI является
|
||||||
|
* валидным JSON-массивом строк по одному символу без дубликатов.
|
||||||
|
* Не проверяет на пустоту массива.
|
||||||
|
* @returns Объект ValidationError при ошибке, иначе null.
|
||||||
|
*/
|
||||||
|
validate(): ValidationError | null {
|
||||||
|
let rawValue: string;
|
||||||
|
try {
|
||||||
|
if (this.textInput) {
|
||||||
|
rawValue = this.textInput.getValue();
|
||||||
|
} else {
|
||||||
|
// Если UI нет, валидация невозможна
|
||||||
|
throw new Error("UI component not available for validation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызываем внешний валидатор для проверки формата JSON и содержимого
|
||||||
|
const jsonError = validateJsonStringArray(rawValue);
|
||||||
|
|
||||||
|
if (jsonError) {
|
||||||
|
// Добавляем ключ поля к ошибке
|
||||||
|
return {
|
||||||
|
field: this.getSettingKey(),
|
||||||
|
message: jsonError.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Ловим ошибки доступа к UI
|
||||||
|
return {
|
||||||
|
field: this.getSettingKey(),
|
||||||
|
message: error instanceof Error ? error.message : String(error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Базовая валидация JSON и содержимого прошла успешно
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/ui/components/CheckedSymbolsSettingComponent.ts
Normal file
76
src/ui/components/CheckedSymbolsSettingComponent.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { Setting } from "obsidian";
|
||||||
|
import { CheckboxSyncPluginSettings } from "src/types";
|
||||||
|
import { ValidationError } from "../validation/types";
|
||||||
|
import { validateNotEmptyArray } from "../validation/validators";
|
||||||
|
import { BaseTextArraySettingComponent } from "./BaseTextArraySettingComponent";
|
||||||
|
|
||||||
|
// ИЗМЕНЕНИЕ: Наследуемся от BaseTextArraySettingComponent
|
||||||
|
export class CheckedSymbolsSettingComponent extends BaseTextArraySettingComponent {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(); // Вызываем конструктор родителя
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Реализация специфичных для этого компонента методов ---
|
||||||
|
|
||||||
|
getSettingKey(): keyof CheckboxSyncPluginSettings {
|
||||||
|
return 'checkedSymbols';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render(container: HTMLElement, currentValue: any): void {
|
||||||
|
// Используем унаследованный arrayToJsonString
|
||||||
|
const jsonStringValue = this.arrayToJsonString(currentValue as string[]);
|
||||||
|
|
||||||
|
this.setting = new Setting(container)
|
||||||
|
// Уникальные тексты для этого компонента
|
||||||
|
.setName("Checked Symbols")
|
||||||
|
.setDesc("Enter symbols as a JSON array of single-character strings. Example: [\"x\", \"✓\", \" \"]")
|
||||||
|
.addText(text => {
|
||||||
|
// Используем унаследованное поле textInput
|
||||||
|
this.textInput = text;
|
||||||
|
text
|
||||||
|
.setPlaceholder("e.g., [\"x\", \" \"]") // Уникальный плейсхолдер?
|
||||||
|
.setValue(jsonStringValue)
|
||||||
|
// Используем унаследованный onChangeCallback
|
||||||
|
.onChange(this.onChangeCallback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Переопределение validate для добавления проверки на пустоту ---
|
||||||
|
validate(): ValidationError | null {
|
||||||
|
// 1. Выполняем базовую валидацию JSON из родительского класса
|
||||||
|
const baseError = super.validate(); // Вызов BaseTextArraySettingComponent.validate()
|
||||||
|
if (baseError) {
|
||||||
|
// Если базовый JSON невалиден, сразу возвращаем ошибку
|
||||||
|
return baseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Если базовый JSON валиден, получаем распарсенное значение
|
||||||
|
let parsedValue: string[];
|
||||||
|
try {
|
||||||
|
// Вызов getValueFromUi из BaseTextArraySettingComponent
|
||||||
|
// Теперь это безопасно, так как super.validate() гарантирует валидный JSON
|
||||||
|
parsedValue = this.getValueFromUi();
|
||||||
|
} catch (error: any) {
|
||||||
|
// На всякий случай ловим ошибку, хотя не должны сюда попасть после super.validate()
|
||||||
|
return {
|
||||||
|
field: this.getSettingKey(),
|
||||||
|
message: error instanceof Error ? error.message : String(error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Выполняем дополнительную проверку на непустой массив
|
||||||
|
const notEmptyError = validateNotEmptyArray(parsedValue);
|
||||||
|
if (notEmptyError) {
|
||||||
|
// Возвращаем ошибку "непустого массива", добавляя ключ поля
|
||||||
|
return {
|
||||||
|
field: this.getSettingKey(),
|
||||||
|
message: notEmptyError.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Все проверки (базовая + специфичная) пройдены
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/ui/components/EnableChildSyncSettingComponent.ts
Normal file
58
src/ui/components/EnableChildSyncSettingComponent.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { ToggleComponent, Setting } from "obsidian";
|
||||||
|
import { CheckboxSyncPluginSettings } from "src/types";
|
||||||
|
import { ValidationError } from "../validation/types";
|
||||||
|
import { validateValueIsBoolean } from "../validation/validators";
|
||||||
|
import { BaseSettingComponent } from "./BaseSettingComponent";
|
||||||
|
|
||||||
|
|
||||||
|
export class EnableChildSyncSettingComponent extends BaseSettingComponent {
|
||||||
|
private toggleComponent: ToggleComponent;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettingKey(): keyof CheckboxSyncPluginSettings {
|
||||||
|
return 'enableAutomaticChildState';
|
||||||
|
}
|
||||||
|
|
||||||
|
render(container: HTMLElement, currentValue: any): void {
|
||||||
|
this.setting = new Setting(container)
|
||||||
|
.setName('Update child checkbox state automatically')
|
||||||
|
.setDesc('If enabled, changing the state of a parent checkbox will automatically update the state of all its direct and nested children. If disabled, changing a parent checkbox will not affect its children.')
|
||||||
|
.addToggle(toggle => {
|
||||||
|
this.toggleComponent = toggle;
|
||||||
|
toggle
|
||||||
|
.setValue(currentValue as boolean)
|
||||||
|
.onChange(this.onChangeCallback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getValueFromUi(): boolean {
|
||||||
|
if (this.toggleComponent) {
|
||||||
|
return this.toggleComponent.getValue();
|
||||||
|
}
|
||||||
|
throw new Error(`[${this.getSettingKey()}] Cannot get value from UI before component is rendered.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValueInUi(value: any): void {
|
||||||
|
if (this.toggleComponent) {
|
||||||
|
this.toggleComponent.setValue(value as boolean);
|
||||||
|
} else {
|
||||||
|
throw new Error(`[${this.getSettingKey()}] Cannot set value before component is rendered.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(): ValidationError | null {
|
||||||
|
const valueFromUi = this.getValueFromUi();
|
||||||
|
const validationResult = validateValueIsBoolean(valueFromUi);
|
||||||
|
|
||||||
|
if (validationResult) {
|
||||||
|
return {
|
||||||
|
field: this.getSettingKey(),
|
||||||
|
message: validationResult.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/ui/components/EnableFileSyncSettingComponent.ts
Normal file
72
src/ui/components/EnableFileSyncSettingComponent.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
// src/settings/components/EnableFileSyncSettingComponent.ts
|
||||||
|
import { Setting, ToggleComponent } from 'obsidian';
|
||||||
|
import { CheckboxSyncPluginSettings } from '../../types';
|
||||||
|
import { BaseSettingComponent } from './BaseSettingComponent';
|
||||||
|
import { ValidationError } from '../validation/types';
|
||||||
|
import { validateValueIsBoolean } from '../validation/validators'; // Используем тот же валидатор
|
||||||
|
|
||||||
|
export class EnableFileSyncSettingComponent extends BaseSettingComponent {
|
||||||
|
private toggleComponent: ToggleComponent;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettingKey(): keyof CheckboxSyncPluginSettings {
|
||||||
|
// ИЗМЕНЕНИЕ: Другой ключ
|
||||||
|
return 'enableAutomaticFileSync';
|
||||||
|
}
|
||||||
|
|
||||||
|
render(container: HTMLElement, currentValue: any): void {
|
||||||
|
this.setting = new Setting(container)
|
||||||
|
// ИЗМЕНЕНИЕ: Другие тексты
|
||||||
|
.setName("Enable automatic file synchronization")
|
||||||
|
.setDesc("If enabled (requires restart or settings reload), automatically syncs checkbox states when files are loaded/opened and after settings changes. If disabled (default), sync only occurs when you manually change a checkbox.")
|
||||||
|
.addToggle(toggle => {
|
||||||
|
this.toggleComponent = toggle;
|
||||||
|
toggle
|
||||||
|
.setValue(currentValue as boolean)
|
||||||
|
.onChange(this.onChangeCallback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getValueFromUi(): boolean {
|
||||||
|
if (this.toggleComponent) {
|
||||||
|
return this.toggleComponent.getValue();
|
||||||
|
}
|
||||||
|
// Выбрасываем ошибку, если компонент не отрисован
|
||||||
|
throw new Error(`[${this.getSettingKey()}] Cannot get value from UI before component is rendered.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValueInUi(value: any): void {
|
||||||
|
if (this.toggleComponent) {
|
||||||
|
this.toggleComponent.setValue(value as boolean);
|
||||||
|
} else {
|
||||||
|
// Выбрасываем ошибку, если компонент не отрисован
|
||||||
|
throw new Error(`[${this.getSettingKey()}] Cannot set value before component is rendered.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(): ValidationError | null {
|
||||||
|
let valueFromUi: boolean;
|
||||||
|
try {
|
||||||
|
valueFromUi = this.getValueFromUi(); // Получаем значение
|
||||||
|
} catch (error) {
|
||||||
|
// Если getValueFromUi выбросил ошибку (маловероятно до рендера)
|
||||||
|
return {
|
||||||
|
field: this.getSettingKey(),
|
||||||
|
message: error instanceof Error ? error.message : String(error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationResult = validateValueIsBoolean(valueFromUi); // Используем внешний валидатор
|
||||||
|
|
||||||
|
if (validationResult) {
|
||||||
|
return {
|
||||||
|
field: this.getSettingKey(),
|
||||||
|
message: validationResult.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/ui/components/IgnoreSymbolsSettingComponent.ts
Normal file
38
src/ui/components/IgnoreSymbolsSettingComponent.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Setting } from "obsidian";
|
||||||
|
import { CheckboxSyncPluginSettings } from "src/types";
|
||||||
|
import { BaseTextArraySettingComponent } from "./BaseTextArraySettingComponent";
|
||||||
|
|
||||||
|
export class IgnoreSymbolsSettingComponent extends BaseTextArraySettingComponent {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Реализация специфичных методов ---
|
||||||
|
|
||||||
|
getSettingKey(): keyof CheckboxSyncPluginSettings {
|
||||||
|
return 'ignoreSymbols';
|
||||||
|
}
|
||||||
|
|
||||||
|
render(container: HTMLElement, currentValue: any): void {
|
||||||
|
const jsonStringValue = this.arrayToJsonString(currentValue as string[]);
|
||||||
|
|
||||||
|
this.setting = new Setting(container)
|
||||||
|
.setName("Ignore Symbols")
|
||||||
|
.setDesc("Checkboxes with these symbols will be ignored during automatic parent/child state updates. Example: [\"-\", \"~\"]")
|
||||||
|
.addText(text => {
|
||||||
|
this.textInput = text;
|
||||||
|
text
|
||||||
|
.setPlaceholder("e.g., [\"-\", \"~\"]")
|
||||||
|
.setValue(jsonStringValue)
|
||||||
|
.onChange(this.onChangeCallback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate() НЕ переопределяем. Используется базовая реализация
|
||||||
|
// из BaseTextArraySettingComponent, которая вызывает только
|
||||||
|
// validateJsonStringArray (проверка формата JSON и содержимого).
|
||||||
|
// Пустой массив здесь допустим.
|
||||||
|
|
||||||
|
// getValueFromUi, setValueInUi, arrayToJsonString унаследованы
|
||||||
|
}
|
||||||
64
src/ui/components/UncheckedSymbolsSettingComponent.ts
Normal file
64
src/ui/components/UncheckedSymbolsSettingComponent.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Setting } from "obsidian";
|
||||||
|
import { CheckboxSyncPluginSettings } from "src/types";
|
||||||
|
import { ValidationError } from "../validation/types";
|
||||||
|
import { validateNotEmptyArray } from "../validation/validators";
|
||||||
|
import { BaseTextArraySettingComponent } from "./BaseTextArraySettingComponent";
|
||||||
|
|
||||||
|
|
||||||
|
export class UncheckedSymbolsSettingComponent extends BaseTextArraySettingComponent {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Реализация специфичных методов ---
|
||||||
|
|
||||||
|
getSettingKey(): keyof CheckboxSyncPluginSettings {
|
||||||
|
return 'uncheckedSymbols';
|
||||||
|
}
|
||||||
|
|
||||||
|
render(container: HTMLElement, currentValue: any): void {
|
||||||
|
const jsonStringValue = this.arrayToJsonString(currentValue as string[]);
|
||||||
|
|
||||||
|
this.setting = new Setting(container)
|
||||||
|
.setName("Unchecked Symbols")
|
||||||
|
.setDesc("Enter symbols as a JSON array of single-character strings. Example: [\" \", \"?\", \",\"]")
|
||||||
|
.addText(text => {
|
||||||
|
this.textInput = text;
|
||||||
|
text
|
||||||
|
.setPlaceholder("e.g., [\" \", \"?\", \",\"]")
|
||||||
|
.setValue(jsonStringValue)
|
||||||
|
.onChange(this.onChangeCallback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переопределяем validate, чтобы добавить проверку на пустоту
|
||||||
|
validate(): ValidationError | null {
|
||||||
|
// 1. Базовая валидация JSON из родителя
|
||||||
|
const baseError = super.validate();
|
||||||
|
if (baseError) {
|
||||||
|
return baseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Получаем значение (теперь безопасно)
|
||||||
|
let parsedValue: string[];
|
||||||
|
try {
|
||||||
|
parsedValue = this.getValueFromUi();
|
||||||
|
} catch (error: any) {
|
||||||
|
return { field: this.getSettingKey(), message: error.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Дополнительная проверка на непустой массив
|
||||||
|
const notEmptyError = validateNotEmptyArray(parsedValue);
|
||||||
|
if (notEmptyError) {
|
||||||
|
return {
|
||||||
|
field: this.getSettingKey(),
|
||||||
|
message: notEmptyError.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Все проверки пройдены
|
||||||
|
}
|
||||||
|
|
||||||
|
// getValueFromUi, setValueInUi, arrayToJsonString унаследованы
|
||||||
|
}
|
||||||
72
src/ui/components/UnknownPolicySettingComponent.ts
Normal file
72
src/ui/components/UnknownPolicySettingComponent.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { DropdownComponent, Setting } from "obsidian";
|
||||||
|
import { CheckboxSyncPluginSettings, CheckboxState } from "src/types";
|
||||||
|
import { ValidationError } from "../validation/types";
|
||||||
|
import { validateIsCheckboxState } from "../validation/validators";
|
||||||
|
import { BaseSettingComponent } from "./BaseSettingComponent";
|
||||||
|
|
||||||
|
|
||||||
|
export class UnknownPolicySettingComponent extends BaseSettingComponent {
|
||||||
|
private dropdownComponent: DropdownComponent;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettingKey(): keyof CheckboxSyncPluginSettings {
|
||||||
|
return 'unknownSymbolPolicy';
|
||||||
|
}
|
||||||
|
|
||||||
|
render(container: HTMLElement, currentValue: any): void {
|
||||||
|
this.setting = new Setting(container)
|
||||||
|
.setName('Unknown Symbol Policy')
|
||||||
|
.setDesc('How to treat symbols not in Checked, Unchecked or Ignore lists.')
|
||||||
|
.addDropdown(dropdown => {
|
||||||
|
this.dropdownComponent = dropdown;
|
||||||
|
dropdown
|
||||||
|
.addOption(CheckboxState.Checked, 'Treat as Checked')
|
||||||
|
.addOption(CheckboxState.Unchecked, 'Treat as Unchecked')
|
||||||
|
.addOption(CheckboxState.Ignore, 'Ignore')
|
||||||
|
.setValue(currentValue as string) // Dropdown работает со строками
|
||||||
|
.onChange(this.onChangeCallback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getValueFromUi(): CheckboxState {
|
||||||
|
if (this.dropdownComponent) {
|
||||||
|
// Значение из dropdown - строка, кастуем к CheckboxState
|
||||||
|
return this.dropdownComponent.getValue() as CheckboxState;
|
||||||
|
}
|
||||||
|
throw new Error(`[${this.getSettingKey()}] Cannot get value from UI before component is rendered.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValueInUi(value: any): void {
|
||||||
|
if (this.dropdownComponent) {
|
||||||
|
this.dropdownComponent.setValue(value as string);
|
||||||
|
} else {
|
||||||
|
throw new Error(`[${this.getSettingKey()}] Cannot set value before component is rendered.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(): ValidationError | null {
|
||||||
|
let valueFromUi: CheckboxState;
|
||||||
|
try {
|
||||||
|
valueFromUi = this.getValueFromUi();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
field: this.getSettingKey(),
|
||||||
|
message: error instanceof Error ? error.message : String(error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызываем внешний валидатор
|
||||||
|
const validationResult = validateIsCheckboxState(valueFromUi);
|
||||||
|
|
||||||
|
if (validationResult) {
|
||||||
|
return {
|
||||||
|
field: this.getSettingKey(),
|
||||||
|
message: validationResult.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// src/settings/validation/validators.ts
|
// src/settings/validation/validators.ts
|
||||||
|
|
||||||
|
import { CheckboxState } from "src/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет, является ли предоставленное значение булевым типом (boolean).
|
* Проверяет, является ли предоставленное значение булевым типом (boolean).
|
||||||
* Эта функция не зависит от контекста Obsidian или конкретного компонента.
|
* Эта функция не зависит от контекста Obsidian или конкретного компонента.
|
||||||
|
|
@ -9,41 +11,85 @@
|
||||||
*/
|
*/
|
||||||
export function validateValueIsBoolean(value: any): { message: string } | null {
|
export function validateValueIsBoolean(value: any): { message: string } | null {
|
||||||
if (typeof value !== 'boolean') {
|
if (typeof value !== 'boolean') {
|
||||||
return { message: 'Value must be a boolean.' };
|
return { message: 'Value must be a boolean.' };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Сюда в будущем можно будет добавлять другие чистые функции валидации ---
|
/**
|
||||||
|
* Проверяет, является ли значение допустимым состоянием CheckboxState.
|
||||||
|
* @param value Значение для проверки.
|
||||||
|
* @returns Объект с сообщением об ошибке, если значение недопустимо, иначе null.
|
||||||
|
*/
|
||||||
|
export function validateIsCheckboxState(value: any): { message: string } | null {
|
||||||
|
// Получаем все возможные значения из enum CheckboxState
|
||||||
|
const validStates = Object.values(CheckboxState);
|
||||||
|
if (validStates.includes(value as CheckboxState)) {
|
||||||
|
return null; // Значение допустимо
|
||||||
|
} else {
|
||||||
|
return { message: `Invalid checkbox state selected. Must be one of: ${validStates.join(', ')}.` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
// Пример для будущей валидации JSON-массива строк
|
* Проверяет, является ли строка валидным JSON-массивом строк,
|
||||||
export function validateJsonStringArray(rawJson: string): { message: string } | null {
|
* где каждая строка состоит ровно из одного символа.
|
||||||
|
* Также проверяет на уникальность символов.
|
||||||
|
* @param rawJson Строка для проверки.
|
||||||
|
* @returns Объект с сообщением об ошибке, если невалидно, иначе null.
|
||||||
|
*/
|
||||||
|
export function validateJsonStringArray(rawJson: string | null | undefined): { message: string } | null {
|
||||||
|
if (rawJson == null || typeof rawJson !== 'string') {
|
||||||
|
return { message: "Input must be a string." }; // Или можно вернуть null, если пустая строка допустима как пустой массив? Зависит от требований.
|
||||||
|
}
|
||||||
|
const trimmedValue = rawJson.trim();
|
||||||
|
// Считаем пустую строку валидным представлением пустого массива []
|
||||||
|
if (trimmedValue === '' || trimmedValue === '[]') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: any;
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(rawJson);
|
parsed = JSON.parse(trimmedValue);
|
||||||
if (!Array.isArray(parsed)) {
|
|
||||||
return { message: 'Input must be a valid JSON array (e.g., ["x", " "]).' };
|
|
||||||
}
|
|
||||||
for (let i = 0; i < parsed.length; i++) {
|
|
||||||
const element = parsed[i];
|
|
||||||
if (typeof element !== 'string') {
|
|
||||||
return { message: `Array element at index ${i} is not a string.` };
|
|
||||||
}
|
|
||||||
if (element.length !== 1) {
|
|
||||||
return { message: `Element "${element}" at index ${i} must be a single character.` };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null; // Все проверки пройдены
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return { message: `Invalid JSON format: ${e.message}` };
|
return { message: `Invalid JSON format: ${e.message}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return { message: "Invalid format: Input must be a valid JSON array (e.g., [\"x\", \" \"])." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = new Set<string>();
|
||||||
|
for (let i = 0; i < parsed.length; i++) {
|
||||||
|
const element = parsed[i];
|
||||||
|
if (typeof element !== 'string') {
|
||||||
|
return { message: `Invalid content: Array element at index ${i} is not a string.` };
|
||||||
|
}
|
||||||
|
if (element.length !== 1) {
|
||||||
|
return { message: `Invalid content: Element "${element}" at index ${i} must be a single character.` };
|
||||||
|
}
|
||||||
|
if (symbols.has(element)) {
|
||||||
|
return { message: `Invalid content: Duplicate symbol "${element}" found.` };
|
||||||
|
}
|
||||||
|
symbols.add(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Все проверки пройдены
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пример для проверки, что массив не пустой
|
|
||||||
export function validateNotEmptyArray(arr: any[]): { message: string } | null {
|
/**
|
||||||
if (!Array.isArray(arr) || arr.length === 0) {
|
* Проверяет, является ли предоставленный массив непустым.
|
||||||
return { message: 'The list cannot be empty.' };
|
* @param arr Массив для проверки.
|
||||||
|
* @returns Объект с сообщением об ошибке, если массив пуст или не является массивом, иначе null.
|
||||||
|
*/
|
||||||
|
export function validateNotEmptyArray(arr: any): { message: string } | null {
|
||||||
|
// Добавим проверку, что это действительно массив
|
||||||
|
if (!Array.isArray(arr)) {
|
||||||
|
return { message: 'Value must be an array.' }; // Ошибка типа данных
|
||||||
|
}
|
||||||
|
if (arr.length === 0) {
|
||||||
|
return { message: 'The list cannot be empty.' };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
Loading…
Reference in a new issue