feat: add template variables support for default task folder (#413)

Enable dynamic folder creation using template variables in the tasksFolder setting.

Features:
- Task variables: {{context}}, {{project}}, {{priority}}, {{status}}, {{title}}
- Date variables: {{year}}, {{month}}, {{day}}, {{date}}
- Works for both regular task creation and inline conversion
- Automatic folder creation and title sanitization
- Comprehensive documentation with examples

Examples:
- Tasks/{{year}}/{{month}} → Tasks/2025/08
- {{project}}/{{priority}} → ProjectName/high
- {{context}}/{{date}} → WorkContext/2025-08-15

Resolves #376
This commit is contained in:
callumalpass 2025-08-16 09:06:34 +10:00 committed by GitHub
parent cb3a0f1d71
commit a1abe71a8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 168 additions and 2 deletions

View file

@ -8,6 +8,91 @@ You can specify a **Default Tasks Folder** where new tasks will be created. You
TaskNotes also provides a system for **Filename Generation**. You can choose from a variety of patterns, including title-based, timestamp-based, and Zettelkasten-style, or you can create your own custom filename template.
### Folder Template Variables
The **Default Tasks Folder** setting supports dynamic folder creation using template variables. This allows you to automatically organize tasks into folders based on their properties and the current date.
#### Available Template Variables
**Task Variables:**
- `{{context}}` - First context from the task's contexts array
- `{{project}}` - First project from the task's projects array
- `{{priority}}` - Task priority (e.g., "high", "medium", "low")
- `{{status}}` - Task status (e.g., "todo", "in-progress", "done")
- `{{title}}` - Task title (sanitized for folder names)
**Date Variables:**
- `{{year}}` - Current year (e.g., "2025")
- `{{month}}` - Current month with leading zero (e.g., "08")
- `{{day}}` - Current day with leading zero (e.g., "15")
- `{{date}}` - Full current date (e.g., "2025-08-15")
#### Template Examples
**Date-based Organization:**
```
Tasks/{{year}}/{{month}}
→ Tasks/2025/08
Tasks/{{year}}/{{date}}
→ Tasks/2025/2025-08-15
```
**Project-based Organization:**
```
{{project}}/{{year}}
→ ProjectName/2025
Projects/{{project}}/{{context}}
→ Projects/ProjectName/ContextName
```
**Priority and Status Organization:**
```
Tasks/{{priority}}/{{status}}
→ Tasks/high/todo
{{status}}/{{priority}}/{{year}}
→ todo/high/2025
```
**Mixed Organization:**
```
{{project}}/{{year}}/{{month}}/{{priority}}
→ ProjectName/2025/08/high
Tasks/{{context}}/{{date}}
→ Tasks/ContextName/2025-08-15
```
#### Important Notes
- **Variable Processing**: Variables are processed when the task is created, using the actual task properties
- **Missing Values**: If a task doesn't have a value for a variable (e.g., no context assigned), the variable is replaced with an empty string
- **Multiple Values**: For arrays like contexts and projects, only the first value is used
- **Title Sanitization**: The `{{title}}` variable automatically removes invalid folder characters (`<>:"/\|?*`) and replaces them with underscores
- **Folder Creation**: Folders are automatically created if they don't exist
- **Inline Tasks**: Template variables also work for the inline task conversion folder setting
#### Advanced Usage
**Conditional Folder Structures:**
Since missing variables become empty strings, you can create conditional folder structures:
```
Tasks/{{project}}/{{context}}/{{year}}
```
- If both project and context exist: `Tasks/ProjectName/ContextName/2025`
- If only project exists: `Tasks/ProjectName//2025` (note the double slash)
- If neither exists: `Tasks///2025`
**Combining with Static Paths:**
```
Work/{{project}}/{{year}}/{{status}}
Archive/{{year}}/{{month}}/{{project}}
```
This feature provides powerful flexibility for automatically organizing your tasks into meaningful folder structures based on their properties and creation date.
### Store Title Exclusively in Filename
This setting provides an alternative way to manage your task titles. 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.

View file

@ -4,6 +4,7 @@ import { Notice, TFile, normalizePath, stringifyYaml } from 'obsidian';
import { TemplateData, mergeTemplateFrontmatter, processTemplate } from '../utils/templateProcessor';
import { addDTSTARTToRecurrenceRule, ensureFolderExists, updateToNextScheduledOccurrence } from '../utils/helpers';
import { formatDateForStorage, getCurrentDateString, getCurrentTimestamp } from '../utils/dateUtils';
import { format } from 'date-fns';
import TaskNotesPlugin from '../main';
@ -20,6 +21,81 @@ export class TaskService {
this.webhookNotifier = notifier;
}
/**
* Process a folder path template with task and date variables
*
* This method enables dynamic folder creation by replacing template variables
* with actual values from the task data and current date.
*
* Supported task variables:
* - {{context}} - First context from the task's contexts array
* - {{project}} - First project from the task's projects array
* - {{priority}} - Task priority (e.g., "high", "medium", "low")
* - {{status}} - Task status (e.g., "todo", "in-progress", "done")
* - {{title}} - Task title (sanitized for folder names)
*
* Supported date variables:
* - {{year}} - Current year (e.g., "2025")
* - {{month}} - Current month with leading zero (e.g., "08")
* - {{day}} - Current day with leading zero (e.g., "15")
* - {{date}} - Full current date (e.g., "2025-08-15")
*
* @param folderTemplate - The template string with variables to process
* @param taskData - Optional task data for variable substitution
* @param date - Date to use for date variables (defaults to current date)
* @returns Processed folder path with variables replaced
*
* @example
* processFolderTemplate("Tasks/{{year}}/{{month}}", taskData)
* // Returns: "Tasks/2025/08"
*
* @example
* processFolderTemplate("{{project}}/{{priority}}", taskData)
* // Returns: "ProjectName/high"
*/
private processFolderTemplate(folderTemplate: string, taskData?: TaskCreationData, date: Date = new Date()): string {
if (!folderTemplate) {
return folderTemplate;
}
let processedPath = folderTemplate;
// Replace task variables if taskData is provided
if (taskData) {
// Handle single context (first one if multiple)
const context = Array.isArray(taskData.contexts) && taskData.contexts.length > 0
? taskData.contexts[0]
: '';
processedPath = processedPath.replace(/\{\{context\}\}/g, context);
// Handle single project (first one if multiple)
const project = Array.isArray(taskData.projects) && taskData.projects.length > 0
? taskData.projects[0]
: '';
processedPath = processedPath.replace(/\{\{project\}\}/g, project);
// Handle priority
const priority = taskData.priority || '';
processedPath = processedPath.replace(/\{\{priority\}\}/g, priority);
// Handle status
const status = taskData.status || '';
processedPath = processedPath.replace(/\{\{status\}\}/g, status);
// Handle title (sanitized for folder names)
const title = taskData.title ? taskData.title.replace(/[<>:"/\\|?*]/g, '_') : '';
processedPath = processedPath.replace(/\{\{title\}\}/g, title);
}
// Replace date variables with current date values
processedPath = processedPath.replace(/\{\{year\}\}/g, format(date, 'yyyy'));
processedPath = processedPath.replace(/\{\{month\}\}/g, format(date, 'MM'));
processedPath = processedPath.replace(/\{\{day\}\}/g, format(date, 'dd'));
processedPath = processedPath.replace(/\{\{date\}\}/g, format(date, 'yyyy-MM-dd'));
return processedPath;
}
/**
* Create a new task file with all the necessary setup
* This is the central method for task creation used by all components
@ -68,6 +144,7 @@ export class TaskService {
const baseFilename = generateTaskFilename(filenameContext, this.plugin.settings);
// Determine folder based on creation context
// Process folder templates with task and date variables for dynamic folder organization
let folder = '';
if (taskData.creationContext === 'inline-conversion') {
// For inline conversion, use the inline task folder setting with variable support
@ -82,13 +159,17 @@ export class TaskService {
} else {
folder = inlineFolder;
}
// Process task and date variables in the inline folder path
folder = this.processFolderTemplate(folder, taskData);
} else {
// Fallback to default tasks folder when inline folder is empty (#128)
folder = this.plugin.settings.tasksFolder || '';
const tasksFolder = this.plugin.settings.tasksFolder || '';
folder = this.processFolderTemplate(tasksFolder, taskData);
}
} else {
// For manual creation and other contexts, use the general tasks folder
folder = this.plugin.settings.tasksFolder || '';
const tasksFolder = this.plugin.settings.tasksFolder || '';
folder = this.processFolderTemplate(tasksFolder, taskData);
}
// Ensure folder exists