feat: adopt Obsidian 1.11.0 API features with backwards compatibility

- Add icon property to settings tab (displays in sidebar on 1.11.0+)
- Refactor all settings tabs to use native SettingGroup class
- Create LegacySettingGroup fallback for Obsidian versions < 1.11.0
- Add configure* helper functions for use with SettingGroup.addSetting
- Add type declarations for new Obsidian 1.11.0 APIs
- Update test mocks with requireApiVersion and SettingGroup
This commit is contained in:
callumalpass 2025-12-29 12:49:25 +11:00
parent b9e7eb7c56
commit a07e3ffc47
10 changed files with 2238 additions and 1758 deletions

View file

@ -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

View file

@ -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) {

View file

@ -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
*/

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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<WebhookConfig>) => {
// 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<WebhookConfig>) => {
// 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(

View file

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

55
src/types/obsidian-1.11.d.ts vendored Normal file
View file

@ -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<T extends BaseComponent>(cb: (el: HTMLElement) => T): this;
}
interface SettingTab {
/**
* The icon to display in the settings sidebar
* @since Obsidian 1.11.0
*/
icon?: IconName;
}
}

View file

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