From d061be4f371cbdfced4cdab389a802811b23b876 Mon Sep 17 00:00:00 2001 From: Callum Alpass Date: Mon, 30 Jun 2025 19:25:12 +1000 Subject: [PATCH] feat: Add option to store task title exclusively in filename (#106) This commit introduces a new setting that allows users to store the task title exclusively in the filename, removing the 'title' property from the frontmatter. This simplifies the frontmatter but disables filename templating. Key Changes: - **Settings:** Added a boolean option to the plugin settings with a UI toggle in the Task Defaults tab. - **Filename Generation:** Updated to use the task title as the filename when the new setting is enabled, bypassing other filename generation logic. - **Field Mapping:** - Modified in to prevent writing the title to the frontmatter when the new setting is enabled. - Updated to extract the title from the filename if it's not present in the frontmatter, ensuring backward compatibility with existing tasks. - **Services:** Updated and to pass the setting to the field mapper. This change provides users with more control over their data storage and simplifies the frontmatter for new tasks created with this option enabled. --- src/main.ts | 6 ++++-- src/services/FieldMapper.ts | 13 +++++++++++-- src/services/TaskService.ts | 4 ++-- src/settings/settings.ts | 16 ++++++++++++++++ src/utils/MinimalNativeCache.ts | 11 ++++++++--- src/utils/filenameGenerator.ts | 4 ++++ src/utils/helpers.ts | 4 ++-- 7 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/main.ts b/src/main.ts index 614c5dbd..99997e6f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -138,7 +138,8 @@ export default class TaskNotesPlugin extends Plugin { this.settings.taskTag, this.settings.excludedFolders, this.fieldMapper, - this.settings.disableNoteIndexing + this.settings.disableNoteIndexing, + this.settings.storeTitleInFilename ); // Use same instance for event emitting @@ -578,7 +579,8 @@ export default class TaskNotesPlugin extends Plugin { this.settings.taskTag, this.settings.excludedFolders, this.fieldMapper, - this.settings.disableNoteIndexing + this.settings.disableNoteIndexing, + this.settings.storeTitleInFilename ); // Update custom styles diff --git a/src/services/FieldMapper.ts b/src/services/FieldMapper.ts index 4ac7d161..011d3cac 100644 --- a/src/services/FieldMapper.ts +++ b/src/services/FieldMapper.ts @@ -16,7 +16,7 @@ export class FieldMapper { /** * Convert frontmatter object using mapping to internal task data */ - mapFromFrontmatter(frontmatter: any, filePath: string): Partial { + mapFromFrontmatter(frontmatter: any, filePath: string, storeTitleInFilename?: boolean): Partial { if (!frontmatter) return {}; const mapped: Partial = { @@ -26,6 +26,11 @@ export class FieldMapper { // Map each field if it exists in frontmatter if (frontmatter[this.mapping.title] !== undefined) { mapped.title = frontmatter[this.mapping.title]; + } else if (storeTitleInFilename) { + const filename = filePath.split('/').pop()?.replace('.md', ''); + if (filename) { + mapped.title = filename; + } } if (frontmatter[this.mapping.status] !== undefined) { @@ -95,7 +100,7 @@ export class FieldMapper { /** * Convert internal task data to frontmatter using mapping */ - mapToFrontmatter(taskData: Partial, taskTag?: string): any { + mapToFrontmatter(taskData: Partial, taskTag?: string, storeTitleInFilename?: boolean): any { const frontmatter: any = {}; // Map each field if it exists in task data @@ -103,6 +108,10 @@ export class FieldMapper { frontmatter[this.mapping.title] = taskData.title; } + if (storeTitleInFilename) { + delete frontmatter[this.mapping.title]; + } + if (taskData.status !== undefined) { frontmatter[this.mapping.status] = taskData.status; } diff --git a/src/services/TaskService.ts b/src/services/TaskService.ts index 704effa3..e5421877 100644 --- a/src/services/TaskService.ts +++ b/src/services/TaskService.ts @@ -96,7 +96,7 @@ export class TaskService { }; // Use field mapper to convert to frontmatter with proper field mapping - const frontmatter = this.plugin.fieldMapper.mapToFrontmatter(completeTaskData, this.plugin.settings.taskTag); + const frontmatter = this.plugin.fieldMapper.mapToFrontmatter(completeTaskData, this.plugin.settings.taskTag, this.plugin.settings.storeTitleInFilename); // Tags are handled separately (not via field mapper) frontmatter.tags = tagsArray; @@ -593,7 +593,7 @@ export class TaskService { } // Use field mapper to convert ALL task data to frontmatter with proper field mapping - const mappedFrontmatter = this.plugin.fieldMapper.mapToFrontmatter(completeTaskData, this.plugin.settings.taskTag); + const mappedFrontmatter = this.plugin.fieldMapper.mapToFrontmatter(completeTaskData, this.plugin.settings.taskTag, this.plugin.settings.storeTitleInFilename); // Apply mapped frontmatter properties, preserving any existing non-task properties // The FieldMapper only includes task-related fields, so this is safe diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 4a74bd21..890b9bc0 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -15,6 +15,7 @@ export interface TaskNotesSettings { taskOrgFiltersCollapsed: boolean; // Save collapse state of task organization filters // Task filename settings taskFilenameFormat: 'title' | 'zettel' | 'timestamp' | 'custom'; + storeTitleInFilename: boolean; customFilenameTemplate: string; // Template for custom format // Task creation defaults taskCreationDefaults: TaskCreationDefaults; @@ -232,6 +233,7 @@ export const DEFAULT_SETTINGS: TaskNotesSettings = { taskOrgFiltersCollapsed: false, // Default to expanded // Task filename defaults taskFilenameFormat: 'zettel', // Keep existing behavior as default + storeTitleInFilename: false, customFilenameTemplate: '{title}', // Simple title template // Task creation defaults taskCreationDefaults: DEFAULT_TASK_CREATION_DEFAULTS, @@ -522,6 +524,20 @@ export class TaskNotesSettingTab extends PluginSettingTab { // Task filename settings new Setting(container).setName('Task filenames').setHeading(); + new Setting(container) + .setName('Store title exclusively in filename') + .setDesc("When enabled, the task's title will be used as the filename, and the 'title' property will be removed from the frontmatter. This is a significant data storage change that simplifies frontmatter but disables all other filename templating options. Existing tasks will not be affected, but new tasks will follow this rule.") + .addToggle(toggle => { + toggle.toggleEl.setAttribute('aria-label', 'Store title exclusively in filename'); + return toggle + .setValue(this.plugin.settings.storeTitleInFilename) + .onChange(async (value) => { + this.plugin.settings.storeTitleInFilename = value; + await this.plugin.saveSettings(); + this.renderActiveTab(); // Re-render to show/hide other options + }); + }); + new Setting(container) .setName('Filename format') .setDesc('How task filenames should be generated') diff --git a/src/utils/MinimalNativeCache.ts b/src/utils/MinimalNativeCache.ts index a6535153..9896dd80 100644 --- a/src/utils/MinimalNativeCache.ts +++ b/src/utils/MinimalNativeCache.ts @@ -24,6 +24,7 @@ export class MinimalNativeCache extends Events { private excludedFolders: string[]; private fieldMapper?: FieldMapper; private disableNoteIndexing: boolean; + private storeTitleInFilename: boolean; // Only essential indexes - everything else computed on-demand private tasksByDate: Map> = new Map(); // YYYY-MM-DD -> task paths @@ -39,7 +40,8 @@ export class MinimalNativeCache extends Events { taskTag: string, excludedFolders = '', fieldMapper?: FieldMapper, - disableNoteIndexing = false + disableNoteIndexing = false, + storeTitleInFilename = false ) { super(); this.app = app; @@ -49,6 +51,7 @@ export class MinimalNativeCache extends Events { : []; this.fieldMapper = fieldMapper; this.disableNoteIndexing = disableNoteIndexing; + this.storeTitleInFilename = storeTitleInFilename; } /** @@ -660,7 +663,7 @@ export class MinimalNativeCache extends Events { if (!this.fieldMapper) return null; try { - const mappedTask = this.fieldMapper.mapFromFrontmatter(frontmatter, path); + const mappedTask = this.fieldMapper.mapFromFrontmatter(frontmatter, path, this.storeTitleInFilename); return { title: mappedTask.title || 'Untitled task', @@ -719,7 +722,8 @@ export class MinimalNativeCache extends Events { taskTag: string, excludedFolders: string, fieldMapper?: FieldMapper, - disableNoteIndexing = false + disableNoteIndexing = false, + storeTitleInFilename = false ): void { this.taskTag = taskTag; this.excludedFolders = excludedFolders @@ -727,6 +731,7 @@ export class MinimalNativeCache extends Events { : []; this.fieldMapper = fieldMapper; this.disableNoteIndexing = disableNoteIndexing; + this.storeTitleInFilename = storeTitleInFilename; if (this.initialized) { this.clearAllIndexes(); diff --git a/src/utils/filenameGenerator.ts b/src/utils/filenameGenerator.ts index 60dcceb6..499119d2 100644 --- a/src/utils/filenameGenerator.ts +++ b/src/utils/filenameGenerator.ts @@ -43,6 +43,10 @@ export function generateTaskFilename( throw new Error('Invalid date provided in context'); } + if (settings.storeTitleInFilename) { + return sanitizeForFilename(context.title); + } + try { switch (settings.taskFilenameFormat) { case 'title': diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 89212ecf..edae229f 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -298,7 +298,7 @@ export function extractTaskInfo( if (yaml) { if (fieldMapper) { // Use field mapper to extract task info - const mappedTask = fieldMapper.mapFromFrontmatter(yaml, path); + const mappedTask = fieldMapper.mapFromFrontmatter(yaml, path, this.plugin.settings.storeTitleInFilename); // Ensure required fields have defaults const taskInfo: TaskInfo = { @@ -324,7 +324,7 @@ export function extractTaskInfo( } else { // Fallback to default field mapping const defaultMapper = new FieldMapper(DEFAULT_FIELD_MAPPING); - const mappedTask = defaultMapper.mapFromFrontmatter(yaml, path); + const mappedTask = defaultMapper.mapFromFrontmatter(yaml, path, this.plugin.settings.storeTitleInFilename); return { title: mappedTask.title || 'Untitled task',