diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index f76b4eda..d62aa6dc 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -26,6 +26,11 @@ Example: ## Added +- Adopted Obsidian 1.11.0 API features with backwards compatibility + - Settings tab now displays TaskNotes icon in the sidebar (Obsidian 1.11.0+) + - Settings sections now use native `SettingGroup` for improved visual grouping (Obsidian 1.11.0+) + - Falls back gracefully to traditional section headers on older Obsidian versions + - (#59) Added `shortYear` template variable for custom filename and folder templates - Use `{shortYear}` in filename templates (e.g., "25" for 2025) - Use `{{shortYear}}` in folder templates @@ -39,6 +44,11 @@ Example: ## Fixed +- (#1386) Fixed `timeEstimateCategory` formula showing "Long (>2h)" instead of "No estimate" for new tasks + - The condition didn't properly handle null values when `timeEstimate` property is unset + - Also fixed the same issue in `trackingStatus` formula + - Thanks to @nicou for reporting and suggesting the fix + - (#1397) Fixed Bases views (Kanban, Calendar, Task List) resetting to Calendar view after a few minutes - Views would show "?" in the Views menu due to view type mismatch - Caused by typo in view type properties not matching registration IDs diff --git a/src/settings/TaskNotesSettingTab.ts b/src/settings/TaskNotesSettingTab.ts index dbecd48b..60089015 100644 --- a/src/settings/TaskNotesSettingTab.ts +++ b/src/settings/TaskNotesSettingTab.ts @@ -1,4 +1,4 @@ -import { App, PluginSettingTab, Platform } from "obsidian"; +import { App, PluginSettingTab, Platform, requireApiVersion } from "obsidian"; import TaskNotesPlugin from "../main"; import { debounce } from "./components/settingHelpers"; import { renderGeneralTab } from "./tabs/generalTab"; @@ -25,6 +25,11 @@ export class TaskNotesSettingTab extends PluginSettingTab { super(app, plugin); this.plugin = plugin; + // Set icon for settings sidebar (Obsidian 1.11.0+) + if (requireApiVersion("1.11.0")) { + this.icon = "tasknotes-simple"; + } + this.plugin.registerEvent( this.plugin.i18n.on("locale-changed", () => { if (this.containerEl.isConnected) { diff --git a/src/settings/components/settingHelpers.ts b/src/settings/components/settingHelpers.ts index c2a8160f..c9dac3bb 100644 --- a/src/settings/components/settingHelpers.ts +++ b/src/settings/components/settingHelpers.ts @@ -1,4 +1,4 @@ -import { Setting } from "obsidian"; +import { Setting, SettingGroup, requireApiVersion } from "obsidian"; export interface ToggleSettingOptions { name: string; @@ -46,14 +46,100 @@ export interface ButtonSettingOptions { buttonClass?: string; } +export interface SettingGroupOptions { + heading: string; + description?: string; + className?: string; +} + /** - * Helper for creating standard toggle settings + * Check if the current Obsidian version supports SettingGroup (1.11.0+) */ -export function createToggleSetting( +function supportsSettingGroup(): boolean { + return requireApiVersion("1.11.0"); +} + +/** + * Legacy fallback that mimics SettingGroup API for Obsidian < 1.11.0 + * Uses the old pattern of section headers and individual settings + */ +class LegacySettingGroup { + private containerEl: HTMLElement; + + constructor(containerEl: HTMLElement) { + this.containerEl = containerEl; + } + + setHeading(text: string | DocumentFragment): this { + new Setting(this.containerEl).setName(text).setHeading(); + return this; + } + + addClass(_cls: string): this { + // No-op for legacy - classes were not applied to groups + return this; + } + + addSetting(cb: (setting: Setting) => void): this { + const setting = new Setting(this.containerEl); + cb(setting); + return this; + } +} + +/** + * Helper for creating a setting group with heading + * Uses native SettingGroup on Obsidian 1.11.0+, falls back to legacy pattern on older versions + */ +export function createSettingGroup( container: HTMLElement, - options: ToggleSettingOptions -): Setting { - return new Setting(container) + options: SettingGroupOptions, + addSettings: (group: SettingGroup | LegacySettingGroup) => void +): SettingGroup | LegacySettingGroup { + if (supportsSettingGroup()) { + // Use native SettingGroup on Obsidian 1.11.0+ + const group = new SettingGroup(container).setHeading(options.heading); + + if (options.className) { + group.addClass(options.className); + } + + // Add description as help text if provided + if (options.description) { + group.addSetting((setting) => { + setting.setDesc(options.description!); + setting.settingEl.addClass("settings-view__group-description"); + }); + } + + addSettings(group); + return group; + } else { + // Fall back to legacy pattern on older Obsidian versions + const group = new LegacySettingGroup(container).setHeading(options.heading); + + if (options.className) { + group.addClass(options.className); + } + + // Add description as help text if provided + if (options.description) { + group.addSetting((setting) => { + setting.setDesc(options.description!); + setting.settingEl.addClass("settings-view__group-description"); + }); + } + + addSettings(group); + return group; + } +} + +/** + * Helper for configuring a toggle setting (works with SettingGroup.addSetting) + */ +export function configureToggleSetting(setting: Setting, options: ToggleSettingOptions): Setting { + return setting .setName(options.name) .setDesc(options.desc) .addToggle((toggle) => { @@ -62,10 +148,20 @@ export function createToggleSetting( } /** - * Helper for creating standard text input settings + * Helper for creating standard toggle settings */ -export function createTextSetting(container: HTMLElement, options: TextSettingOptions): Setting { - return new Setting(container) +export function createToggleSetting( + container: HTMLElement, + options: ToggleSettingOptions +): Setting { + return configureToggleSetting(new Setting(container), options); +} + +/** + * Helper for configuring a text input setting (works with SettingGroup.addSetting) + */ +export function configureTextSetting(setting: Setting, options: TextSettingOptions): Setting { + return setting .setName(options.name) .setDesc(options.desc) .addText((text) => { @@ -95,13 +191,17 @@ export function createTextSetting(container: HTMLElement, options: TextSettingOp } /** - * Helper for creating standard dropdown settings + * Helper for creating standard text input settings */ -export function createDropdownSetting( - container: HTMLElement, - options: DropdownSettingOptions -): Setting { - return new Setting(container) +export function createTextSetting(container: HTMLElement, options: TextSettingOptions): Setting { + return configureTextSetting(new Setting(container), options); +} + +/** + * Helper for configuring a dropdown setting (works with SettingGroup.addSetting) + */ +export function configureDropdownSetting(setting: Setting, options: DropdownSettingOptions): Setting { + return setting .setName(options.name) .setDesc(options.desc) .addDropdown((dropdown) => { @@ -120,17 +220,24 @@ export function createDropdownSetting( } /** - * Helper for creating standard number input settings + * Helper for creating standard dropdown settings */ -export function createNumberSetting( +export function createDropdownSetting( container: HTMLElement, - options: NumberSettingOptions + options: DropdownSettingOptions ): Setting { + return configureDropdownSetting(new Setting(container), options); +} + +/** + * Helper for configuring a number input setting (works with SettingGroup.addSetting) + */ +export function configureNumberSetting(setting: Setting, options: NumberSettingOptions): Setting { const setValue = options.debounceMs ? debounce(options.setValue, options.debounceMs) : options.setValue; - return new Setting(container) + return setting .setName(options.name) .setDesc(options.desc) .addText((text) => { @@ -169,13 +276,20 @@ export function createNumberSetting( } /** - * Helper for creating standard button settings + * Helper for creating standard number input settings */ -export function createButtonSetting( +export function createNumberSetting( container: HTMLElement, - options: ButtonSettingOptions + options: NumberSettingOptions ): Setting { - return new Setting(container) + return configureNumberSetting(new Setting(container), options); +} + +/** + * Helper for configuring a button setting (works with SettingGroup.addSetting) + */ +export function configureButtonSetting(setting: Setting, options: ButtonSettingOptions): Setting { + return setting .setName(options.name) .setDesc(options.desc) .addButton((button) => { @@ -191,6 +305,16 @@ export function createButtonSetting( }); } +/** + * Helper for creating standard button settings + */ +export function createButtonSetting( + container: HTMLElement, + options: ButtonSettingOptions +): Setting { + return configureButtonSetting(new Setting(container), options); +} + /** * Helper for creating section headers */ diff --git a/src/settings/tabs/appearanceTab.ts b/src/settings/tabs/appearanceTab.ts index 43ed588f..a3618dd0 100644 --- a/src/settings/tabs/appearanceTab.ts +++ b/src/settings/tabs/appearanceTab.ts @@ -1,13 +1,12 @@ -import { Setting, Notice } from "obsidian"; +import { Notice } from "obsidian"; import TaskNotesPlugin from "../../main"; import type { TranslationKey } from "../../i18n"; import { - createSectionHeader, - createTextSetting, - createToggleSetting, - createDropdownSetting, - createNumberSetting, - createHelpText, + createSettingGroup, + configureTextSetting, + configureToggleSetting, + configureDropdownSetting, + configureNumberSetting, } from "../components/settingHelpers"; import { PropertySelectorModal } from "../../modals/PropertySelectorModal"; import { getAvailableProperties, getPropertyLabels } from "../../utils/propertyHelpers"; @@ -26,611 +25,648 @@ export function renderAppearanceTab( plugin.i18n.translate(key, params); // Task Cards Section - createSectionHeader(container, translate("settings.appearance.taskCards.header")); - createHelpText(container, translate("settings.appearance.taskCards.description")); - - // Default visible properties const availableProperties = getAvailableProperties(plugin); const currentProperties = plugin.settings.defaultVisibleProperties || []; - - new Setting(container) - .setName(translate("settings.appearance.taskCards.defaultVisibleProperties.name")) - .setDesc(translate("settings.appearance.taskCards.defaultVisibleProperties.description")) - .addButton((button) => { - button.setButtonText("Configure").onClick(() => { - const modal = new PropertySelectorModal( - plugin.app, - availableProperties, - currentProperties, - async (selected) => { - plugin.settings.defaultVisibleProperties = selected; - save(); - new Notice("Default task card properties updated"); - // Re-render to update display - renderAppearanceTab(container, plugin, save); - }, - "Select Default Task Card Properties", - "Choose which properties to display in task cards (views, kanban, etc.). Selected properties will appear in the order shown below." - ); - modal.open(); - }); - }); - const currentLabels = getPropertyLabels(plugin, currentProperties); - createHelpText(container, `Currently showing: ${currentLabels.join(", ")}`); - // Note: Task Filenames settings have been moved to the Title property card in Task Properties tab + createSettingGroup( + container, + { + heading: translate("settings.appearance.taskCards.header"), + description: translate("settings.appearance.taskCards.description"), + }, + (group) => { + group.addSetting((setting) => { + setting + .setName(translate("settings.appearance.taskCards.defaultVisibleProperties.name")) + .setDesc(translate("settings.appearance.taskCards.defaultVisibleProperties.description")) + .addButton((button) => { + button.setButtonText("Configure").onClick(() => { + const modal = new PropertySelectorModal( + plugin.app, + availableProperties, + currentProperties, + async (selected) => { + plugin.settings.defaultVisibleProperties = selected; + save(); + new Notice("Default task card properties updated"); + // Re-render to update display + renderAppearanceTab(container, plugin, save); + }, + "Select Default Task Card Properties", + "Choose which properties to display in task cards (views, kanban, etc.). Selected properties will appear in the order shown below." + ); + modal.open(); + }); + }); + }); + + // Show currently selected properties + group.addSetting((setting) => { + setting.setDesc(`Currently showing: ${currentLabels.join(", ")}`); + setting.settingEl.addClass("settings-view__group-description"); + }); + } + ); // Display Formatting Section - createSectionHeader(container, translate("settings.appearance.displayFormatting.header")); - createHelpText(container, translate("settings.appearance.displayFormatting.description")); - - createDropdownSetting(container, { - name: translate("settings.appearance.displayFormatting.timeFormat.name"), - desc: translate("settings.appearance.displayFormatting.timeFormat.description"), - options: [ - { - value: "12", - label: translate( - "settings.appearance.displayFormatting.timeFormat.options.twelveHour" - ), - }, - { - value: "24", - label: translate( - "settings.appearance.displayFormatting.timeFormat.options.twentyFourHour" - ), - }, - ], - getValue: () => plugin.settings.calendarViewSettings.timeFormat, - setValue: async (value: string) => { - plugin.settings.calendarViewSettings.timeFormat = value as "12" | "24"; - save(); + createSettingGroup( + container, + { + heading: translate("settings.appearance.displayFormatting.header"), + description: translate("settings.appearance.displayFormatting.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.appearance.displayFormatting.timeFormat.name"), + desc: translate("settings.appearance.displayFormatting.timeFormat.description"), + options: [ + { + value: "12", + label: translate( + "settings.appearance.displayFormatting.timeFormat.options.twelveHour" + ), + }, + { + value: "24", + label: translate( + "settings.appearance.displayFormatting.timeFormat.options.twentyFourHour" + ), + }, + ], + getValue: () => plugin.settings.calendarViewSettings.timeFormat, + setValue: async (value: string) => { + plugin.settings.calendarViewSettings.timeFormat = value as "12" | "24"; + save(); + }, + }) + ); + } + ); // Calendar View Section - createSectionHeader(container, translate("settings.appearance.calendarView.header")); - createHelpText(container, translate("settings.appearance.calendarView.description")); - - createDropdownSetting(container, { - name: translate("settings.appearance.calendarView.defaultView.name"), - desc: translate("settings.appearance.calendarView.defaultView.description"), - options: [ - { - value: "dayGridMonth", - label: translate("settings.appearance.calendarView.defaultView.options.monthGrid"), - }, - { - value: "timeGridWeek", - label: translate( - "settings.appearance.calendarView.defaultView.options.weekTimeline" - ), - }, - { - value: "timeGridDay", - label: translate( - "settings.appearance.calendarView.defaultView.options.dayTimeline" - ), - }, - { - value: "multiMonthYear", - label: translate("settings.appearance.calendarView.defaultView.options.yearView"), - }, - { - value: "timeGridCustom", - label: translate( - "settings.appearance.calendarView.defaultView.options.customMultiDay" - ), - }, - ], - getValue: () => plugin.settings.calendarViewSettings.defaultView, - setValue: async (value: string) => { - plugin.settings.calendarViewSettings.defaultView = value as any; - save(); - // Re-render to show custom day count if needed - renderAppearanceTab(container, plugin, save); + createSettingGroup( + container, + { + heading: translate("settings.appearance.calendarView.header"), + description: translate("settings.appearance.calendarView.description"), }, - }); - - if (plugin.settings.calendarViewSettings.defaultView === "timeGridCustom") { - createNumberSetting(container, { - name: translate("settings.appearance.calendarView.customDayCount.name"), - desc: translate("settings.appearance.calendarView.customDayCount.description"), - placeholder: translate("settings.appearance.calendarView.customDayCount.placeholder"), - min: 2, - max: 10, - getValue: () => plugin.settings.calendarViewSettings.customDayCount, - setValue: async (value: number) => { - plugin.settings.calendarViewSettings.customDayCount = value; - save(); - }, - }); - } - - createDropdownSetting(container, { - name: translate("settings.appearance.calendarView.firstDayOfWeek.name"), - desc: translate("settings.appearance.calendarView.firstDayOfWeek.description"), - options: [ - { value: "0", label: translate("common.weekdays.sunday") }, - { value: "1", label: translate("common.weekdays.monday") }, - { value: "2", label: translate("common.weekdays.tuesday") }, - { value: "3", label: translate("common.weekdays.wednesday") }, - { value: "4", label: translate("common.weekdays.thursday") }, - { value: "5", label: translate("common.weekdays.friday") }, - { value: "6", label: translate("common.weekdays.saturday") }, - ], - getValue: () => plugin.settings.calendarViewSettings.firstDay.toString(), - setValue: async (value: string) => { - plugin.settings.calendarViewSettings.firstDay = parseInt(value) as any; - save(); - }, - }); - - createToggleSetting(container, { - name: translate("settings.appearance.calendarView.showWeekends.name"), - desc: translate("settings.appearance.calendarView.showWeekends.description"), - getValue: () => plugin.settings.calendarViewSettings.showWeekends, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.showWeekends = value; - save(); - }, - }); - - createToggleSetting(container, { - name: translate("settings.appearance.calendarView.showWeekNumbers.name"), - desc: translate("settings.appearance.calendarView.showWeekNumbers.description"), - getValue: () => plugin.settings.calendarViewSettings.weekNumbers, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.weekNumbers = value; - save(); - }, - }); - - createToggleSetting(container, { - name: translate("settings.appearance.calendarView.showTodayHighlight.name"), - desc: translate("settings.appearance.calendarView.showTodayHighlight.description"), - getValue: () => plugin.settings.calendarViewSettings.showTodayHighlight, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.showTodayHighlight = value; - save(); - }, - }); - - createToggleSetting(container, { - name: translate("settings.appearance.calendarView.showCurrentTimeIndicator.name"), - desc: translate("settings.appearance.calendarView.showCurrentTimeIndicator.description"), - getValue: () => plugin.settings.calendarViewSettings.nowIndicator, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.nowIndicator = value; - save(); - }, - }); - - createToggleSetting(container, { - name: translate("settings.appearance.calendarView.selectionMirror.name"), - desc: translate("settings.appearance.calendarView.selectionMirror.description"), - getValue: () => plugin.settings.calendarViewSettings.selectMirror, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.selectMirror = value; - save(); - }, - }); - - // Calendar locale setting with blur validation (not on every keystroke) - new Setting(container) - .setName(translate("settings.appearance.calendarView.calendarLocale.name")) - .setDesc(translate("settings.appearance.calendarView.calendarLocale.description")) - .addText((text) => { - text.setPlaceholder(translate("settings.appearance.calendarView.calendarLocale.placeholder")); - text.setValue(plugin.settings.calendarViewSettings.locale || ""); - text.inputEl.addClass("settings-view__input"); - - // Validate and save on blur (when user clicks out of field) - text.inputEl.addEventListener("blur", () => { - const trimmed = text.getValue().trim(); - if (trimmed) { - try { - // Use Intl.getCanonicalLocales to validate the locale tag - Intl.getCanonicalLocales(trimmed); - plugin.settings.calendarViewSettings.locale = trimmed; + (group) => { + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.appearance.calendarView.defaultView.name"), + desc: translate("settings.appearance.calendarView.defaultView.description"), + options: [ + { + value: "dayGridMonth", + label: translate("settings.appearance.calendarView.defaultView.options.monthGrid"), + }, + { + value: "timeGridWeek", + label: translate( + "settings.appearance.calendarView.defaultView.options.weekTimeline" + ), + }, + { + value: "timeGridDay", + label: translate( + "settings.appearance.calendarView.defaultView.options.dayTimeline" + ), + }, + { + value: "multiMonthYear", + label: translate("settings.appearance.calendarView.defaultView.options.yearView"), + }, + { + value: "timeGridCustom", + label: translate( + "settings.appearance.calendarView.defaultView.options.customMultiDay" + ), + }, + ], + getValue: () => plugin.settings.calendarViewSettings.defaultView, + setValue: async (value: string) => { + plugin.settings.calendarViewSettings.defaultView = value as any; save(); - } catch { - // Invalid locale - show notice and clear the field - new Notice(translate("settings.appearance.calendarView.calendarLocale.invalidLocale")); - plugin.settings.calendarViewSettings.locale = ""; - text.setValue(""); + // Re-render to show custom day count if needed + renderAppearanceTab(container, plugin, save); + }, + }) + ); + + if (plugin.settings.calendarViewSettings.defaultView === "timeGridCustom") { + group.addSetting((setting) => + configureNumberSetting(setting, { + name: translate("settings.appearance.calendarView.customDayCount.name"), + desc: translate("settings.appearance.calendarView.customDayCount.description"), + placeholder: translate("settings.appearance.calendarView.customDayCount.placeholder"), + min: 2, + max: 10, + getValue: () => plugin.settings.calendarViewSettings.customDayCount, + setValue: async (value: number) => { + plugin.settings.calendarViewSettings.customDayCount = value; + save(); + }, + }) + ); + } + + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.appearance.calendarView.firstDayOfWeek.name"), + desc: translate("settings.appearance.calendarView.firstDayOfWeek.description"), + options: [ + { value: "0", label: translate("common.weekdays.sunday") }, + { value: "1", label: translate("common.weekdays.monday") }, + { value: "2", label: translate("common.weekdays.tuesday") }, + { value: "3", label: translate("common.weekdays.wednesday") }, + { value: "4", label: translate("common.weekdays.thursday") }, + { value: "5", label: translate("common.weekdays.friday") }, + { value: "6", label: translate("common.weekdays.saturday") }, + ], + getValue: () => plugin.settings.calendarViewSettings.firstDay.toString(), + setValue: async (value: string) => { + plugin.settings.calendarViewSettings.firstDay = parseInt(value) as any; save(); - } - } else { - // Empty string is valid (means auto-detect) - plugin.settings.calendarViewSettings.locale = ""; - save(); - } + }, + }) + ); + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.calendarView.showWeekends.name"), + desc: translate("settings.appearance.calendarView.showWeekends.description"), + getValue: () => plugin.settings.calendarViewSettings.showWeekends, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.showWeekends = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.calendarView.showWeekNumbers.name"), + desc: translate("settings.appearance.calendarView.showWeekNumbers.description"), + getValue: () => plugin.settings.calendarViewSettings.weekNumbers, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.weekNumbers = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.calendarView.showTodayHighlight.name"), + desc: translate("settings.appearance.calendarView.showTodayHighlight.description"), + getValue: () => plugin.settings.calendarViewSettings.showTodayHighlight, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.showTodayHighlight = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.calendarView.showCurrentTimeIndicator.name"), + desc: translate("settings.appearance.calendarView.showCurrentTimeIndicator.description"), + getValue: () => plugin.settings.calendarViewSettings.nowIndicator, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.nowIndicator = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.calendarView.selectionMirror.name"), + desc: translate("settings.appearance.calendarView.selectionMirror.description"), + getValue: () => plugin.settings.calendarViewSettings.selectMirror, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.selectMirror = value; + save(); + }, + }) + ); + + // Calendar locale setting with blur validation + group.addSetting((setting) => { + setting + .setName(translate("settings.appearance.calendarView.calendarLocale.name")) + .setDesc(translate("settings.appearance.calendarView.calendarLocale.description")) + .addText((text) => { + text.setPlaceholder(translate("settings.appearance.calendarView.calendarLocale.placeholder")); + text.setValue(plugin.settings.calendarViewSettings.locale || ""); + text.inputEl.addClass("settings-view__input"); + + // Validate and save on blur (when user clicks out of field) + text.inputEl.addEventListener("blur", () => { + const trimmed = text.getValue().trim(); + if (trimmed) { + try { + // Use Intl.getCanonicalLocales to validate the locale tag + Intl.getCanonicalLocales(trimmed); + plugin.settings.calendarViewSettings.locale = trimmed; + save(); + } catch { + // Invalid locale - show notice and clear the field + new Notice(translate("settings.appearance.calendarView.calendarLocale.invalidLocale")); + plugin.settings.calendarViewSettings.locale = ""; + text.setValue(""); + save(); + } + } else { + // Empty string is valid (means auto-detect) + plugin.settings.calendarViewSettings.locale = ""; + save(); + } + }); + }); }); - }); + } + ); // Default event visibility section - createSectionHeader(container, translate("settings.appearance.defaultEventVisibility.header")); - createHelpText(container, translate("settings.appearance.defaultEventVisibility.description")); - - createToggleSetting(container, { - name: translate("settings.appearance.defaultEventVisibility.showScheduledTasks.name"), - desc: translate( - "settings.appearance.defaultEventVisibility.showScheduledTasks.description" - ), - getValue: () => plugin.settings.calendarViewSettings.defaultShowScheduled, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.defaultShowScheduled = value; - save(); + createSettingGroup( + container, + { + heading: translate("settings.appearance.defaultEventVisibility.header"), + description: translate("settings.appearance.defaultEventVisibility.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.defaultEventVisibility.showScheduledTasks.name"), + desc: translate("settings.appearance.defaultEventVisibility.showScheduledTasks.description"), + getValue: () => plugin.settings.calendarViewSettings.defaultShowScheduled, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.defaultShowScheduled = value; + save(); + }, + }) + ); - createToggleSetting(container, { - name: translate("settings.appearance.defaultEventVisibility.showDueDates.name"), - desc: translate("settings.appearance.defaultEventVisibility.showDueDates.description"), - getValue: () => plugin.settings.calendarViewSettings.defaultShowDue, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.defaultShowDue = value; - save(); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.defaultEventVisibility.showDueDates.name"), + desc: translate("settings.appearance.defaultEventVisibility.showDueDates.description"), + getValue: () => plugin.settings.calendarViewSettings.defaultShowDue, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.defaultShowDue = value; + save(); + }, + }) + ); - createToggleSetting(container, { - name: translate("settings.appearance.defaultEventVisibility.showDueWhenScheduled.name"), - desc: translate( - "settings.appearance.defaultEventVisibility.showDueWhenScheduled.description" - ), - getValue: () => plugin.settings.calendarViewSettings.defaultShowDueWhenScheduled, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.defaultShowDueWhenScheduled = value; - save(); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.defaultEventVisibility.showDueWhenScheduled.name"), + desc: translate("settings.appearance.defaultEventVisibility.showDueWhenScheduled.description"), + getValue: () => plugin.settings.calendarViewSettings.defaultShowDueWhenScheduled, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.defaultShowDueWhenScheduled = value; + save(); + }, + }) + ); - createToggleSetting(container, { - name: translate("settings.appearance.defaultEventVisibility.showTimeEntries.name"), - desc: translate("settings.appearance.defaultEventVisibility.showTimeEntries.description"), - getValue: () => plugin.settings.calendarViewSettings.defaultShowTimeEntries, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.defaultShowTimeEntries = value; - save(); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.defaultEventVisibility.showTimeEntries.name"), + desc: translate("settings.appearance.defaultEventVisibility.showTimeEntries.description"), + getValue: () => plugin.settings.calendarViewSettings.defaultShowTimeEntries, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.defaultShowTimeEntries = value; + save(); + }, + }) + ); - createToggleSetting(container, { - name: translate("settings.appearance.defaultEventVisibility.showRecurringTasks.name"), - desc: translate( - "settings.appearance.defaultEventVisibility.showRecurringTasks.description" - ), - getValue: () => plugin.settings.calendarViewSettings.defaultShowRecurring, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.defaultShowRecurring = value; - save(); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.defaultEventVisibility.showRecurringTasks.name"), + desc: translate("settings.appearance.defaultEventVisibility.showRecurringTasks.description"), + getValue: () => plugin.settings.calendarViewSettings.defaultShowRecurring, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.defaultShowRecurring = value; + save(); + }, + }) + ); - createToggleSetting(container, { - name: translate("settings.appearance.defaultEventVisibility.showICSEvents.name"), - desc: translate("settings.appearance.defaultEventVisibility.showICSEvents.description"), - getValue: () => plugin.settings.calendarViewSettings.defaultShowICSEvents, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.defaultShowICSEvents = value; - save(); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.defaultEventVisibility.showICSEvents.name"), + desc: translate("settings.appearance.defaultEventVisibility.showICSEvents.description"), + getValue: () => plugin.settings.calendarViewSettings.defaultShowICSEvents, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.defaultShowICSEvents = value; + save(); + }, + }) + ); + } + ); // Time Settings - createSectionHeader(container, translate("settings.appearance.timeSettings.header")); - createHelpText(container, translate("settings.appearance.timeSettings.description")); - - createDropdownSetting(container, { - name: translate("settings.appearance.timeSettings.timeSlotDuration.name"), - desc: translate("settings.appearance.timeSettings.timeSlotDuration.description"), - options: [ - { - value: "00:15:00", - label: translate( - "settings.appearance.timeSettings.timeSlotDuration.options.fifteenMinutes" - ), - }, - { - value: "00:30:00", - label: translate( - "settings.appearance.timeSettings.timeSlotDuration.options.thirtyMinutes" - ), - }, - { - value: "01:00:00", - label: translate( - "settings.appearance.timeSettings.timeSlotDuration.options.sixtyMinutes" - ), - }, - ], - getValue: () => plugin.settings.calendarViewSettings.slotDuration, - setValue: async (value: string) => { - plugin.settings.calendarViewSettings.slotDuration = value as any; - save(); + createSettingGroup( + container, + { + heading: translate("settings.appearance.timeSettings.header"), + description: translate("settings.appearance.timeSettings.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.appearance.timeSettings.timeSlotDuration.name"), + desc: translate("settings.appearance.timeSettings.timeSlotDuration.description"), + options: [ + { + value: "00:15:00", + label: translate("settings.appearance.timeSettings.timeSlotDuration.options.fifteenMinutes"), + }, + { + value: "00:30:00", + label: translate("settings.appearance.timeSettings.timeSlotDuration.options.thirtyMinutes"), + }, + { + value: "01:00:00", + label: translate("settings.appearance.timeSettings.timeSlotDuration.options.sixtyMinutes"), + }, + ], + getValue: () => plugin.settings.calendarViewSettings.slotDuration, + setValue: async (value: string) => { + plugin.settings.calendarViewSettings.slotDuration = value as any; + save(); + }, + }) + ); - createTextSetting(container, { - name: translate("settings.appearance.timeSettings.startTime.name"), - desc: translate("settings.appearance.timeSettings.startTime.description"), - placeholder: translate("settings.appearance.timeSettings.startTime.placeholder"), - debounceMs: 500, - getValue: () => { - const timeValue = plugin.settings.calendarViewSettings.slotMinTime; - // Validate and fallback to default if invalid - if (!timeValue || timeValue.length < 5 || !/^\d{2}:\d{2}:\d{2}$/.test(timeValue)) { - return "00:00"; // Default start time - } - return timeValue.slice(0, 5); // Remove seconds - }, - setValue: async (value: string) => { - // Validate time format (HH:MM) - if (!/^\d{2}:\d{2}$/.test(value)) { - new Notice("Invalid time format. Please use HH:MM format (e.g., 08:00)"); - return; - } + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.appearance.timeSettings.startTime.name"), + desc: translate("settings.appearance.timeSettings.startTime.description"), + placeholder: translate("settings.appearance.timeSettings.startTime.placeholder"), + debounceMs: 500, + getValue: () => { + const timeValue = plugin.settings.calendarViewSettings.slotMinTime; + if (!timeValue || timeValue.length < 5 || !/^\d{2}:\d{2}:\d{2}$/.test(timeValue)) { + return "00:00"; + } + return timeValue.slice(0, 5); + }, + setValue: async (value: string) => { + if (!/^\d{2}:\d{2}$/.test(value)) { + new Notice("Invalid time format. Please use HH:MM format (e.g., 08:00)"); + return; + } + const [hours, minutes] = value.split(":").map(Number); + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { + new Notice("Invalid time. Hours must be 00-23 and minutes must be 00-59"); + return; + } + plugin.settings.calendarViewSettings.slotMinTime = value + ":00"; + save(); + }, + }) + ); - // Validate time range (00:00 to 23:59) - const [hours, minutes] = value.split(":").map(Number); - if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { - new Notice("Invalid time. Hours must be 00-23 and minutes must be 00-59"); - return; - } + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.appearance.timeSettings.endTime.name"), + desc: translate("settings.appearance.timeSettings.endTime.description"), + placeholder: translate("settings.appearance.timeSettings.endTime.placeholder"), + debounceMs: 500, + getValue: () => { + const timeValue = plugin.settings.calendarViewSettings.slotMaxTime; + if (!timeValue || timeValue.length < 5 || !/^\d{2}:\d{2}:\d{2}$/.test(timeValue)) { + return "24:00"; + } + return timeValue.slice(0, 5); + }, + setValue: async (value: string) => { + if (!/^\d{2}:\d{2}$/.test(value)) { + new Notice("Invalid time format. Please use HH:MM format (e.g., 23:00)"); + return; + } + const [hours, minutes] = value.split(":").map(Number); + if (hours < 0 || hours > 24 || minutes < 0 || minutes > 59) { + new Notice("Invalid time. Hours must be 00-24 and minutes must be 00-59"); + return; + } + if (hours === 24 && minutes !== 0) { + new Notice("When hour is 24, minutes must be 00"); + return; + } + plugin.settings.calendarViewSettings.slotMaxTime = value + ":00"; + save(); + }, + }) + ); - plugin.settings.calendarViewSettings.slotMinTime = value + ":00"; - save(); - }, - }); + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.appearance.timeSettings.initialScrollTime.name"), + desc: translate("settings.appearance.timeSettings.initialScrollTime.description"), + placeholder: translate("settings.appearance.timeSettings.initialScrollTime.placeholder"), + debounceMs: 500, + getValue: () => { + const timeValue = plugin.settings.calendarViewSettings.scrollTime; + if (!timeValue || timeValue.length < 5 || !/^\d{2}:\d{2}:\d{2}$/.test(timeValue)) { + return "08:00"; + } + return timeValue.slice(0, 5); + }, + setValue: async (value: string) => { + if (!/^\d{2}:\d{2}$/.test(value)) { + new Notice("Invalid time format. Please use HH:MM format (e.g., 08:00)"); + return; + } + const [hours, minutes] = value.split(":").map(Number); + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { + new Notice("Invalid time. Hours must be 00-23 and minutes must be 00-59"); + return; + } + plugin.settings.calendarViewSettings.scrollTime = value + ":00"; + save(); + }, + }) + ); - createTextSetting(container, { - name: translate("settings.appearance.timeSettings.endTime.name"), - desc: translate("settings.appearance.timeSettings.endTime.description"), - placeholder: translate("settings.appearance.timeSettings.endTime.placeholder"), - debounceMs: 500, - getValue: () => { - const timeValue = plugin.settings.calendarViewSettings.slotMaxTime; - // Validate and fallback to default if invalid - if (!timeValue || timeValue.length < 5 || !/^\d{2}:\d{2}:\d{2}$/.test(timeValue)) { - return "24:00"; // Default end time - } - return timeValue.slice(0, 5); // Remove seconds - }, - setValue: async (value: string) => { - // Validate time format (HH:MM) - if (!/^\d{2}:\d{2}$/.test(value)) { - new Notice("Invalid time format. Please use HH:MM format (e.g., 23:00)"); - return; - } - - // Validate time range (00:00 to 24:00 for end time) - const [hours, minutes] = value.split(":").map(Number); - if (hours < 0 || hours > 24 || minutes < 0 || minutes > 59) { - new Notice("Invalid time. Hours must be 00-24 and minutes must be 00-59"); - return; - } - - // Special case: 24:XX is only valid as 24:00 - if (hours === 24 && minutes !== 0) { - new Notice("When hour is 24, minutes must be 00"); - return; - } - - plugin.settings.calendarViewSettings.slotMaxTime = value + ":00"; - save(); - }, - }); - - createTextSetting(container, { - name: translate("settings.appearance.timeSettings.initialScrollTime.name"), - desc: translate("settings.appearance.timeSettings.initialScrollTime.description"), - placeholder: translate("settings.appearance.timeSettings.initialScrollTime.placeholder"), - debounceMs: 500, - getValue: () => { - const timeValue = plugin.settings.calendarViewSettings.scrollTime; - // Validate and fallback to default if invalid - if (!timeValue || timeValue.length < 5 || !/^\d{2}:\d{2}:\d{2}$/.test(timeValue)) { - return "08:00"; // Default scroll time - } - return timeValue.slice(0, 5); // Remove seconds - }, - setValue: async (value: string) => { - // Validate time format (HH:MM) - if (!/^\d{2}:\d{2}$/.test(value)) { - new Notice("Invalid time format. Please use HH:MM format (e.g., 08:00)"); - return; - } - - // Validate time range (00:00 to 23:59) - const [hours, minutes] = value.split(":").map(Number); - if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { - new Notice("Invalid time. Hours must be 00-23 and minutes must be 00-59"); - return; - } - - plugin.settings.calendarViewSettings.scrollTime = value + ":00"; - save(); - }, - }); - - createNumberSetting(container, { - name: translate("settings.appearance.timeSettings.eventMinHeight.name"), - desc: translate("settings.appearance.timeSettings.eventMinHeight.description"), - placeholder: translate("settings.appearance.timeSettings.eventMinHeight.placeholder"), - min: 5, - max: 100, - debounceMs: 300, - getValue: () => plugin.settings.calendarViewSettings.eventMinHeight, - setValue: async (value: number) => { - plugin.settings.calendarViewSettings.eventMinHeight = value; - save(); - }, - }); + group.addSetting((setting) => + configureNumberSetting(setting, { + name: translate("settings.appearance.timeSettings.eventMinHeight.name"), + desc: translate("settings.appearance.timeSettings.eventMinHeight.description"), + placeholder: translate("settings.appearance.timeSettings.eventMinHeight.placeholder"), + min: 5, + max: 100, + debounceMs: 300, + getValue: () => plugin.settings.calendarViewSettings.eventMinHeight, + setValue: async (value: number) => { + plugin.settings.calendarViewSettings.eventMinHeight = value; + save(); + }, + }) + ); + } + ); // UI Elements Section - createSectionHeader(container, translate("settings.appearance.uiElements.header")); - createHelpText(container, translate("settings.appearance.uiElements.description")); - - createToggleSetting(container, { - name: translate("settings.appearance.uiElements.showTrackedTasksInStatusBar.name"), - desc: translate("settings.appearance.uiElements.showTrackedTasksInStatusBar.description"), - getValue: () => plugin.settings.showTrackedTasksInStatusBar, - setValue: async (value: boolean) => { - plugin.settings.showTrackedTasksInStatusBar = value; - save(); + createSettingGroup( + container, + { + heading: translate("settings.appearance.uiElements.header"), + description: translate("settings.appearance.uiElements.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.uiElements.showTrackedTasksInStatusBar.name"), + desc: translate("settings.appearance.uiElements.showTrackedTasksInStatusBar.description"), + getValue: () => plugin.settings.showTrackedTasksInStatusBar, + setValue: async (value: boolean) => { + plugin.settings.showTrackedTasksInStatusBar = value; + save(); + }, + }) + ); - createToggleSetting(container, { - name: translate("settings.appearance.uiElements.showRelationshipsWidget.name"), - desc: translate("settings.appearance.uiElements.showRelationshipsWidget.description"), - getValue: () => plugin.settings.showRelationships, - setValue: async (value: boolean) => { - plugin.settings.showRelationships = value; - save(); - // Re-render to show position setting - renderAppearanceTab(container, plugin, save); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.uiElements.showRelationshipsWidget.name"), + desc: translate("settings.appearance.uiElements.showRelationshipsWidget.description"), + getValue: () => plugin.settings.showRelationships, + setValue: async (value: boolean) => { + plugin.settings.showRelationships = value; + save(); + renderAppearanceTab(container, plugin, save); + }, + }) + ); - if (plugin.settings.showRelationships) { - createDropdownSetting(container, { - name: translate("settings.appearance.uiElements.relationshipsPosition.name"), - desc: translate("settings.appearance.uiElements.relationshipsPosition.description"), - options: [ - { - value: "top", - label: translate( - "settings.appearance.uiElements.relationshipsPosition.options.top" - ), - }, - { - value: "bottom", - label: translate( - "settings.appearance.uiElements.relationshipsPosition.options.bottom" - ), - }, - ], - getValue: () => plugin.settings.relationshipsPosition, - setValue: async (value: string) => { - plugin.settings.relationshipsPosition = value as "top" | "bottom"; - save(); - }, - }); - } + if (plugin.settings.showRelationships) { + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.appearance.uiElements.relationshipsPosition.name"), + desc: translate("settings.appearance.uiElements.relationshipsPosition.description"), + options: [ + { value: "top", label: translate("settings.appearance.uiElements.relationshipsPosition.options.top") }, + { value: "bottom", label: translate("settings.appearance.uiElements.relationshipsPosition.options.bottom") }, + ], + getValue: () => plugin.settings.relationshipsPosition, + setValue: async (value: string) => { + plugin.settings.relationshipsPosition = value as "top" | "bottom"; + save(); + }, + }) + ); + } - // Add task card in note setting - createToggleSetting(container, { - name: translate("settings.appearance.uiElements.showTaskCardInNote.name"), - desc: translate("settings.appearance.uiElements.showTaskCardInNote.description"), - getValue: () => plugin.settings.showTaskCardInNote, - setValue: async (value: boolean) => { - plugin.settings.showTaskCardInNote = value; - save(); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.uiElements.showTaskCardInNote.name"), + desc: translate("settings.appearance.uiElements.showTaskCardInNote.description"), + getValue: () => plugin.settings.showTaskCardInNote, + setValue: async (value: boolean) => { + plugin.settings.showTaskCardInNote = value; + save(); + }, + }) + ); - createToggleSetting(container, { - name: translate("settings.appearance.uiElements.showExpandableSubtasks.name"), - desc: translate("settings.appearance.uiElements.showExpandableSubtasks.description"), - getValue: () => plugin.settings.showExpandableSubtasks, - setValue: async (value: boolean) => { - plugin.settings.showExpandableSubtasks = value; - save(); - // Re-render to show chevron position setting - renderAppearanceTab(container, plugin, save); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.appearance.uiElements.showExpandableSubtasks.name"), + desc: translate("settings.appearance.uiElements.showExpandableSubtasks.description"), + getValue: () => plugin.settings.showExpandableSubtasks, + setValue: async (value: boolean) => { + plugin.settings.showExpandableSubtasks = value; + save(); + renderAppearanceTab(container, plugin, save); + }, + }) + ); - if (plugin.settings.showExpandableSubtasks) { - createDropdownSetting(container, { - name: translate("settings.appearance.uiElements.subtaskChevronPosition.name"), - desc: translate("settings.appearance.uiElements.subtaskChevronPosition.description"), - options: [ - { - value: "left", - label: translate( - "settings.appearance.uiElements.subtaskChevronPosition.options.left" - ), - }, - { - value: "right", - label: translate( - "settings.appearance.uiElements.subtaskChevronPosition.options.right" - ), - }, - ], - getValue: () => plugin.settings.subtaskChevronPosition, - setValue: async (value: string) => { - plugin.settings.subtaskChevronPosition = value as "left" | "right"; - save(); - }, - }); - } + if (plugin.settings.showExpandableSubtasks) { + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.appearance.uiElements.subtaskChevronPosition.name"), + desc: translate("settings.appearance.uiElements.subtaskChevronPosition.description"), + options: [ + { value: "left", label: translate("settings.appearance.uiElements.subtaskChevronPosition.options.left") }, + { value: "right", label: translate("settings.appearance.uiElements.subtaskChevronPosition.options.right") }, + ], + getValue: () => plugin.settings.subtaskChevronPosition, + setValue: async (value: string) => { + plugin.settings.subtaskChevronPosition = value as "left" | "right"; + save(); + }, + }) + ); + } - createDropdownSetting(container, { - name: translate("settings.appearance.uiElements.viewsButtonAlignment.name"), - desc: translate("settings.appearance.uiElements.viewsButtonAlignment.description"), - options: [ - { - value: "left", - label: translate( - "settings.appearance.uiElements.viewsButtonAlignment.options.left" - ), - }, - { - value: "right", - label: translate( - "settings.appearance.uiElements.viewsButtonAlignment.options.right" - ), - }, - ], - getValue: () => plugin.settings.viewsButtonAlignment, - setValue: async (value: string) => { - plugin.settings.viewsButtonAlignment = value as "left" | "right"; - save(); - }, - }); + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.appearance.uiElements.viewsButtonAlignment.name"), + desc: translate("settings.appearance.uiElements.viewsButtonAlignment.description"), + options: [ + { value: "left", label: translate("settings.appearance.uiElements.viewsButtonAlignment.options.left") }, + { value: "right", label: translate("settings.appearance.uiElements.viewsButtonAlignment.options.right") }, + ], + getValue: () => plugin.settings.viewsButtonAlignment, + setValue: async (value: string) => { + plugin.settings.viewsButtonAlignment = value as "left" | "right"; + save(); + }, + }) + ); + } + ); // Task Interaction Section - createSectionHeader(container, translate("settings.general.taskInteraction.header")); - createHelpText(container, translate("settings.general.taskInteraction.description")); - - createDropdownSetting(container, { - name: translate("settings.general.taskInteraction.singleClick.name"), - desc: translate("settings.general.taskInteraction.singleClick.description"), - options: [ - { value: "edit", label: translate("settings.general.taskInteraction.actions.edit") }, - { - value: "openNote", - label: translate("settings.general.taskInteraction.actions.openNote"), - }, - ], - getValue: () => plugin.settings.singleClickAction, - setValue: async (value: string) => { - plugin.settings.singleClickAction = value as "edit" | "openNote"; - save(); + createSettingGroup( + container, + { + heading: translate("settings.general.taskInteraction.header"), + description: translate("settings.general.taskInteraction.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.general.taskInteraction.singleClick.name"), + desc: translate("settings.general.taskInteraction.singleClick.description"), + options: [ + { value: "edit", label: translate("settings.general.taskInteraction.actions.edit") }, + { value: "openNote", label: translate("settings.general.taskInteraction.actions.openNote") }, + ], + getValue: () => plugin.settings.singleClickAction, + setValue: async (value: string) => { + plugin.settings.singleClickAction = value as "edit" | "openNote"; + save(); + }, + }) + ); - createDropdownSetting(container, { - name: translate("settings.general.taskInteraction.doubleClick.name"), - desc: translate("settings.general.taskInteraction.doubleClick.description"), - options: [ - { value: "edit", label: translate("settings.general.taskInteraction.actions.edit") }, - { - value: "openNote", - label: translate("settings.general.taskInteraction.actions.openNote"), - }, - { value: "none", label: translate("settings.general.taskInteraction.actions.none") }, - ], - getValue: () => plugin.settings.doubleClickAction, - setValue: async (value: string) => { - plugin.settings.doubleClickAction = value as "edit" | "openNote" | "none"; - save(); - }, - }); + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.general.taskInteraction.doubleClick.name"), + desc: translate("settings.general.taskInteraction.doubleClick.description"), + options: [ + { value: "edit", label: translate("settings.general.taskInteraction.actions.edit") }, + { value: "openNote", label: translate("settings.general.taskInteraction.actions.openNote") }, + { value: "none", label: translate("settings.general.taskInteraction.actions.none") }, + ], + getValue: () => plugin.settings.doubleClickAction, + setValue: async (value: string) => { + plugin.settings.doubleClickAction = value as "edit" | "openNote" | "none"; + save(); + }, + }) + ); + } + ); } diff --git a/src/settings/tabs/featuresTab.ts b/src/settings/tabs/featuresTab.ts index 1ec76738..f5f04f64 100644 --- a/src/settings/tabs/featuresTab.ts +++ b/src/settings/tabs/featuresTab.ts @@ -1,12 +1,11 @@ -import { Notice, Setting } from "obsidian"; +import { Notice } from "obsidian"; import TaskNotesPlugin from "../../main"; import { - createSectionHeader, - createTextSetting, - createToggleSetting, - createDropdownSetting, - createNumberSetting, - createHelpText, + createSettingGroup, + configureTextSetting, + configureToggleSetting, + configureDropdownSetting, + configureNumberSetting, } from "../components/settingHelpers"; import { showStorageLocationConfirmationModal } from "../../modals/StorageLocationConfirmationModal"; import { getAvailableLanguages } from "../../locales"; @@ -28,495 +27,557 @@ export function renderFeaturesTab( plugin.i18n.translate(key, params); // Inline Tasks Section - createSectionHeader(container, translate("settings.features.inlineTasks.header")); - createHelpText(container, translate("settings.features.inlineTasks.description")); + const availableProperties = getAvailableProperties(plugin); + const currentInlineProperties = plugin.settings.inlineVisibleProperties || [ + "status", "priority", "due", "scheduled", "recurrence", + ]; + const currentInlineLabels = getPropertyLabels(plugin, currentInlineProperties); - createToggleSetting(container, { - name: translate("settings.features.overlays.taskLinkToggle.name"), - desc: translate("settings.features.overlays.taskLinkToggle.description"), - getValue: () => plugin.settings.enableTaskLinkOverlay, - setValue: async (value: boolean) => { - plugin.settings.enableTaskLinkOverlay = value; - save(); - // Re-render to show/hide inline properties setting - renderFeaturesTab(container, plugin, save); + createSettingGroup( + container, + { + heading: translate("settings.features.inlineTasks.header"), + description: translate("settings.features.inlineTasks.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.overlays.taskLinkToggle.name"), + desc: translate("settings.features.overlays.taskLinkToggle.description"), + getValue: () => plugin.settings.enableTaskLinkOverlay, + setValue: async (value: boolean) => { + plugin.settings.enableTaskLinkOverlay = value; + save(); + renderFeaturesTab(container, plugin, save); + }, + }) + ); - // Inline task card visible properties (shown when task link overlay is enabled) - if (plugin.settings.enableTaskLinkOverlay) { - const availableProperties = getAvailableProperties(plugin); - - const currentProperties = plugin.settings.inlineVisibleProperties || [ - "status", - "priority", - "due", - "scheduled", - "recurrence", - ]; - - new Setting(container) - .setName("Inline Task Card Properties") - .setDesc( - "Select which properties to show in inline task cards (task links in editor). Fewer properties = more compact display." - ) - .addButton((button) => { - button.setButtonText("Configure").onClick(() => { - const modal = new PropertySelectorModal( - plugin.app, - availableProperties, - currentProperties, - async (selected) => { - plugin.settings.inlineVisibleProperties = selected; - save(); - new Notice("Inline task card properties updated"); - // Re-render to update display - renderFeaturesTab(container, plugin, save); - }, - "Select Inline Task Card Properties", - "Choose which properties to display in inline task cards. Selected properties will appear in the order shown below." - ); - modal.open(); + if (plugin.settings.enableTaskLinkOverlay) { + group.addSetting((setting) => { + setting + .setName("Inline Task Card Properties") + .setDesc("Select which properties to show in inline task cards.") + .addButton((button) => { + button.setButtonText("Configure").onClick(() => { + const modal = new PropertySelectorModal( + plugin.app, + availableProperties, + currentInlineProperties, + async (selected) => { + plugin.settings.inlineVisibleProperties = selected; + save(); + new Notice("Inline task card properties updated"); + renderFeaturesTab(container, plugin, save); + }, + "Select Inline Task Card Properties", + "Choose which properties to display in inline task cards." + ); + modal.open(); + }); + }); }); - }); - const currentLabels = getPropertyLabels(plugin, currentProperties); - createHelpText(container, `Currently showing: ${currentLabels.join(", ")}`); - } + group.addSetting((setting) => { + setting.setDesc(`Currently showing: ${currentInlineLabels.join(", ")}`); + setting.settingEl.addClass("settings-view__group-description"); + }); + } - createToggleSetting(container, { - name: translate("settings.features.instantConvert.toggle.name"), - desc: translate("settings.features.instantConvert.toggle.description"), - getValue: () => plugin.settings.enableInstantTaskConvert, - setValue: async (value: boolean) => { - plugin.settings.enableInstantTaskConvert = value; - save(); - // Re-render to show additional settings (and update General tab folder visibility) - renderFeaturesTab(container, plugin, save); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.instantConvert.toggle.name"), + desc: translate("settings.features.instantConvert.toggle.description"), + getValue: () => plugin.settings.enableInstantTaskConvert, + setValue: async (value: boolean) => { + plugin.settings.enableInstantTaskConvert = value; + save(); + renderFeaturesTab(container, plugin, save); + }, + }) + ); + } + ); // Natural Language Processing Section - createSectionHeader(container, translate("settings.features.nlp.header")); - createHelpText(container, translate("settings.features.nlp.description")); - - createToggleSetting(container, { - name: translate("settings.features.nlp.enable.name"), - desc: translate("settings.features.nlp.enable.description"), - getValue: () => plugin.settings.enableNaturalLanguageInput, - setValue: async (value: boolean) => { - plugin.settings.enableNaturalLanguageInput = value; - save(); - // Re-render to show NLP settings - renderFeaturesTab(container, plugin, save); + createSettingGroup( + container, + { + heading: translate("settings.features.nlp.header"), + description: translate("settings.features.nlp.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.nlp.enable.name"), + desc: translate("settings.features.nlp.enable.description"), + getValue: () => plugin.settings.enableNaturalLanguageInput, + setValue: async (value: boolean) => { + plugin.settings.enableNaturalLanguageInput = value; + save(); + renderFeaturesTab(container, plugin, save); + }, + }) + ); - if (plugin.settings.enableNaturalLanguageInput) { - createToggleSetting(container, { - name: translate("settings.features.nlp.defaultToScheduled.name"), - desc: translate("settings.features.nlp.defaultToScheduled.description"), - getValue: () => plugin.settings.nlpDefaultToScheduled, - setValue: async (value: boolean) => { - plugin.settings.nlpDefaultToScheduled = value; - save(); - }, - }); - - createDropdownSetting(container, { - name: translate("settings.features.nlp.language.name"), - desc: translate("settings.features.nlp.language.description"), - options: getAvailableLanguages(), - getValue: () => plugin.settings.nlpLanguage, - setValue: async (value: string) => { - plugin.settings.nlpLanguage = value; - save(); - }, - }); - - // Note: NLP triggers are now configured per-property in the Task Properties tab - } - - // Task Creation Section (Body Templates) - createSectionHeader(container, translate("settings.defaults.header.bodyTemplate")); - createHelpText(container, translate("settings.defaults.description.bodyTemplate")); - - createToggleSetting(container, { - name: translate("settings.defaults.bodyTemplate.useBodyTemplate.name"), - desc: translate("settings.defaults.bodyTemplate.useBodyTemplate.description"), - getValue: () => plugin.settings.taskCreationDefaults.useBodyTemplate, - setValue: async (value: boolean) => { - plugin.settings.taskCreationDefaults.useBodyTemplate = value; - save(); - // Re-render to show/hide template path - renderFeaturesTab(container, plugin, save); - }, - }); - - if (plugin.settings.taskCreationDefaults.useBodyTemplate) { - createTextSetting(container, { - name: translate("settings.defaults.bodyTemplate.bodyTemplateFile.name"), - desc: translate("settings.defaults.bodyTemplate.bodyTemplateFile.description"), - placeholder: translate("settings.defaults.bodyTemplate.bodyTemplateFile.placeholder"), - getValue: () => plugin.settings.taskCreationDefaults.bodyTemplate, - setValue: async (value: string) => { - plugin.settings.taskCreationDefaults.bodyTemplate = value; - save(); - }, - }); - - // Template Variables Help - const helpContainer = container.createDiv("tasknotes-settings__help-section"); - helpContainer.createEl("h4", { - text: translate("settings.defaults.bodyTemplate.variablesHeader"), - }); - const helpList = helpContainer.createEl("ul"); - helpList.createEl("li", { - text: translate("settings.defaults.bodyTemplate.variables.title"), - }); - helpList.createEl("li", { - text: translate("settings.defaults.bodyTemplate.variables.details"), - }); - helpList.createEl("li", { - text: translate("settings.defaults.bodyTemplate.variables.date"), - }); - helpList.createEl("li", { - text: translate("settings.defaults.bodyTemplate.variables.time"), - }); - helpList.createEl("li", { - text: translate("settings.defaults.bodyTemplate.variables.priority"), - }); - helpList.createEl("li", { - text: translate("settings.defaults.bodyTemplate.variables.status"), - }); - helpList.createEl("li", { - text: translate("settings.defaults.bodyTemplate.variables.contexts"), - }); - helpList.createEl("li", { - text: translate("settings.defaults.bodyTemplate.variables.tags"), - }); - helpList.createEl("li", { - text: translate("settings.defaults.bodyTemplate.variables.projects"), - }); - } - - // Instant Conversion Defaults - createToggleSetting(container, { - name: translate("settings.defaults.instantConversion.useDefaultsOnInstantConvert.name"), - desc: translate( - "settings.defaults.instantConversion.useDefaultsOnInstantConvert.description" - ), - getValue: () => plugin.settings.useDefaultsOnInstantConvert, - setValue: async (value: boolean) => { - plugin.settings.useDefaultsOnInstantConvert = value; - save(); - }, - }); - - // Pomodoro Timer Section - createSectionHeader(container, translate("settings.features.pomodoro.header")); - createHelpText(container, translate("settings.features.pomodoro.description")); - - // Work duration - createNumberSetting(container, { - name: translate("settings.features.pomodoro.workDuration.name"), - desc: translate("settings.features.pomodoro.workDuration.description"), - placeholder: "25", - min: 1, - max: 120, - getValue: () => plugin.settings.pomodoroWorkDuration, - setValue: async (value: number) => { - plugin.settings.pomodoroWorkDuration = value; - save(); - }, - }); - - // Short break duration - createNumberSetting(container, { - name: translate("settings.features.pomodoro.shortBreak.name"), - desc: translate("settings.features.pomodoro.shortBreak.description"), - placeholder: "5", - min: 1, - max: 60, - getValue: () => plugin.settings.pomodoroShortBreakDuration, - setValue: async (value: number) => { - plugin.settings.pomodoroShortBreakDuration = value; - save(); - }, - }); - - // Long break duration - createNumberSetting(container, { - name: translate("settings.features.pomodoro.longBreak.name"), - desc: translate("settings.features.pomodoro.longBreak.description"), - placeholder: "15", - min: 1, - max: 120, - getValue: () => plugin.settings.pomodoroLongBreakDuration, - setValue: async (value: number) => { - plugin.settings.pomodoroLongBreakDuration = value; - save(); - }, - }); - - // Long break interval - createNumberSetting(container, { - name: translate("settings.features.pomodoro.longBreakInterval.name"), - desc: translate("settings.features.pomodoro.longBreakInterval.description"), - placeholder: "4", - min: 1, - max: 10, - getValue: () => plugin.settings.pomodoroLongBreakInterval, - setValue: async (value: number) => { - plugin.settings.pomodoroLongBreakInterval = value; - save(); - }, - }); - - // Auto-start options - createToggleSetting(container, { - name: translate("settings.features.pomodoro.autoStartBreaks.name"), - desc: translate("settings.features.pomodoro.autoStartBreaks.description"), - getValue: () => plugin.settings.pomodoroAutoStartBreaks, - setValue: async (value: boolean) => { - plugin.settings.pomodoroAutoStartBreaks = value; - save(); - }, - }); - - createToggleSetting(container, { - name: translate("settings.features.pomodoro.autoStartWork.name"), - desc: translate("settings.features.pomodoro.autoStartWork.description"), - getValue: () => plugin.settings.pomodoroAutoStartWork, - setValue: async (value: boolean) => { - plugin.settings.pomodoroAutoStartWork = value; - save(); - }, - }); - - // Notification settings - createToggleSetting(container, { - name: translate("settings.features.pomodoro.notifications.name"), - desc: translate("settings.features.pomodoro.notifications.description"), - getValue: () => plugin.settings.pomodoroNotifications, - setValue: async (value: boolean) => { - plugin.settings.pomodoroNotifications = value; - save(); - }, - }); - - // Sound settings - createToggleSetting(container, { - name: translate("settings.features.pomodoroSound.enabledName"), - desc: translate("settings.features.pomodoroSound.enabledDesc"), - getValue: () => plugin.settings.pomodoroSoundEnabled, - setValue: async (value: boolean) => { - plugin.settings.pomodoroSoundEnabled = value; - save(); - // Re-render to show volume setting - renderFeaturesTab(container, plugin, save); - }, - }); - - if (plugin.settings.pomodoroSoundEnabled) { - createNumberSetting(container, { - name: translate("settings.features.pomodoroSound.volumeName"), - desc: translate("settings.features.pomodoroSound.volumeDesc"), - placeholder: "50", - min: 0, - max: 100, - getValue: () => plugin.settings.pomodoroSoundVolume, - setValue: async (value: number) => { - plugin.settings.pomodoroSoundVolume = value; - save(); - }, - }); - } - - // Storage location setting - createDropdownSetting(container, { - name: translate("settings.features.dataStorage.name"), - desc: translate("settings.features.dataStorage.description"), - options: [ - { value: "plugin", label: translate("settings.features.dataStorage.pluginData") }, - { value: "daily-notes", label: translate("settings.features.dataStorage.dailyNotes") }, - ], - getValue: () => plugin.settings.pomodoroStorageLocation, - setValue: async (value: string) => { - const newLocation = value as "plugin" | "daily-notes"; - if (newLocation !== plugin.settings.pomodoroStorageLocation) { - // Check if there's existing data to migrate - const data = await plugin.loadData(); - const hasExistingData = - data?.pomodoroHistory && - Array.isArray(data.pomodoroHistory) && - data.pomodoroHistory.length > 0; - - // Show confirmation modal for storage location change - const confirmed = await showStorageLocationConfirmationModal( - plugin, - hasExistingData + if (plugin.settings.enableNaturalLanguageInput) { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.nlp.defaultToScheduled.name"), + desc: translate("settings.features.nlp.defaultToScheduled.description"), + getValue: () => plugin.settings.nlpDefaultToScheduled, + setValue: async (value: boolean) => { + plugin.settings.nlpDefaultToScheduled = value; + save(); + }, + }) ); - if (confirmed) { - plugin.settings.pomodoroStorageLocation = newLocation; - save(); - new Notice( - translate("settings.features.dataStorage.notices.locationChanged", { - location: - newLocation === "plugin" - ? translate("settings.features.dataStorage.pluginData") - : translate("settings.features.dataStorage.dailyNotes"), - }) - ); - } else { - // Reset the dropdown to the current value - renderFeaturesTab(container, plugin, save); - } + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.features.nlp.language.name"), + desc: translate("settings.features.nlp.language.description"), + options: getAvailableLanguages(), + getValue: () => plugin.settings.nlpLanguage, + setValue: async (value: string) => { + plugin.settings.nlpLanguage = value; + save(); + }, + }) + ); } + } + ); + + // Task Creation Section (Body Templates) + createSettingGroup( + container, + { + heading: translate("settings.defaults.header.bodyTemplate"), + description: translate("settings.defaults.description.bodyTemplate"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.defaults.bodyTemplate.useBodyTemplate.name"), + desc: translate("settings.defaults.bodyTemplate.useBodyTemplate.description"), + getValue: () => plugin.settings.taskCreationDefaults.useBodyTemplate, + setValue: async (value: boolean) => { + plugin.settings.taskCreationDefaults.useBodyTemplate = value; + save(); + renderFeaturesTab(container, plugin, save); + }, + }) + ); + + if (plugin.settings.taskCreationDefaults.useBodyTemplate) { + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.defaults.bodyTemplate.bodyTemplateFile.name"), + desc: translate("settings.defaults.bodyTemplate.bodyTemplateFile.description"), + placeholder: translate("settings.defaults.bodyTemplate.bodyTemplateFile.placeholder"), + getValue: () => plugin.settings.taskCreationDefaults.bodyTemplate, + setValue: async (value: string) => { + plugin.settings.taskCreationDefaults.bodyTemplate = value; + save(); + }, + }) + ); + + // Template Variables Help (rendered as description text) + group.addSetting((setting) => { + const variables = [ + translate("settings.defaults.bodyTemplate.variables.title"), + translate("settings.defaults.bodyTemplate.variables.details"), + translate("settings.defaults.bodyTemplate.variables.date"), + translate("settings.defaults.bodyTemplate.variables.time"), + translate("settings.defaults.bodyTemplate.variables.priority"), + translate("settings.defaults.bodyTemplate.variables.status"), + translate("settings.defaults.bodyTemplate.variables.contexts"), + translate("settings.defaults.bodyTemplate.variables.tags"), + translate("settings.defaults.bodyTemplate.variables.projects"), + ]; + setting.setName(translate("settings.defaults.bodyTemplate.variablesHeader")); + setting.setDesc(variables.join(" • ")); + }); + } + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.defaults.instantConversion.useDefaultsOnInstantConvert.name"), + desc: translate("settings.defaults.instantConversion.useDefaultsOnInstantConvert.description"), + getValue: () => plugin.settings.useDefaultsOnInstantConvert, + setValue: async (value: boolean) => { + plugin.settings.useDefaultsOnInstantConvert = value; + save(); + }, + }) + ); + } + ); + + // Pomodoro Timer Section + createSettingGroup( + container, + { + heading: translate("settings.features.pomodoro.header"), + description: translate("settings.features.pomodoro.description"), + }, + (group) => { + group.addSetting((setting) => + configureNumberSetting(setting, { + name: translate("settings.features.pomodoro.workDuration.name"), + desc: translate("settings.features.pomodoro.workDuration.description"), + placeholder: "25", + min: 1, + max: 120, + getValue: () => plugin.settings.pomodoroWorkDuration, + setValue: async (value: number) => { + plugin.settings.pomodoroWorkDuration = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureNumberSetting(setting, { + name: translate("settings.features.pomodoro.shortBreak.name"), + desc: translate("settings.features.pomodoro.shortBreak.description"), + placeholder: "5", + min: 1, + max: 60, + getValue: () => plugin.settings.pomodoroShortBreakDuration, + setValue: async (value: number) => { + plugin.settings.pomodoroShortBreakDuration = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureNumberSetting(setting, { + name: translate("settings.features.pomodoro.longBreak.name"), + desc: translate("settings.features.pomodoro.longBreak.description"), + placeholder: "15", + min: 1, + max: 120, + getValue: () => plugin.settings.pomodoroLongBreakDuration, + setValue: async (value: number) => { + plugin.settings.pomodoroLongBreakDuration = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureNumberSetting(setting, { + name: translate("settings.features.pomodoro.longBreakInterval.name"), + desc: translate("settings.features.pomodoro.longBreakInterval.description"), + placeholder: "4", + min: 1, + max: 10, + getValue: () => plugin.settings.pomodoroLongBreakInterval, + setValue: async (value: number) => { + plugin.settings.pomodoroLongBreakInterval = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.pomodoro.autoStartBreaks.name"), + desc: translate("settings.features.pomodoro.autoStartBreaks.description"), + getValue: () => plugin.settings.pomodoroAutoStartBreaks, + setValue: async (value: boolean) => { + plugin.settings.pomodoroAutoStartBreaks = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.pomodoro.autoStartWork.name"), + desc: translate("settings.features.pomodoro.autoStartWork.description"), + getValue: () => plugin.settings.pomodoroAutoStartWork, + setValue: async (value: boolean) => { + plugin.settings.pomodoroAutoStartWork = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.pomodoro.notifications.name"), + desc: translate("settings.features.pomodoro.notifications.description"), + getValue: () => plugin.settings.pomodoroNotifications, + setValue: async (value: boolean) => { + plugin.settings.pomodoroNotifications = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.pomodoroSound.enabledName"), + desc: translate("settings.features.pomodoroSound.enabledDesc"), + getValue: () => plugin.settings.pomodoroSoundEnabled, + setValue: async (value: boolean) => { + plugin.settings.pomodoroSoundEnabled = value; + save(); + renderFeaturesTab(container, plugin, save); + }, + }) + ); + + if (plugin.settings.pomodoroSoundEnabled) { + group.addSetting((setting) => + configureNumberSetting(setting, { + name: translate("settings.features.pomodoroSound.volumeName"), + desc: translate("settings.features.pomodoroSound.volumeDesc"), + placeholder: "50", + min: 0, + max: 100, + getValue: () => plugin.settings.pomodoroSoundVolume, + setValue: async (value: number) => { + plugin.settings.pomodoroSoundVolume = value; + save(); + }, + }) + ); + } + + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.features.dataStorage.name"), + desc: translate("settings.features.dataStorage.description"), + options: [ + { value: "plugin", label: translate("settings.features.dataStorage.pluginData") }, + { value: "daily-notes", label: translate("settings.features.dataStorage.dailyNotes") }, + ], + getValue: () => plugin.settings.pomodoroStorageLocation, + setValue: async (value: string) => { + const newLocation = value as "plugin" | "daily-notes"; + if (newLocation !== plugin.settings.pomodoroStorageLocation) { + const data = await plugin.loadData(); + const hasExistingData = + data?.pomodoroHistory && + Array.isArray(data.pomodoroHistory) && + data.pomodoroHistory.length > 0; + + const confirmed = await showStorageLocationConfirmationModal(plugin, hasExistingData); + + if (confirmed) { + plugin.settings.pomodoroStorageLocation = newLocation; + save(); + new Notice( + translate("settings.features.dataStorage.notices.locationChanged", { + location: + newLocation === "plugin" + ? translate("settings.features.dataStorage.pluginData") + : translate("settings.features.dataStorage.dailyNotes"), + }) + ); + } else { + renderFeaturesTab(container, plugin, save); + } + } + }, + }) + ); + } + ); // Notifications Section - createSectionHeader(container, translate("settings.features.notifications.header")); - createHelpText(container, translate("settings.features.notifications.description")); - - createToggleSetting(container, { - name: translate("settings.features.notifications.enableName"), - desc: translate("settings.features.notifications.enableDesc"), - getValue: () => plugin.settings.enableNotifications, - setValue: async (value: boolean) => { - plugin.settings.enableNotifications = value; - save(); - // Re-render to show notification type setting - renderFeaturesTab(container, plugin, save); + createSettingGroup( + container, + { + heading: translate("settings.features.notifications.header"), + description: translate("settings.features.notifications.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.notifications.enableName"), + desc: translate("settings.features.notifications.enableDesc"), + getValue: () => plugin.settings.enableNotifications, + setValue: async (value: boolean) => { + plugin.settings.enableNotifications = value; + save(); + renderFeaturesTab(container, plugin, save); + }, + }) + ); - if (plugin.settings.enableNotifications) { - createDropdownSetting(container, { - name: translate("settings.features.notifications.typeName"), - desc: translate("settings.features.notifications.typeDesc"), - options: [ - { value: "in-app", label: translate("settings.features.notifications.inAppLabel") }, - { - value: "system", - label: translate("settings.features.notifications.systemLabel"), - }, - ], - getValue: () => plugin.settings.notificationType, - setValue: async (value: string) => { - plugin.settings.notificationType = value as "in-app" | "system"; - save(); - }, - }); - } + if (plugin.settings.enableNotifications) { + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.features.notifications.typeName"), + desc: translate("settings.features.notifications.typeDesc"), + options: [ + { value: "in-app", label: translate("settings.features.notifications.inAppLabel") }, + { value: "system", label: translate("settings.features.notifications.systemLabel") }, + ], + getValue: () => plugin.settings.notificationType, + setValue: async (value: string) => { + plugin.settings.notificationType = value as "in-app" | "system"; + save(); + }, + }) + ); + } + } + ); // Performance & Behavior Section - createSectionHeader(container, translate("settings.features.performance.header")); - createHelpText(container, translate("settings.features.performance.description")); - - createToggleSetting(container, { - name: translate("settings.features.overdue.hideCompletedName"), - desc: translate("settings.features.overdue.hideCompletedDesc"), - getValue: () => plugin.settings.hideCompletedFromOverdue, - setValue: async (value: boolean) => { - plugin.settings.hideCompletedFromOverdue = value; - save(); + createSettingGroup( + container, + { + heading: translate("settings.features.performance.header"), + description: translate("settings.features.performance.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.overdue.hideCompletedName"), + desc: translate("settings.features.overdue.hideCompletedDesc"), + getValue: () => plugin.settings.hideCompletedFromOverdue, + setValue: async (value: boolean) => { + plugin.settings.hideCompletedFromOverdue = value; + save(); + }, + }) + ); - createToggleSetting(container, { - name: translate("settings.features.indexing.disableName"), - desc: translate("settings.features.indexing.disableDesc"), - getValue: () => plugin.settings.disableNoteIndexing, - setValue: async (value: boolean) => { - plugin.settings.disableNoteIndexing = value; - save(); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.indexing.disableName"), + desc: translate("settings.features.indexing.disableDesc"), + getValue: () => plugin.settings.disableNoteIndexing, + setValue: async (value: boolean) => { + plugin.settings.disableNoteIndexing = value; + save(); + }, + }) + ); - // Suggestion debounce setting - if (plugin.settings.suggestionDebounceMs !== undefined) { - createNumberSetting(container, { - name: translate("settings.features.suggestions.debounceName"), - desc: translate("settings.features.suggestions.debounceDesc"), - placeholder: "300", - min: 0, - max: 2000, - getValue: () => plugin.settings.suggestionDebounceMs || 0, - setValue: async (value: number) => { - plugin.settings.suggestionDebounceMs = value > 0 ? value : undefined; - save(); - }, - }); - } + if (plugin.settings.suggestionDebounceMs !== undefined) { + group.addSetting((setting) => + configureNumberSetting(setting, { + name: translate("settings.features.suggestions.debounceName"), + desc: translate("settings.features.suggestions.debounceDesc"), + placeholder: "300", + min: 0, + max: 2000, + getValue: () => plugin.settings.suggestionDebounceMs || 0, + setValue: async (value: number) => { + plugin.settings.suggestionDebounceMs = value > 0 ? value : undefined; + save(); + }, + }) + ); + } + } + ); // Time Tracking Section - createSectionHeader(container, translate("settings.features.timeTrackingSection.header")); - createHelpText(container, translate("settings.features.timeTrackingSection.description")); - - createToggleSetting(container, { - name: translate("settings.features.timeTracking.autoStopName"), - desc: translate("settings.features.timeTracking.autoStopDesc"), - getValue: () => plugin.settings.autoStopTimeTrackingOnComplete, - setValue: async (value: boolean) => { - plugin.settings.autoStopTimeTrackingOnComplete = value; - save(); + createSettingGroup( + container, + { + heading: translate("settings.features.timeTrackingSection.header"), + description: translate("settings.features.timeTrackingSection.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.timeTracking.autoStopName"), + desc: translate("settings.features.timeTracking.autoStopDesc"), + getValue: () => plugin.settings.autoStopTimeTrackingOnComplete, + setValue: async (value: boolean) => { + plugin.settings.autoStopTimeTrackingOnComplete = value; + save(); + }, + }) + ); - createToggleSetting(container, { - name: translate("settings.features.timeTracking.stopNotificationName"), - desc: translate("settings.features.timeTracking.stopNotificationDesc"), - getValue: () => plugin.settings.autoStopTimeTrackingNotification, - setValue: async (value: boolean) => { - plugin.settings.autoStopTimeTrackingNotification = value; - save(); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.timeTracking.stopNotificationName"), + desc: translate("settings.features.timeTracking.stopNotificationDesc"), + getValue: () => plugin.settings.autoStopTimeTrackingNotification, + setValue: async (value: boolean) => { + plugin.settings.autoStopTimeTrackingNotification = value; + save(); + }, + }) + ); + } + ); // Recurring Tasks Section - createSectionHeader(container, translate("settings.features.recurringSection.header")); - createHelpText(container, translate("settings.features.recurringSection.description")); - - createToggleSetting(container, { - name: translate("settings.features.recurring.maintainOffsetName"), - desc: translate("settings.features.recurring.maintainOffsetDesc"), - getValue: () => plugin.settings.maintainDueDateOffsetInRecurring, - setValue: async (value: boolean) => { - plugin.settings.maintainDueDateOffsetInRecurring = value; - save(); + createSettingGroup( + container, + { + heading: translate("settings.features.recurringSection.header"), + description: translate("settings.features.recurringSection.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.recurring.maintainOffsetName"), + desc: translate("settings.features.recurring.maintainOffsetDesc"), + getValue: () => plugin.settings.maintainDueDateOffsetInRecurring, + setValue: async (value: boolean) => { + plugin.settings.maintainDueDateOffsetInRecurring = value; + save(); + }, + }) + ); + } + ); // Timeblocking Section - createSectionHeader(container, translate("settings.features.timeblocking.header")); - createHelpText(container, translate("settings.features.timeblocking.description")); - - createToggleSetting(container, { - name: translate("settings.features.timeblocking.enableName"), - desc: translate("settings.features.timeblocking.enableDesc"), - getValue: () => plugin.settings.calendarViewSettings.enableTimeblocking, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.enableTimeblocking = value; - save(); - // Re-render to show/hide timeblocks visibility setting - renderFeaturesTab(container, plugin, save); + createSettingGroup( + container, + { + heading: translate("settings.features.timeblocking.header"), + description: translate("settings.features.timeblocking.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.timeblocking.enableName"), + desc: translate("settings.features.timeblocking.enableDesc"), + getValue: () => plugin.settings.calendarViewSettings.enableTimeblocking, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.enableTimeblocking = value; + save(); + renderFeaturesTab(container, plugin, save); + }, + }) + ); - if (plugin.settings.calendarViewSettings.enableTimeblocking) { - createToggleSetting(container, { - name: translate("settings.features.timeblocking.showBlocksName"), - desc: translate("settings.features.timeblocking.showBlocksDesc"), - getValue: () => plugin.settings.calendarViewSettings.defaultShowTimeblocks, - setValue: async (value: boolean) => { - plugin.settings.calendarViewSettings.defaultShowTimeblocks = value; - save(); - }, - }); + if (plugin.settings.calendarViewSettings.enableTimeblocking) { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.features.timeblocking.showBlocksName"), + desc: translate("settings.features.timeblocking.showBlocksDesc"), + getValue: () => plugin.settings.calendarViewSettings.defaultShowTimeblocks, + setValue: async (value: boolean) => { + plugin.settings.calendarViewSettings.defaultShowTimeblocks = value; + save(); + }, + }) + ); - createHelpText(container, translate("settings.features.timeblocking.usage")); - } + group.addSetting((setting) => { + setting.setDesc(translate("settings.features.timeblocking.usage")); + setting.settingEl.addClass("settings-view__group-description"); + }); + } + } + ); } diff --git a/src/settings/tabs/generalTab.ts b/src/settings/tabs/generalTab.ts index 6dd31c79..7319579c 100644 --- a/src/settings/tabs/generalTab.ts +++ b/src/settings/tabs/generalTab.ts @@ -1,11 +1,10 @@ -import { Setting, Notice } from "obsidian"; +import { Notice } from "obsidian"; import TaskNotesPlugin from "../../main"; import { - createSectionHeader, - createTextSetting, - createToggleSetting, - createDropdownSetting, - createHelpText, + createSettingGroup, + configureTextSetting, + configureToggleSetting, + configureDropdownSetting, } from "../components/settingHelpers"; import { TranslationKey } from "../../i18n"; import { showConfirmationModal } from "../../modals/ConfirmationModal"; @@ -24,156 +23,167 @@ export function renderGeneralTab( plugin.i18n.translate(key, params); // Tasks Storage Section - createSectionHeader(container, translate("settings.general.taskStorage.header")); - createHelpText(container, translate("settings.general.taskStorage.description")); - - createTextSetting(container, { - name: translate("settings.general.taskStorage.defaultFolder.name"), - desc: translate("settings.general.taskStorage.defaultFolder.description"), - placeholder: "TaskNotes", - getValue: () => plugin.settings.tasksFolder, - setValue: async (value: string) => { - plugin.settings.tasksFolder = value; - save(); + createSettingGroup( + container, + { + heading: translate("settings.general.taskStorage.header"), + description: translate("settings.general.taskStorage.description"), }, - ariaLabel: "Default folder path for new tasks", - }); + (group) => { + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.general.taskStorage.defaultFolder.name"), + desc: translate("settings.general.taskStorage.defaultFolder.description"), + placeholder: "TaskNotes", + getValue: () => plugin.settings.tasksFolder, + setValue: async (value: string) => { + plugin.settings.tasksFolder = value; + save(); + }, + ariaLabel: "Default folder path for new tasks", + }) + ); - // Folder for converted inline tasks (only shown when instant convert is enabled) - if (plugin.settings.enableInstantTaskConvert) { - createTextSetting(container, { - name: translate("settings.features.instantConvert.folder.name"), - desc: translate("settings.features.instantConvert.folder.description"), - placeholder: "{{currentNotePath}}", - getValue: () => plugin.settings.inlineTaskConvertFolder, - setValue: async (value: string) => { - plugin.settings.inlineTaskConvertFolder = value; - save(); - }, - ariaLabel: "Folder for converted inline tasks", - }); - } + // Folder for converted inline tasks (only shown when instant convert is enabled) + if (plugin.settings.enableInstantTaskConvert) { + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.features.instantConvert.folder.name"), + desc: translate("settings.features.instantConvert.folder.description"), + placeholder: "{{currentNotePath}}", + getValue: () => plugin.settings.inlineTaskConvertFolder, + setValue: async (value: string) => { + plugin.settings.inlineTaskConvertFolder = value; + save(); + }, + ariaLabel: "Folder for converted inline tasks", + }) + ); + } - createToggleSetting(container, { - name: translate("settings.general.taskStorage.moveArchived.name"), - desc: translate("settings.general.taskStorage.moveArchived.description"), - getValue: () => plugin.settings.moveArchivedTasks, - setValue: async (value: boolean) => { - plugin.settings.moveArchivedTasks = value; - save(); - // Re-render to show/hide archive folder setting - renderGeneralTab(container, plugin, save); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.general.taskStorage.moveArchived.name"), + desc: translate("settings.general.taskStorage.moveArchived.description"), + getValue: () => plugin.settings.moveArchivedTasks, + setValue: async (value: boolean) => { + plugin.settings.moveArchivedTasks = value; + save(); + // Re-render to show/hide archive folder setting + renderGeneralTab(container, plugin, save); + }, + }) + ); - if (plugin.settings.moveArchivedTasks) { - createTextSetting(container, { - name: translate("settings.general.taskStorage.archiveFolder.name"), - desc: translate("settings.general.taskStorage.archiveFolder.description"), - placeholder: "TaskNotes/Archive", - getValue: () => plugin.settings.archiveFolder, - setValue: async (value: string) => { - plugin.settings.archiveFolder = value; - save(); - }, - ariaLabel: "Archive folder path", - }); - } + if (plugin.settings.moveArchivedTasks) { + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.general.taskStorage.archiveFolder.name"), + desc: translate("settings.general.taskStorage.archiveFolder.description"), + placeholder: "TaskNotes/Archive", + getValue: () => plugin.settings.archiveFolder, + setValue: async (value: string) => { + plugin.settings.archiveFolder = value; + save(); + }, + ariaLabel: "Archive folder path", + }) + ); + } + } + ); // Task Identification Section - createSectionHeader(container, translate("settings.general.taskIdentification.header")); - createHelpText(container, translate("settings.general.taskIdentification.description")); - - createDropdownSetting(container, { - name: translate("settings.general.taskIdentification.identifyBy.name"), - desc: translate("settings.general.taskIdentification.identifyBy.description"), - options: [ - { - value: "tag", - label: translate("settings.general.taskIdentification.identifyBy.options.tag"), - }, - { - value: "property", - label: translate("settings.general.taskIdentification.identifyBy.options.property"), - }, - ], - getValue: () => plugin.settings.taskIdentificationMethod, - setValue: async (value: string) => { - plugin.settings.taskIdentificationMethod = value as "tag" | "property"; - save(); - // Re-render to show/hide conditional fields - renderGeneralTab(container, plugin, save); + createSettingGroup( + container, + { + heading: translate("settings.general.taskIdentification.header"), + description: translate("settings.general.taskIdentification.description"), }, - ariaLabel: "Task identification method", - }); + (group) => { + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.general.taskIdentification.identifyBy.name"), + desc: translate("settings.general.taskIdentification.identifyBy.description"), + options: [ + { + value: "tag", + label: translate("settings.general.taskIdentification.identifyBy.options.tag"), + }, + { + value: "property", + label: translate("settings.general.taskIdentification.identifyBy.options.property"), + }, + ], + getValue: () => plugin.settings.taskIdentificationMethod, + setValue: async (value: string) => { + plugin.settings.taskIdentificationMethod = value as "tag" | "property"; + save(); + // Re-render to show/hide conditional fields + renderGeneralTab(container, plugin, save); + }, + ariaLabel: "Task identification method", + }) + ); - if (plugin.settings.taskIdentificationMethod === "tag") { - createTextSetting(container, { - name: translate("settings.general.taskIdentification.taskTag.name"), - desc: translate("settings.general.taskIdentification.taskTag.description"), - placeholder: "task", - getValue: () => plugin.settings.taskTag, - setValue: async (value: string) => { - plugin.settings.taskTag = value; - save(); - }, - ariaLabel: "Task identification tag", - }); + if (plugin.settings.taskIdentificationMethod === "tag") { + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.general.taskIdentification.taskTag.name"), + desc: translate("settings.general.taskIdentification.taskTag.description"), + placeholder: "task", + getValue: () => plugin.settings.taskTag, + setValue: async (value: string) => { + plugin.settings.taskTag = value; + save(); + }, + ariaLabel: "Task identification tag", + }) + ); - createToggleSetting(container, { - name: translate("settings.general.taskIdentification.hideIdentifyingTags.name"), - desc: translate("settings.general.taskIdentification.hideIdentifyingTags.description"), - getValue: () => plugin.settings.hideIdentifyingTagsInCards, - setValue: async (value: boolean) => { - plugin.settings.hideIdentifyingTagsInCards = value; - save(); - }, - }); - } else { - createTextSetting(container, { - name: translate("settings.general.taskIdentification.taskProperty.name"), - desc: translate("settings.general.taskIdentification.taskProperty.description"), - placeholder: "category", - getValue: () => plugin.settings.taskPropertyName, - setValue: async (value: string) => { - plugin.settings.taskPropertyName = value; - save(); - }, - }); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.general.taskIdentification.hideIdentifyingTags.name"), + desc: translate("settings.general.taskIdentification.hideIdentifyingTags.description"), + getValue: () => plugin.settings.hideIdentifyingTagsInCards, + setValue: async (value: boolean) => { + plugin.settings.hideIdentifyingTagsInCards = value; + save(); + }, + }) + ); + } else { + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.general.taskIdentification.taskProperty.name"), + desc: translate("settings.general.taskIdentification.taskProperty.description"), + placeholder: "category", + getValue: () => plugin.settings.taskPropertyName, + setValue: async (value: string) => { + plugin.settings.taskPropertyName = value; + save(); + }, + }) + ); - createTextSetting(container, { - name: translate("settings.general.taskIdentification.taskPropertyValue.name"), - desc: translate("settings.general.taskIdentification.taskPropertyValue.description"), - placeholder: "task", - getValue: () => plugin.settings.taskPropertyValue, - setValue: async (value: string) => { - plugin.settings.taskPropertyValue = value; - save(); - }, - }); - } + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.general.taskIdentification.taskPropertyValue.name"), + desc: translate("settings.general.taskIdentification.taskPropertyValue.description"), + placeholder: "task", + getValue: () => plugin.settings.taskPropertyValue, + setValue: async (value: string) => { + plugin.settings.taskPropertyValue = value; + save(); + }, + }) + ); + } + } + ); // Views & Base Files Section (moved above Folder Management) - createSectionHeader(container, translate("settings.integrations.basesIntegration.viewCommands.header")); - createHelpText( - container, - translate("settings.integrations.basesIntegration.viewCommands.description") - ); - createHelpText( - container, - translate("settings.integrations.basesIntegration.viewCommands.descriptionRegen") - ); - - // Documentation link - const docsLinkContainer = container.createDiv({ cls: "setting-item-description" }); - const docsLink = docsLinkContainer.createEl("a", { - text: translate("settings.integrations.basesIntegration.viewCommands.docsLink"), - href: translate("settings.integrations.basesIntegration.viewCommands.docsLinkUrl"), - }); - docsLink.setAttr("target", "_blank"); - docsLinkContainer.style.marginBottom = "1em"; - - // Command file mappings + // Command file mappings data const commandMappings = [ { id: 'open-calendar-view', @@ -207,145 +217,182 @@ export function renderGeneralTab( }, ]; - commandMappings.forEach(({ id, nameKey, defaultPath }) => { - const setting = new Setting(container); - const commandName = translate(`settings.integrations.basesIntegration.viewCommands.commands.${nameKey}` as any); - setting.setName(commandName); - setting.setDesc(translate("settings.integrations.basesIntegration.viewCommands.fileLabel", { - path: plugin.settings.commandFileMapping[id] - })); + createSettingGroup( + container, + { + heading: translate("settings.integrations.basesIntegration.viewCommands.header"), + description: translate("settings.integrations.basesIntegration.viewCommands.description"), + }, + (group) => { + // Additional description + group.addSetting((setting) => { + setting.setDesc(translate("settings.integrations.basesIntegration.viewCommands.descriptionRegen")); + setting.settingEl.addClass("settings-view__group-description"); + }); - // Text input for file path - setting.addText(text => { - text.setPlaceholder(defaultPath) - .setValue(plugin.settings.commandFileMapping[id]) - .onChange(async (value) => { - plugin.settings.commandFileMapping[id] = value; - await save(); - // Update description + // Documentation link + group.addSetting((setting) => { + const descEl = setting.descEl; + const docsLink = descEl.createEl("a", { + text: translate("settings.integrations.basesIntegration.viewCommands.docsLink"), + href: translate("settings.integrations.basesIntegration.viewCommands.docsLinkUrl"), + }); + docsLink.setAttr("target", "_blank"); + setting.settingEl.addClass("settings-view__group-description"); + }); + + // Command file mappings + commandMappings.forEach(({ id, nameKey, defaultPath }) => { + group.addSetting((setting) => { + const commandName = translate(`settings.integrations.basesIntegration.viewCommands.commands.${nameKey}` as any); + setting.setName(commandName); setting.setDesc(translate("settings.integrations.basesIntegration.viewCommands.fileLabel", { - path: value + path: plugin.settings.commandFileMapping[id] })); - }); - text.inputEl.style.width = '100%'; - return text; - }); - // Reset button - setting.addButton(button => { - button.setButtonText(translate("settings.integrations.basesIntegration.viewCommands.resetButton")) - .setTooltip(translate("settings.integrations.basesIntegration.viewCommands.resetTooltip")) - .onClick(async () => { - plugin.settings.commandFileMapping[id] = defaultPath; - await save(); - // Refresh the entire settings display - if (app.setting.activeTab) { - app.setting.openTabById(app.setting.activeTab.id); - } - }); - return button; - }); - }); - - // Auto-create default files toggle - new Setting(container) - .setName(translate("settings.integrations.basesIntegration.autoCreateDefaultFiles.name")) - .setDesc(translate("settings.integrations.basesIntegration.autoCreateDefaultFiles.description")) - .addToggle(toggle => { - toggle.setValue(plugin.settings.autoCreateDefaultBasesFiles) - .onChange(async (value) => { - plugin.settings.autoCreateDefaultBasesFiles = value; - await save(); - }); - return toggle; - }); - - // Create Default Files button - new Setting(container) - .setName(translate("settings.integrations.basesIntegration.createDefaultFiles.name")) - .setDesc(translate("settings.integrations.basesIntegration.createDefaultFiles.description")) - .addButton(button => { - button.setButtonText(translate("settings.integrations.basesIntegration.createDefaultFiles.buttonText")) - .setCta() - .onClick(async () => { - await plugin.createDefaultBasesFiles(); - }); - return button; - }); - - // Export All Saved Views button - new Setting(container) - .setName(translate("settings.integrations.basesIntegration.exportV3Views.name")) - .setDesc(translate("settings.integrations.basesIntegration.exportV3Views.description")) - .addButton(button => { - button.setButtonText(translate("settings.integrations.basesIntegration.exportV3Views.buttonText")) - .onClick(async () => { - try { - const savedViews = plugin.viewStateManager.getSavedViews(); - - if (savedViews.length === 0) { - new Notice(translate("settings.integrations.basesIntegration.exportV3Views.noViews")); - return; - } - - const basesContent = plugin.basesFilterConverter.convertAllSavedViewsToBasesFile(savedViews); - const fileName = 'all-saved-views.base'; - const filePath = `TaskNotes/Views/${fileName}`; - - // Create folder if needed - const folder = plugin.app.vault.getAbstractFileByPath('TaskNotes/Views'); - if (!folder) { - await plugin.app.vault.createFolder('TaskNotes/Views'); - } - - // Handle file overwrite confirmation - const existingFile = plugin.app.vault.getAbstractFileByPath(filePath); - if (existingFile) { - const confirmed = await showConfirmationModal(plugin.app, { - title: translate("settings.integrations.basesIntegration.exportV3Views.fileExists"), - message: translate("settings.integrations.basesIntegration.exportV3Views.confirmOverwrite", { fileName }), - isDestructive: false, + // Text input for file path + setting.addText(text => { + text.setPlaceholder(defaultPath) + .setValue(plugin.settings.commandFileMapping[id]) + .onChange(async (value) => { + plugin.settings.commandFileMapping[id] = value; + await save(); + // Update description + setting.setDesc(translate("settings.integrations.basesIntegration.viewCommands.fileLabel", { + path: value + })); }); - if (!confirmed) return; - await plugin.app.vault.modify(existingFile as any, basesContent); - } else { - await plugin.app.vault.create(filePath, basesContent); - } + text.inputEl.style.width = '100%'; + return text; + }); - new Notice(translate("settings.integrations.basesIntegration.exportV3Views.success", { - count: savedViews.length.toString(), - filePath - })); - await plugin.app.workspace.openLinkText(filePath, '', true); - } catch (error) { - console.error('Error exporting all views to Bases:', error); - new Notice(translate("settings.integrations.basesIntegration.exportV3Views.error", { - message: error.message - })); - } + // Reset button + setting.addButton(button => { + button.setButtonText(translate("settings.integrations.basesIntegration.viewCommands.resetButton")) + .setTooltip(translate("settings.integrations.basesIntegration.viewCommands.resetTooltip")) + .onClick(async () => { + plugin.settings.commandFileMapping[id] = defaultPath; + await save(); + // Refresh the entire settings display + if (app.setting.activeTab) { + app.setting.openTabById(app.setting.activeTab.id); + } + }); + return button; + }); }); - return button; - }); + }); + + // Auto-create default files toggle + group.addSetting((setting) => { + setting + .setName(translate("settings.integrations.basesIntegration.autoCreateDefaultFiles.name")) + .setDesc(translate("settings.integrations.basesIntegration.autoCreateDefaultFiles.description")) + .addToggle(toggle => { + toggle.setValue(plugin.settings.autoCreateDefaultBasesFiles) + .onChange(async (value) => { + plugin.settings.autoCreateDefaultBasesFiles = value; + await save(); + }); + return toggle; + }); + }); + + // Create Default Files button + group.addSetting((setting) => { + setting + .setName(translate("settings.integrations.basesIntegration.createDefaultFiles.name")) + .setDesc(translate("settings.integrations.basesIntegration.createDefaultFiles.description")) + .addButton(button => { + button.setButtonText(translate("settings.integrations.basesIntegration.createDefaultFiles.buttonText")) + .setCta() + .onClick(async () => { + await plugin.createDefaultBasesFiles(); + }); + return button; + }); + }); + + // Export All Saved Views button + group.addSetting((setting) => { + setting + .setName(translate("settings.integrations.basesIntegration.exportV3Views.name")) + .setDesc(translate("settings.integrations.basesIntegration.exportV3Views.description")) + .addButton(button => { + button.setButtonText(translate("settings.integrations.basesIntegration.exportV3Views.buttonText")) + .onClick(async () => { + try { + const savedViews = plugin.viewStateManager.getSavedViews(); + + if (savedViews.length === 0) { + new Notice(translate("settings.integrations.basesIntegration.exportV3Views.noViews")); + return; + } + + const basesContent = plugin.basesFilterConverter.convertAllSavedViewsToBasesFile(savedViews); + const fileName = 'all-saved-views.base'; + const filePath = `TaskNotes/Views/${fileName}`; + + // Create folder if needed + const folder = plugin.app.vault.getAbstractFileByPath('TaskNotes/Views'); + if (!folder) { + await plugin.app.vault.createFolder('TaskNotes/Views'); + } + + // Handle file overwrite confirmation + const existingFile = plugin.app.vault.getAbstractFileByPath(filePath); + if (existingFile) { + const confirmed = await showConfirmationModal(plugin.app, { + title: translate("settings.integrations.basesIntegration.exportV3Views.fileExists"), + message: translate("settings.integrations.basesIntegration.exportV3Views.confirmOverwrite", { fileName }), + isDestructive: false, + }); + if (!confirmed) return; + await plugin.app.vault.modify(existingFile as any, basesContent); + } else { + await plugin.app.vault.create(filePath, basesContent); + } + + new Notice(translate("settings.integrations.basesIntegration.exportV3Views.success", { + count: savedViews.length.toString(), + filePath + })); + await plugin.app.workspace.openLinkText(filePath, '', true); + } catch (error) { + console.error('Error exporting all views to Bases:', error); + new Notice(translate("settings.integrations.basesIntegration.exportV3Views.error", { + message: error.message + })); + } + }); + return button; + }); + }); + } + ); // Folder Management Section - createSectionHeader(container, translate("settings.general.folderManagement.header")); - - createTextSetting(container, { - name: translate("settings.general.folderManagement.excludedFolders.name"), - desc: translate("settings.general.folderManagement.excludedFolders.description"), - placeholder: "Templates, Archive", - getValue: () => plugin.settings.excludedFolders, - setValue: async (value: string) => { - plugin.settings.excludedFolders = value; - save(); - }, - ariaLabel: "Excluded folder paths", - }); + createSettingGroup( + container, + { heading: translate("settings.general.folderManagement.header") }, + (group) => { + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.general.folderManagement.excludedFolders.name"), + desc: translate("settings.general.folderManagement.excludedFolders.description"), + placeholder: "Templates, Archive", + getValue: () => plugin.settings.excludedFolders, + setValue: async (value: string) => { + plugin.settings.excludedFolders = value; + save(); + }, + ariaLabel: "Excluded folder paths", + }) + ); + } + ); // UI Language Section - createSectionHeader(container, translate("settings.features.uiLanguage.header")); - createHelpText(container, translate("settings.features.uiLanguage.description")); - const uiLanguageOptions = (() => { const options: Array<{ value: string; label: string }> = [ { value: "system", label: translate("common.systemDefault") }, @@ -358,59 +405,88 @@ export function renderGeneralTab( return options; })(); - createDropdownSetting(container, { - name: translate("settings.features.uiLanguage.dropdown.name"), - desc: translate("settings.features.uiLanguage.dropdown.description"), - options: uiLanguageOptions, - getValue: () => plugin.settings.uiLanguage ?? "system", - setValue: async (value: string) => { - plugin.settings.uiLanguage = value; - plugin.i18n.setLocale(value); - save(); - renderGeneralTab(container, plugin, save); + createSettingGroup( + container, + { + heading: translate("settings.features.uiLanguage.header"), + description: translate("settings.features.uiLanguage.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.features.uiLanguage.dropdown.name"), + desc: translate("settings.features.uiLanguage.dropdown.description"), + options: uiLanguageOptions, + getValue: () => plugin.settings.uiLanguage ?? "system", + setValue: async (value: string) => { + plugin.settings.uiLanguage = value; + plugin.i18n.setLocale(value); + save(); + renderGeneralTab(container, plugin, save); + }, + }) + ); + } + ); // Frontmatter Section - only show if user has markdown links enabled globally const useMarkdownLinks = plugin.app.vault.getConfig('useMarkdownLinks'); if (useMarkdownLinks) { - createSectionHeader(container, translate("settings.general.frontmatter.header")); - createHelpText(container, translate("settings.general.frontmatter.description")); - - createToggleSetting(container, { - name: translate("settings.general.frontmatter.useMarkdownLinks.name"), - desc: translate("settings.general.frontmatter.useMarkdownLinks.description"), - getValue: () => plugin.settings.useFrontmatterMarkdownLinks, - setValue: async (value: boolean) => { - plugin.settings.useFrontmatterMarkdownLinks = value; - save(); + createSettingGroup( + container, + { + heading: translate("settings.general.frontmatter.header"), + description: translate("settings.general.frontmatter.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.general.frontmatter.useMarkdownLinks.name"), + desc: translate("settings.general.frontmatter.useMarkdownLinks.description"), + getValue: () => plugin.settings.useFrontmatterMarkdownLinks, + setValue: async (value: boolean) => { + plugin.settings.useFrontmatterMarkdownLinks = value; + save(); + }, + }) + ); + } + ); } // Release Notes Section - createSectionHeader(container, translate("settings.general.releaseNotes.header")); - createHelpText(container, translate("settings.general.releaseNotes.description", { version: plugin.manifest.version })); - - createToggleSetting(container, { - name: translate("settings.general.releaseNotes.showOnUpdate.name"), - desc: translate("settings.general.releaseNotes.showOnUpdate.description"), - getValue: () => plugin.settings.showReleaseNotesOnUpdate ?? true, - setValue: async (value: boolean) => { - plugin.settings.showReleaseNotesOnUpdate = value; - save(); + createSettingGroup( + container, + { + heading: translate("settings.general.releaseNotes.header"), + description: translate("settings.general.releaseNotes.description", { version: plugin.manifest.version }), }, - }); - - new Setting(container) - .setName(translate("settings.general.releaseNotes.viewButton.name")) - .setDesc(translate("settings.general.releaseNotes.viewButton.description")) - .addButton((button) => - button - .setButtonText(translate("settings.general.releaseNotes.viewButton.buttonText")) - .setCta() - .onClick(async () => { - await plugin.activateReleaseNotesView(); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.general.releaseNotes.showOnUpdate.name"), + desc: translate("settings.general.releaseNotes.showOnUpdate.description"), + getValue: () => plugin.settings.showReleaseNotesOnUpdate ?? true, + setValue: async (value: boolean) => { + plugin.settings.showReleaseNotesOnUpdate = value; + save(); + }, }) - ); + ); + + group.addSetting((setting) => { + setting + .setName(translate("settings.general.releaseNotes.viewButton.name")) + .setDesc(translate("settings.general.releaseNotes.viewButton.description")) + .addButton((button) => + button + .setButtonText(translate("settings.general.releaseNotes.viewButton.buttonText")) + .setCta() + .onClick(async () => { + await plugin.activateReleaseNotesView(); + }) + ); + }); + } + ); } diff --git a/src/settings/tabs/integrationsTab.ts b/src/settings/tabs/integrationsTab.ts index 77962e2b..536293ab 100644 --- a/src/settings/tabs/integrationsTab.ts +++ b/src/settings/tabs/integrationsTab.ts @@ -4,13 +4,13 @@ import { WebhookConfig } from "../../types"; import { TranslationKey } from "../../i18n"; import { loadAPIEndpoints } from "../../api/loadAPIEndpoints"; import { - createSectionHeader, - createTextSetting, - createToggleSetting, - createDropdownSetting, - createNumberSetting, + createSettingGroup, + configureTextSetting, + configureToggleSetting, + configureDropdownSetting, + configureNumberSetting, + configureButtonSetting, createHelpText, - createButtonSetting, } from "../components/settingHelpers"; import { showConfirmationModal } from "../../modals/ConfirmationModal"; import { @@ -84,10 +84,15 @@ export function renderIntegrationsTab( plugin.i18n.translate(key, params); // OAuth Calendar Integration Section - createSectionHeader(container, "OAuth Calendar Integration"); - createHelpText( + createSettingGroup( container, - "Connect your Google Calendar or Microsoft Outlook to sync events directly into TaskNotes." + { + heading: "OAuth Calendar Integration", + description: "Connect your Google Calendar or Microsoft Outlook to sync events directly into TaskNotes.", + }, + () => { + // Settings added via card components below + } ); // TaskNotes License Card (appears before calendar cards) @@ -777,206 +782,275 @@ export function renderIntegrationsTab( renderMicrosoftCalendarCard(); // Calendar Subscriptions Section (ICS) - createSectionHeader(container, translate("settings.integrations.calendarSubscriptions.header")); - createHelpText(container, translate("settings.integrations.calendarSubscriptions.description")); - - // Default settings for ICS integration - createTextSetting(container, { - name: translate("settings.integrations.calendarSubscriptions.defaultNoteTemplate.name"), - desc: translate( - "settings.integrations.calendarSubscriptions.defaultNoteTemplate.description" - ), - placeholder: translate( - "settings.integrations.calendarSubscriptions.defaultNoteTemplate.placeholder" - ), - getValue: () => plugin.settings.icsIntegration.defaultNoteTemplate, - setValue: async (value: string) => { - plugin.settings.icsIntegration.defaultNoteTemplate = value; - save(); + createSettingGroup( + container, + { + heading: translate("settings.integrations.calendarSubscriptions.header"), + description: translate("settings.integrations.calendarSubscriptions.description"), }, - }); + (group) => { + // Default settings for ICS integration + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.integrations.calendarSubscriptions.defaultNoteTemplate.name"), + desc: translate( + "settings.integrations.calendarSubscriptions.defaultNoteTemplate.description" + ), + placeholder: translate( + "settings.integrations.calendarSubscriptions.defaultNoteTemplate.placeholder" + ), + getValue: () => plugin.settings.icsIntegration.defaultNoteTemplate, + setValue: async (value: string) => { + plugin.settings.icsIntegration.defaultNoteTemplate = value; + save(); + }, + }) + ); - createTextSetting(container, { - name: translate("settings.integrations.calendarSubscriptions.defaultNoteFolder.name"), - desc: translate( - "settings.integrations.calendarSubscriptions.defaultNoteFolder.description" - ), - placeholder: translate( - "settings.integrations.calendarSubscriptions.defaultNoteFolder.placeholder" - ), - getValue: () => plugin.settings.icsIntegration.defaultNoteFolder, - setValue: async (value: string) => { - plugin.settings.icsIntegration.defaultNoteFolder = value; - save(); - }, - }); + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.integrations.calendarSubscriptions.defaultNoteFolder.name"), + desc: translate( + "settings.integrations.calendarSubscriptions.defaultNoteFolder.description" + ), + placeholder: translate( + "settings.integrations.calendarSubscriptions.defaultNoteFolder.placeholder" + ), + getValue: () => plugin.settings.icsIntegration.defaultNoteFolder, + setValue: async (value: string) => { + plugin.settings.icsIntegration.defaultNoteFolder = value; + save(); + }, + }) + ); - createDropdownSetting(container, { - name: translate("settings.integrations.calendarSubscriptions.filenameFormat.name"), - desc: translate("settings.integrations.calendarSubscriptions.filenameFormat.description"), - options: [ - { - value: "title", - label: translate( - "settings.integrations.calendarSubscriptions.filenameFormat.options.title" - ), - }, - { - value: "zettel", - label: translate( - "settings.integrations.calendarSubscriptions.filenameFormat.options.zettel" - ), - }, - { - value: "timestamp", - label: translate( - "settings.integrations.calendarSubscriptions.filenameFormat.options.timestamp" - ), - }, - { - value: "custom", - label: translate( - "settings.integrations.calendarSubscriptions.filenameFormat.options.custom" - ), - }, - ], - getValue: () => plugin.settings.icsIntegration.icsNoteFilenameFormat, - setValue: async (value: string) => { - plugin.settings.icsIntegration.icsNoteFilenameFormat = value as any; - save(); - // Re-render to show custom template field if needed - renderIntegrationsTab(container, plugin, save); - }, - }); + group.addSetting((setting) => + configureDropdownSetting(setting, { + name: translate("settings.integrations.calendarSubscriptions.filenameFormat.name"), + desc: translate("settings.integrations.calendarSubscriptions.filenameFormat.description"), + options: [ + { + value: "title", + label: translate( + "settings.integrations.calendarSubscriptions.filenameFormat.options.title" + ), + }, + { + value: "zettel", + label: translate( + "settings.integrations.calendarSubscriptions.filenameFormat.options.zettel" + ), + }, + { + value: "timestamp", + label: translate( + "settings.integrations.calendarSubscriptions.filenameFormat.options.timestamp" + ), + }, + { + value: "custom", + label: translate( + "settings.integrations.calendarSubscriptions.filenameFormat.options.custom" + ), + }, + ], + getValue: () => plugin.settings.icsIntegration.icsNoteFilenameFormat, + setValue: async (value: string) => { + plugin.settings.icsIntegration.icsNoteFilenameFormat = value as any; + save(); + // Re-render to show custom template field if needed + renderIntegrationsTab(container, plugin, save); + }, + }) + ); - if (plugin.settings.icsIntegration.icsNoteFilenameFormat === "custom") { - createTextSetting(container, { - name: translate("settings.integrations.calendarSubscriptions.customTemplate.name"), - desc: translate( - "settings.integrations.calendarSubscriptions.customTemplate.description" - ), - placeholder: translate( - "settings.integrations.calendarSubscriptions.customTemplate.placeholder" - ), - getValue: () => plugin.settings.icsIntegration.customICSNoteFilenameTemplate, - setValue: async (value: string) => { - plugin.settings.icsIntegration.customICSNoteFilenameTemplate = value; - save(); - }, - }); - } + if (plugin.settings.icsIntegration.icsNoteFilenameFormat === "custom") { + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.integrations.calendarSubscriptions.customTemplate.name"), + desc: translate( + "settings.integrations.calendarSubscriptions.customTemplate.description" + ), + placeholder: translate( + "settings.integrations.calendarSubscriptions.customTemplate.placeholder" + ), + getValue: () => plugin.settings.icsIntegration.customICSNoteFilenameTemplate, + setValue: async (value: string) => { + plugin.settings.icsIntegration.customICSNoteFilenameTemplate = value; + save(); + }, + }) + ); + } + } + ); - // ICS Subscriptions List - Add proper section header - createSectionHeader(container, translate("settings.integrations.subscriptionsList.header")); + // ICS Subscriptions List const icsContainer = container.createDiv("ics-subscriptions-container"); renderICSSubscriptionsList(icsContainer, plugin, save); - // Add subscription button - createButtonSetting(container, { - name: translate("settings.integrations.subscriptionsList.addSubscription.name"), - desc: translate("settings.integrations.subscriptionsList.addSubscription.description"), - buttonText: translate("settings.integrations.subscriptionsList.addSubscription.buttonText"), - onClick: async () => { - // Create a new subscription with temporary values - const newSubscription = { - name: translate("settings.integrations.subscriptionsList.newCalendarName"), - url: "", - color: "#6366f1", - enabled: false, // Start disabled until user fills in details - type: "remote" as const, - refreshInterval: 60, - }; - - if (!plugin.icsSubscriptionService) { - new Notice( - translate("settings.integrations.subscriptionsList.notices.serviceUnavailable") - ); - return; - } - - try { - await plugin.icsSubscriptionService.addSubscription(newSubscription); - new Notice(translate("settings.integrations.subscriptionsList.notices.addSuccess")); - // Re-render to show the new subscription card - renderICSSubscriptionsList(icsContainer, plugin, save); - } catch (error) { - console.error("Error adding subscription:", error); - new Notice(translate("settings.integrations.subscriptionsList.notices.addFailure")); - } + createSettingGroup( + container, + { + heading: translate("settings.integrations.subscriptionsList.header"), }, - }); + (group) => { + // Add subscription button + group.addSetting((setting) => + configureButtonSetting(setting, { + name: translate("settings.integrations.subscriptionsList.addSubscription.name"), + desc: translate("settings.integrations.subscriptionsList.addSubscription.description"), + buttonText: translate("settings.integrations.subscriptionsList.addSubscription.buttonText"), + onClick: async () => { + // Create a new subscription with temporary values + const newSubscription = { + name: translate("settings.integrations.subscriptionsList.newCalendarName"), + url: "", + color: "#6366f1", + enabled: false, // Start disabled until user fills in details + type: "remote" as const, + refreshInterval: 60, + }; - // Refresh all subscriptions button - createButtonSetting(container, { - name: translate("settings.integrations.subscriptionsList.refreshAll.name"), - desc: translate("settings.integrations.subscriptionsList.refreshAll.description"), - buttonText: translate("settings.integrations.subscriptionsList.refreshAll.buttonText"), - onClick: async () => { - if (plugin.icsSubscriptionService) { - try { - await plugin.icsSubscriptionService.refreshAllSubscriptions(); - new Notice( - translate("settings.integrations.subscriptionsList.notices.refreshSuccess") - ); - } catch (error) { - console.error("Error refreshing subscriptions:", error); - new Notice( - translate("settings.integrations.subscriptionsList.notices.refreshFailure") - ); - } - } - }, - }); + if (!plugin.icsSubscriptionService) { + new Notice( + translate("settings.integrations.subscriptionsList.notices.serviceUnavailable") + ); + return; + } + + try { + await plugin.icsSubscriptionService.addSubscription(newSubscription); + new Notice(translate("settings.integrations.subscriptionsList.notices.addSuccess")); + // Re-render to show the new subscription card + renderICSSubscriptionsList(icsContainer, plugin, save); + } catch (error) { + console.error("Error adding subscription:", error); + new Notice(translate("settings.integrations.subscriptionsList.notices.addFailure")); + } + }, + }) + ); + + // Refresh all subscriptions button + group.addSetting((setting) => + configureButtonSetting(setting, { + name: translate("settings.integrations.subscriptionsList.refreshAll.name"), + desc: translate("settings.integrations.subscriptionsList.refreshAll.description"), + buttonText: translate("settings.integrations.subscriptionsList.refreshAll.buttonText"), + onClick: async () => { + if (plugin.icsSubscriptionService) { + try { + await plugin.icsSubscriptionService.refreshAllSubscriptions(); + new Notice( + translate("settings.integrations.subscriptionsList.notices.refreshSuccess") + ); + } catch (error) { + console.error("Error refreshing subscriptions:", error); + new Notice( + translate("settings.integrations.subscriptionsList.notices.refreshFailure") + ); + } + } + }, + }) + ); + } + ); // Automatic ICS Export Section - createSectionHeader(container, translate("settings.integrations.autoExport.header")); - createHelpText(container, translate("settings.integrations.autoExport.description")); - - createToggleSetting(container, { - name: translate("settings.integrations.autoExport.enable.name"), - desc: translate("settings.integrations.autoExport.enable.description"), - getValue: () => plugin.settings.icsIntegration.enableAutoExport, - setValue: async (value: boolean) => { - plugin.settings.icsIntegration.enableAutoExport = value; - save(); - new Notice(translate("settings.integrations.autoExport.notices.reloadRequired")); - // Re-render to show/hide export settings - renderIntegrationsTab(container, plugin, save); + createSettingGroup( + container, + { + heading: translate("settings.integrations.autoExport.header"), + description: translate("settings.integrations.autoExport.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.integrations.autoExport.enable.name"), + desc: translate("settings.integrations.autoExport.enable.description"), + getValue: () => plugin.settings.icsIntegration.enableAutoExport, + setValue: async (value: boolean) => { + plugin.settings.icsIntegration.enableAutoExport = value; + save(); + new Notice(translate("settings.integrations.autoExport.notices.reloadRequired")); + // Re-render to show/hide export settings + renderIntegrationsTab(container, plugin, save); + }, + }) + ); + if (plugin.settings.icsIntegration.enableAutoExport) { + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.integrations.autoExport.filePath.name"), + desc: translate("settings.integrations.autoExport.filePath.description"), + placeholder: translate("settings.integrations.autoExport.filePath.placeholder"), + getValue: () => plugin.settings.icsIntegration.autoExportPath, + setValue: async (value: string) => { + plugin.settings.icsIntegration.autoExportPath = value || "tasknotes-calendar.ics"; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureNumberSetting(setting, { + name: translate("settings.integrations.autoExport.interval.name"), + desc: translate("settings.integrations.autoExport.interval.description"), + placeholder: translate("settings.integrations.autoExport.interval.placeholder"), + min: 5, + max: 1440, // 24 hours max + getValue: () => plugin.settings.icsIntegration.autoExportInterval, + setValue: async (value: number) => { + plugin.settings.icsIntegration.autoExportInterval = Math.max(5, value || 60); + save(); + // Restart the auto export service with new interval + if (plugin.autoExportService) { + plugin.autoExportService.updateInterval( + plugin.settings.icsIntegration.autoExportInterval + ); + } + }, + }) + ); + + // Manual export trigger button + group.addSetting((setting) => + configureButtonSetting(setting, { + name: translate("settings.integrations.autoExport.exportNow.name"), + desc: translate("settings.integrations.autoExport.exportNow.description"), + buttonText: translate("settings.integrations.autoExport.exportNow.buttonText"), + onClick: async () => { + if (plugin.autoExportService) { + try { + await plugin.autoExportService.exportNow(); + new Notice( + translate("settings.integrations.autoExport.notices.exportSuccess") + ); + // Re-render to update status + renderIntegrationsTab(container, plugin, save); + } catch (error) { + console.error("Manual export failed:", error); + new Notice( + translate("settings.integrations.autoExport.notices.exportFailure") + ); + } + } else { + new Notice( + translate("settings.integrations.autoExport.notices.serviceUnavailable") + ); + } + }, + }) + ); + } + } + ); + + // Show current export status (outside group, as a dynamic element) if (plugin.settings.icsIntegration.enableAutoExport) { - createTextSetting(container, { - name: translate("settings.integrations.autoExport.filePath.name"), - desc: translate("settings.integrations.autoExport.filePath.description"), - placeholder: translate("settings.integrations.autoExport.filePath.placeholder"), - getValue: () => plugin.settings.icsIntegration.autoExportPath, - setValue: async (value: string) => { - plugin.settings.icsIntegration.autoExportPath = value || "tasknotes-calendar.ics"; - save(); - }, - }); - - createNumberSetting(container, { - name: translate("settings.integrations.autoExport.interval.name"), - desc: translate("settings.integrations.autoExport.interval.description"), - placeholder: translate("settings.integrations.autoExport.interval.placeholder"), - min: 5, - max: 1440, // 24 hours max - getValue: () => plugin.settings.icsIntegration.autoExportInterval, - setValue: async (value: number) => { - plugin.settings.icsIntegration.autoExportInterval = Math.max(5, value || 60); - save(); - // Restart the auto export service with new interval - if (plugin.autoExportService) { - plugin.autoExportService.updateInterval( - plugin.settings.icsIntegration.autoExportInterval - ); - } - }, - }); - - // Show current export status const statusContainer = container.createDiv("auto-export-status"); statusContainer.style.marginTop = "10px"; statusContainer.style.padding = "10px"; @@ -1013,79 +1087,65 @@ export function renderIntegrationsTab( errorDiv.style.color = "var(--text-warning)"; errorDiv.textContent = translate("settings.integrations.autoExport.status.serviceNotInitialized"); } - - // Manual export trigger button - createButtonSetting(container, { - name: translate("settings.integrations.autoExport.exportNow.name"), - desc: translate("settings.integrations.autoExport.exportNow.description"), - buttonText: translate("settings.integrations.autoExport.exportNow.buttonText"), - onClick: async () => { - if (plugin.autoExportService) { - try { - await plugin.autoExportService.exportNow(); - new Notice( - translate("settings.integrations.autoExport.notices.exportSuccess") - ); - // Re-render to update status - renderIntegrationsTab(container, plugin, save); - } catch (error) { - console.error("Manual export failed:", error); - new Notice( - translate("settings.integrations.autoExport.notices.exportFailure") - ); - } - } else { - new Notice( - translate("settings.integrations.autoExport.notices.serviceUnavailable") - ); - } - }, - }); } // HTTP API Section (Skip on mobile) if (!Platform.isMobile) { - createSectionHeader(container, translate("settings.integrations.httpApi.header")); - createHelpText(container, translate("settings.integrations.httpApi.description")); - - createToggleSetting(container, { - name: translate("settings.integrations.httpApi.enable.name"), - desc: translate("settings.integrations.httpApi.enable.description"), - getValue: () => plugin.settings.enableAPI, - setValue: async (value: boolean) => { - plugin.settings.enableAPI = value; - save(); - // Re-render to show API settings - renderIntegrationsTab(container, plugin, save); + createSettingGroup( + container, + { + heading: translate("settings.integrations.httpApi.header"), + description: translate("settings.integrations.httpApi.description"), }, - }); + (group) => { + group.addSetting((setting) => + configureToggleSetting(setting, { + name: translate("settings.integrations.httpApi.enable.name"), + desc: translate("settings.integrations.httpApi.enable.description"), + getValue: () => plugin.settings.enableAPI, + setValue: async (value: boolean) => { + plugin.settings.enableAPI = value; + save(); + // Re-render to show API settings + renderIntegrationsTab(container, plugin, save); + }, + }) + ); + if (plugin.settings.enableAPI) { + group.addSetting((setting) => + configureNumberSetting(setting, { + name: translate("settings.integrations.httpApi.port.name"), + desc: translate("settings.integrations.httpApi.port.description"), + placeholder: translate("settings.integrations.httpApi.port.placeholder"), + min: 1024, + max: 65535, + getValue: () => plugin.settings.apiPort, + setValue: async (value: number) => { + plugin.settings.apiPort = value; + save(); + }, + }) + ); + + group.addSetting((setting) => + configureTextSetting(setting, { + name: translate("settings.integrations.httpApi.authToken.name"), + desc: translate("settings.integrations.httpApi.authToken.description"), + placeholder: translate("settings.integrations.httpApi.authToken.placeholder"), + getValue: () => plugin.settings.apiAuthToken, + setValue: async (value: string) => { + plugin.settings.apiAuthToken = value; + save(); + }, + }) + ); + } + } + ); + + // API endpoint info (outside group, as a collapsible dynamic element) if (plugin.settings.enableAPI) { - createNumberSetting(container, { - name: translate("settings.integrations.httpApi.port.name"), - desc: translate("settings.integrations.httpApi.port.description"), - placeholder: translate("settings.integrations.httpApi.port.placeholder"), - min: 1024, - max: 65535, - getValue: () => plugin.settings.apiPort, - setValue: async (value: number) => { - plugin.settings.apiPort = value; - save(); - }, - }); - - createTextSetting(container, { - name: translate("settings.integrations.httpApi.authToken.name"), - desc: translate("settings.integrations.httpApi.authToken.description"), - placeholder: translate("settings.integrations.httpApi.authToken.placeholder"), - getValue: () => plugin.settings.apiAuthToken, - setValue: async (value: string) => { - plugin.settings.apiAuthToken = value; - save(); - }, - }); - - // API endpoint info const apiInfoContainer = container.createDiv("tasknotes-settings__help-section"); const apiHeader = apiInfoContainer.createDiv("tasknotes-settings__collapsible-header"); const apiHeaderContent = apiHeader.createDiv( @@ -1121,71 +1181,79 @@ export function renderIntegrationsTab( } // Webhooks Section - createSectionHeader(container, translate("settings.integrations.webhooks.header")); - - // Webhook description - const webhookDescEl = container.createDiv("setting-item-description"); - webhookDescEl.createEl("p", { - text: translate("settings.integrations.webhooks.description.overview"), - }); - webhookDescEl.createEl("p", { - text: translate("settings.integrations.webhooks.description.usage"), - }); - // Webhook management renderWebhookList(container, plugin, save); - // Add webhook button - createButtonSetting(container, { - name: translate("settings.integrations.webhooks.addWebhook.name"), - desc: translate("settings.integrations.webhooks.addWebhook.description"), - buttonText: translate("settings.integrations.webhooks.addWebhook.buttonText"), - onClick: async () => { - const modal = new WebhookModal( - plugin.app, - async (webhookConfig: Partial) => { - // Generate ID and secret - const webhook: WebhookConfig = { - id: `wh_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, - url: webhookConfig.url || "", - events: webhookConfig.events || [], - secret: generateWebhookSecret(), - active: true, - createdAt: new Date().toISOString(), - failureCount: 0, - successCount: 0, - transformFile: webhookConfig.transformFile, - corsHeaders: webhookConfig.corsHeaders, - }; - - if (!plugin.settings.webhooks) { - plugin.settings.webhooks = []; - } - - plugin.settings.webhooks.push(webhook); - save(); - - // Re-render webhook list to show the new webhook - renderWebhookList( - container.querySelector(".tasknotes-webhooks-container") - ?.parentElement || container, - plugin, - save - ); - - // Show success message with secret - new SecretNoticeModal(plugin.app, webhook.secret).open(); - new Notice(translate("settings.integrations.webhooks.notices.created")); - } - ); - modal.open(); + createSettingGroup( + container, + { + heading: translate("settings.integrations.webhooks.header"), + description: translate("settings.integrations.webhooks.description.overview") + " " + translate("settings.integrations.webhooks.description.usage"), }, - }); + (group) => { + // Add webhook button + group.addSetting((setting) => + configureButtonSetting(setting, { + name: translate("settings.integrations.webhooks.addWebhook.name"), + desc: translate("settings.integrations.webhooks.addWebhook.description"), + buttonText: translate("settings.integrations.webhooks.addWebhook.buttonText"), + onClick: async () => { + const modal = new WebhookModal( + plugin.app, + async (webhookConfig: Partial) => { + // Generate ID and secret + const webhook: WebhookConfig = { + id: `wh_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + url: webhookConfig.url || "", + events: webhookConfig.events || [], + secret: generateWebhookSecret(), + active: true, + createdAt: new Date().toISOString(), + failureCount: 0, + successCount: 0, + transformFile: webhookConfig.transformFile, + corsHeaders: webhookConfig.corsHeaders, + }; + + if (!plugin.settings.webhooks) { + plugin.settings.webhooks = []; + } + + plugin.settings.webhooks.push(webhook); + save(); + + // Re-render webhook list to show the new webhook + renderWebhookList( + container.querySelector(".tasknotes-webhooks-container") + ?.parentElement || container, + plugin, + save + ); + + // Show success message with secret + new SecretNoticeModal(plugin.app, webhook.secret).open(); + new Notice(translate("settings.integrations.webhooks.notices.created")); + } + ); + modal.open(); + }, + }) + ); + } + ); } // Other Integrations Section - createSectionHeader(container, translate("settings.integrations.otherIntegrations.header")); - createHelpText(container, translate("settings.integrations.otherIntegrations.description")); + createSettingGroup( + container, + { + heading: translate("settings.integrations.otherIntegrations.header"), + description: translate("settings.integrations.otherIntegrations.description"), + }, + () => { + // No settings yet - placeholder for future integrations + } + ); } function renderICSSubscriptionsList( diff --git a/src/settings/tabs/modalFieldsTab.ts b/src/settings/tabs/modalFieldsTab.ts index 306668f9..c91530e8 100644 --- a/src/settings/tabs/modalFieldsTab.ts +++ b/src/settings/tabs/modalFieldsTab.ts @@ -1,7 +1,7 @@ import { Notice } from "obsidian"; import TaskNotesPlugin from "../../main"; import { TranslationKey } from "../../i18n"; -import { createSectionHeader, createHelpText, createToggleSetting } from "../components/settingHelpers"; +import { createSettingGroup, configureToggleSetting } from "../components/settingHelpers"; import { createFieldManager, addFieldManagerStyles } from "../components/FieldManagerComponent"; import { initializeFieldConfig } from "../../utils/fieldConfigDefaults"; import type { TaskModalFieldsConfig, UserMappedField } from "../../types/settings"; @@ -32,48 +32,81 @@ export function renderModalFieldsTab( save(); // Save the initialized config } - // Header - createSectionHeader( + // Configuration Section + createSettingGroup( container, - "Task Modal Fields Configuration" - ); - - createHelpText( - container, - "Configure which fields appear in task creation and edit modals. Drag fields to reorder them within each group." - ); - - // Split layout toggle - createToggleSetting(container, { - name: "Split layout on wide screens", - desc: "When enabled, the details editor appears in a right column on screens 900px or wider. When disabled, the modal uses a stacked layout.", - getValue: () => plugin.settings.enableModalSplitLayout, - setValue: (value) => { - plugin.settings.enableModalSplitLayout = value; - save(); + { + heading: "Task Modal Fields Configuration", + description: "Configure which fields appear in task creation and edit modals. Drag fields to reorder them within each group.", }, - }); + (group) => { + // Split layout toggle + group.addSetting((setting) => + configureToggleSetting(setting, { + name: "Split layout on wide screens", + desc: "When enabled, the details editor appears in a right column on screens 900px or wider. When disabled, the modal uses a stacked layout.", + getValue: () => plugin.settings.enableModalSplitLayout, + setValue: (value) => { + plugin.settings.enableModalSplitLayout = value; + save(); + }, + }) + ); - // Sync button to update from user fields - const syncContainer = container.createDiv({ cls: "modal-fields-sync" }); - const syncButton = syncContainer.createEl("button", { - cls: "mod-cta", - text: "Sync User Fields", - }); - syncButton.onclick = () => { - syncUserFieldsToConfig(plugin); - save(); - new Notice("User fields synced to modal configuration"); - // Re-render the tab - renderModalFieldsTab(container, plugin, save); - }; + // Sync button + group.addSetting((setting) => { + setting + .setName("Sync User Fields") + .setDesc("Click to sync custom user fields from Task Properties settings into this configuration.") + .addButton((button) => { + button + .setButtonText("Sync User Fields") + .setCta() + .onClick(() => { + syncUserFieldsToConfig(plugin); + save(); + new Notice("User fields synced to modal configuration"); + // Re-render the tab + renderModalFieldsTab(container, plugin, save); + }); + }); + }); - createHelpText( - syncContainer, - "Click to sync custom user fields from Task Properties settings into this configuration." + // Reset button + group.addSetting((setting) => { + setting + .setName("Reset to Defaults") + .setDesc("Reset all field configurations to their default values. This will remove any custom configurations.") + .addButton((button) => { + button + .setButtonText("Reset to Defaults") + .setWarning() + .onClick(async () => { + const confirmed = await showConfirmationModal(plugin.app, { + title: "Reset Field Configuration", + message: "Are you sure you want to reset field configuration to defaults? This will remove any custom field configurations.", + confirmText: "Reset", + cancelText: "Cancel", + isDestructive: true, + }); + + if (confirmed) { + plugin.settings.modalFieldsConfig = initializeFieldConfig( + undefined, + plugin.settings.userFields + ); + save(); + new Notice("Field configuration reset to defaults"); + // Re-render the tab + renderModalFieldsTab(container, plugin, save); + } + }); + }); + }); + } ); - // Field manager + // Field manager (keep existing component for now - has its own internal tabs) const managerContainer = container.createDiv({ cls: "modal-fields-manager-container" }); // Double-check config exists before creating field manager @@ -92,33 +125,6 @@ export function renderModalFieldsTab( }, plugin.app ); - - // Reset button - const resetContainer = container.createDiv({ cls: "modal-fields-reset" }); - const resetButton = resetContainer.createEl("button", { - cls: "mod-warning", - text: "Reset to Defaults", - }); - resetButton.onclick = async () => { - const confirmed = await showConfirmationModal(plugin.app, { - title: "Reset Field Configuration", - message: "Are you sure you want to reset field configuration to defaults? This will remove any custom field configurations.", - confirmText: "Reset", - cancelText: "Cancel", - isDestructive: true, - }); - - if (confirmed) { - plugin.settings.modalFieldsConfig = initializeFieldConfig( - undefined, - plugin.settings.userFields - ); - save(); - new Notice("Field configuration reset to defaults"); - // Re-render the tab - renderModalFieldsTab(container, plugin, save); - } - }; } /** diff --git a/src/types/obsidian-1.11.d.ts b/src/types/obsidian-1.11.d.ts new file mode 100644 index 00000000..b26e6a42 --- /dev/null +++ b/src/types/obsidian-1.11.d.ts @@ -0,0 +1,55 @@ +/** + * Type declarations for Obsidian 1.11.0 APIs + * These augment the existing obsidian module types + */ + +import "obsidian"; + +declare module "obsidian" { + /** + * A group of related settings with a heading + * @since Obsidian 1.11.0 + */ + export class SettingGroup { + /** + * Creates a new setting group + * @param containerEl - The container element to add the group to + */ + constructor(containerEl: HTMLElement); + + /** + * Sets the heading text for the group + * @param text - The heading text or a DocumentFragment + */ + setHeading(text: string | DocumentFragment): this; + + /** + * Adds a CSS class to the group element + * @param cls - The class name to add + */ + addClass(cls: string): this; + + /** + * Adds a setting to the group + * @param cb - Callback that receives the Setting to configure + */ + addSetting(cb: (setting: Setting) => void): this; + } + + interface Setting { + /** + * Adds a custom component to the setting + * @since Obsidian 1.11.0 + * @param cb - Callback that receives the setting element and returns a component + */ + addComponent(cb: (el: HTMLElement) => T): this; + } + + interface SettingTab { + /** + * The icon to display in the settings sidebar + * @since Obsidian 1.11.0 + */ + icon?: IconName; + } +} diff --git a/tests/__mocks__/obsidian.ts b/tests/__mocks__/obsidian.ts index ed9e3805..00aa7f20 100644 --- a/tests/__mocks__/obsidian.ts +++ b/tests/__mocks__/obsidian.ts @@ -1011,6 +1011,45 @@ export function setTooltip(element: HTMLElement, tooltip: string, options?: { pl element.classList.add('has-tooltip'); } +// API version check utilities (added in Obsidian 1.11.0) +export function requireApiVersion(version: string): boolean { + // Mock implementation - returns true for testing purposes + // This allows testing code that uses SettingGroup + return true; +} + +// SettingGroup mock class (added in Obsidian 1.11.0) +export class SettingGroup { + private containerEl: HTMLElement; + + constructor(containerEl: HTMLElement) { + this.containerEl = containerEl; + } + + setHeading(text: string | DocumentFragment): this { + const heading = document.createElement('div'); + heading.classList.add('setting-item-heading'); + if (typeof text === 'string') { + heading.textContent = text; + } else { + heading.appendChild(text); + } + this.containerEl.appendChild(heading); + return this; + } + + addClass(cls: string): this { + this.containerEl.classList.add(cls); + return this; + } + + addSetting(cb: (setting: Setting) => void): this { + const setting = new Setting(this.containerEl); + cb(setting); + return this; + } +} + // Keymap mock class export class Keymap { pushScope = jest.fn();