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.
This commit is contained in:
Callum Alpass 2025-06-30 19:25:12 +10:00
parent e3c4bb18d2
commit d061be4f37
7 changed files with 47 additions and 11 deletions

View file

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

View file

@ -16,7 +16,7 @@ export class FieldMapper {
/**
* Convert frontmatter object using mapping to internal task data
*/
mapFromFrontmatter(frontmatter: any, filePath: string): Partial<TaskInfo> {
mapFromFrontmatter(frontmatter: any, filePath: string, storeTitleInFilename?: boolean): Partial<TaskInfo> {
if (!frontmatter) return {};
const mapped: Partial<TaskInfo> = {
@ -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<TaskInfo>, taskTag?: string): any {
mapToFrontmatter(taskData: Partial<TaskInfo>, 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;
}

View file

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

View file

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

View file

@ -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<string, Set<string>> = 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();

View file

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

View file

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