mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
feat: add customizable task field mapping, statuses, and priorities (#2)
• Introduce new service classes: – FieldMapper: Provides bidirectional mapping between internal task fields and user-configured YAML property names. This ensures backward compatibility while allowing custom field names. – StatusManager: Enables custom task statuses (with colors, labels, and cycling order) by replacing hardcoded “open”, “in-progress”, “done”. It includes helper functions for status cycling and for generating CSS rules. – PriorityManager: Allows custom priority levels defined by users (with weight, label, and color), and provides functions for cycling priorities, comparing priorities for sorting, and generating dynamic CSS. • Update main plugin (main.ts): – Instantiate and initialize the FieldMapper, StatusManager, and PriorityManager using settings. – Inject dynamic CSS for custom statuses and priorities. – Update task updates and file indexer interactions to use the FieldMapper for mapping frontmatter properties. – Refresh UI and cached task info when settings change. • Revise TaskCreationModal: – Change priority and status selection to use custom configurations from PriorityManager and StatusManager rather than fixed options. – Use FieldMapper to map task info into YAML frontmatter when creating a new task. – Update task YAML generation logic to include additional fields (e.g., due date, contexts, recurrence, time estimates) correctly. • Enhance settings UI: – Extend settings (in settings.ts) to include new customization options such as fieldMapping, customStatuses, and customPriorities. – Redesign the settings pane with tabbed navigation (General, Field mapping, Statuses, Priorities, Daily notes, Pomodoro) to allow easier configuration of defaults, field mappings, and custom status/priority settings. – Add warnings for changing field mappings to prevent accidental misconfiguration. • Update types and utilities: – Add FieldMapping, StatusConfig, and PriorityConfig interfaces to types.ts. – Update helper functions (updateTaskProperty and extractTaskInfo in helpers.ts) to use FieldMapper if provided, with fallback to legacy behavior. – Adapt FileIndexer to accept and update the FieldMapper. – Adjust filenameGenerator and TaskListView to work with string-based priority/status values and incorporate custom sorting and cycling logic. • Modify TaskListView: – Replace hardcoded priority and status handling with calls to PriorityManager and StatusManager. – Dynamically generate CSS class names based on custom priorities/statuses. – Update UI event handlers (including cycling of priorities/statuses) to reflect changes via the new managers. – Improve sorting and filtering using custom comparison functions. • Update styles: – Add CSS rules to provide a dynamic top border on task items using the custom priority color on hover. This refactor provides a more flexible and customizable task management system, allowing users to tailor field names, statuses, and priorities to their workflows while maintaining backward compatibility.
This commit is contained in:
parent
a280b66c61
commit
8905686a69
12 changed files with 1674 additions and 332 deletions
144
src/main.ts
144
src/main.ts
|
|
@ -31,11 +31,15 @@ import {
|
|||
ensureFolderExists,
|
||||
generateDailyNoteTemplate,
|
||||
updateYamlFrontmatter,
|
||||
extractTaskInfo
|
||||
extractTaskInfo,
|
||||
updateTaskProperty
|
||||
} from './utils/helpers';
|
||||
import { EventEmitter } from './utils/EventEmitter';
|
||||
import { FileIndexer } from './utils/FileIndexer';
|
||||
import { YAMLCache } from './utils/YAMLCache';
|
||||
import { FieldMapper } from './services/FieldMapper';
|
||||
import { StatusManager } from './services/StatusManager';
|
||||
import { PriorityManager } from './services/PriorityManager';
|
||||
|
||||
export default class TaskNotesPlugin extends Plugin {
|
||||
settings: TaskNotesSettings;
|
||||
|
|
@ -52,21 +56,35 @@ export default class TaskNotesPlugin extends Plugin {
|
|||
// Pomodoro service
|
||||
pomodoroService: PomodoroService;
|
||||
|
||||
// Customization services
|
||||
fieldMapper: FieldMapper;
|
||||
statusManager: StatusManager;
|
||||
priorityManager: PriorityManager;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// Initialize customization services
|
||||
this.fieldMapper = new FieldMapper(this.settings.fieldMapping);
|
||||
this.statusManager = new StatusManager(this.settings.customStatuses);
|
||||
this.priorityManager = new PriorityManager(this.settings.customPriorities);
|
||||
|
||||
// Initialize the file indexer
|
||||
this.fileIndexer = new FileIndexer(
|
||||
this.app.vault,
|
||||
this.settings.taskTag,
|
||||
this.settings.excludedFolders,
|
||||
this.settings.dailyNotesFolder,
|
||||
this.settings.dailyNoteTemplate
|
||||
this.settings.dailyNoteTemplate,
|
||||
this.fieldMapper
|
||||
);
|
||||
|
||||
// Initialize Pomodoro service
|
||||
this.pomodoroService = new PomodoroService(this);
|
||||
await this.pomodoroService.initialize();
|
||||
|
||||
// Inject dynamic styles for custom statuses and priorities
|
||||
this.injectCustomStyles();
|
||||
|
||||
// Register view types
|
||||
this.registerView(
|
||||
|
|
@ -180,10 +198,22 @@ export default class TaskNotesPlugin extends Plugin {
|
|||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
|
||||
// Update customization services with new settings
|
||||
if (this.fieldMapper) {
|
||||
this.fieldMapper.updateMapping(this.settings.fieldMapping);
|
||||
}
|
||||
if (this.statusManager) {
|
||||
this.statusManager.updateStatuses(this.settings.customStatuses);
|
||||
}
|
||||
if (this.priorityManager) {
|
||||
this.priorityManager.updatePriorities(this.settings.customPriorities);
|
||||
}
|
||||
|
||||
// Update the file indexer with new settings if relevant
|
||||
if (this.fileIndexer) {
|
||||
// Update template path without recreating the entire indexer
|
||||
// Update template path and field mapper without recreating the entire indexer
|
||||
this.fileIndexer.updateDailyNoteTemplatePath(this.settings.dailyNoteTemplate);
|
||||
this.fileIndexer.updateFieldMapper(this.fieldMapper);
|
||||
|
||||
// Only recreate indexer if core settings changed
|
||||
const coreSettingsChanged = this.fileIndexer.taskTag !== this.settings.taskTag ||
|
||||
|
|
@ -199,11 +229,15 @@ export default class TaskNotesPlugin extends Plugin {
|
|||
this.settings.taskTag,
|
||||
this.settings.excludedFolders,
|
||||
this.settings.dailyNotesFolder,
|
||||
this.settings.dailyNoteTemplate
|
||||
this.settings.dailyNoteTemplate,
|
||||
this.fieldMapper
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update custom styles
|
||||
this.injectCustomStyles();
|
||||
|
||||
// If settings have changed, notify views to refresh their data
|
||||
this.notifyDataChanged();
|
||||
}
|
||||
|
|
@ -677,6 +711,32 @@ async generateDailyNoteTemplate(date: Date): Promise<string> {
|
|||
return generateDailyNoteTemplate(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject dynamic CSS for custom statuses and priorities
|
||||
*/
|
||||
private injectCustomStyles(): void {
|
||||
// Remove existing custom styles
|
||||
const existingStyle = document.getElementById('tasknotes-custom-styles');
|
||||
if (existingStyle) {
|
||||
existingStyle.remove();
|
||||
}
|
||||
|
||||
// Generate new styles
|
||||
const statusStyles = this.statusManager.getStatusStyles();
|
||||
const priorityStyles = this.priorityManager.getPriorityStyles();
|
||||
|
||||
// Create style element
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.id = 'tasknotes-custom-styles';
|
||||
styleEl.textContent = `
|
||||
${statusStyles}
|
||||
${priorityStyles}
|
||||
`;
|
||||
|
||||
// Inject into document head
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
async updateTaskProperty(task: TaskInfo, property: string, value: any, options: { silent?: boolean } = {}): Promise<void> {
|
||||
try {
|
||||
const file = this.app.vault.getAbstractFileByPath(task.path);
|
||||
|
|
@ -691,29 +751,30 @@ async generateDailyNoteTemplate(date: Date): Promise<string> {
|
|||
|
||||
// Special handling for status changes - update completedDate in local copy
|
||||
if (property === 'status' && !task.recurrence) {
|
||||
if (value === 'done') {
|
||||
if (this.statusManager.isCompletedStatus(value)) {
|
||||
updatedTask.completedDate = format(new Date(), 'yyyy-MM-dd');
|
||||
} else if (value === 'open' || value === 'in-progress') {
|
||||
} else {
|
||||
updatedTask.completedDate = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the frontmatter
|
||||
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
// Update the property
|
||||
frontmatter[property] = value;
|
||||
|
||||
// Special handling for status changes - update completedDate
|
||||
if (property === 'status' && !task.recurrence) {
|
||||
if (value === 'done') {
|
||||
// Set completedDate to today when marking as done
|
||||
frontmatter.completedDate = format(new Date(), 'yyyy-MM-dd');
|
||||
} else if (value === 'open' || value === 'in-progress') {
|
||||
// Clear completedDate when marking as open or in-progress
|
||||
delete frontmatter.completedDate;
|
||||
}
|
||||
// Read current content and use field mapping to update
|
||||
const content = await this.app.vault.read(file);
|
||||
const propertyUpdates: Partial<TaskInfo> = {};
|
||||
(propertyUpdates as any)[property] = value;
|
||||
|
||||
// Special handling for status changes - update completedDate
|
||||
if (property === 'status' && !task.recurrence) {
|
||||
if (this.statusManager.isCompletedStatus(value)) {
|
||||
propertyUpdates.completedDate = format(new Date(), 'yyyy-MM-dd');
|
||||
} else {
|
||||
propertyUpdates.completedDate = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Use field mapping to update the content
|
||||
const updatedContent = updateTaskProperty(content, propertyUpdates, this.fieldMapper);
|
||||
await this.app.vault.modify(file, updatedContent);
|
||||
|
||||
// Show a notice (unless silent)
|
||||
if (!options.silent) {
|
||||
|
|
@ -756,7 +817,7 @@ async generateDailyNoteTemplate(date: Date): Promise<string> {
|
|||
try {
|
||||
if (!task.recurrence) {
|
||||
// Not a recurring task - do regular status toggle
|
||||
const newStatus = task.status === 'done' ? 'open' : 'done';
|
||||
const newStatus = this.statusManager.getNextStatus(task.status);
|
||||
await this.updateTaskProperty(task, 'status', newStatus);
|
||||
return;
|
||||
}
|
||||
|
|
@ -844,40 +905,23 @@ async generateDailyNoteTemplate(date: Date): Promise<string> {
|
|||
const updatedTask = { ...task };
|
||||
updatedTask.archived = !task.archived;
|
||||
|
||||
// Process the frontmatter
|
||||
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||
// Make sure tags array exists
|
||||
if (!frontmatter.tags) {
|
||||
frontmatter.tags = [];
|
||||
}
|
||||
|
||||
// Convert to array if it's not already
|
||||
if (!Array.isArray(frontmatter.tags)) {
|
||||
frontmatter.tags = [frontmatter.tags];
|
||||
}
|
||||
|
||||
// Toggle archive tag
|
||||
if (task.archived) {
|
||||
// Remove archive tag
|
||||
frontmatter.tags = frontmatter.tags.filter(
|
||||
(tag: string) => tag !== 'archive'
|
||||
);
|
||||
} else {
|
||||
// Add archive tag if not present
|
||||
if (!frontmatter.tags.includes('archive')) {
|
||||
frontmatter.tags.push('archive');
|
||||
}
|
||||
}
|
||||
});
|
||||
// Read current content and use field mapping to update
|
||||
const content = await this.app.vault.read(file);
|
||||
const propertyUpdates: Partial<TaskInfo> = {
|
||||
archived: !task.archived
|
||||
};
|
||||
|
||||
// Use field mapping to update the content
|
||||
const updatedContent = updateTaskProperty(content, propertyUpdates, this.fieldMapper);
|
||||
await this.app.vault.modify(file, updatedContent);
|
||||
|
||||
// Show a notice
|
||||
new Notice(task.archived ? 'Task unarchived' : 'Task archived');
|
||||
|
||||
// Add the updated task to the file indexer's cache
|
||||
if (this.fileIndexer) {
|
||||
// Read the file to get the most up-to-date content
|
||||
const content = await this.app.vault.cachedRead(file);
|
||||
const taskInfo = extractTaskInfo(content, task.path);
|
||||
// Use the updated task info with field mapping
|
||||
const taskInfo = extractTaskInfo(updatedContent, task.path, this.fieldMapper);
|
||||
|
||||
// Find and update this file in the index
|
||||
await this.fileIndexer.updateTaskInfoInCache(task.path, taskInfo as TaskInfo);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ export class TaskCreationModal extends Modal {
|
|||
title: string = '';
|
||||
details: string = '';
|
||||
dueDate: string = '';
|
||||
priority: 'low' | 'normal' | 'high' = 'normal';
|
||||
status: 'open' | 'in-progress' | 'done' = 'open';
|
||||
priority: string = 'normal';
|
||||
status: string = 'open';
|
||||
contexts: string = '';
|
||||
tags: string = '';
|
||||
timeEstimate: number = 0;
|
||||
|
|
@ -188,22 +188,22 @@ export class TaskCreationModal extends Modal {
|
|||
this.createFormGroup(contentEl, 'Priority', (container) => {
|
||||
const select = container.createEl('select');
|
||||
|
||||
const options = [
|
||||
{ value: 'low', text: 'Low' },
|
||||
{ value: 'normal', text: 'Normal' },
|
||||
{ value: 'high', text: 'High' }
|
||||
];
|
||||
// Get custom priorities ordered by weight (highest first)
|
||||
const priorities = this.plugin.priorityManager.getPrioritiesByWeight();
|
||||
|
||||
options.forEach(option => {
|
||||
const optEl = select.createEl('option', { value: option.value, text: option.text });
|
||||
if (option.value === this.plugin.settings.defaultTaskPriority) {
|
||||
priorities.forEach(priorityConfig => {
|
||||
const optEl = select.createEl('option', {
|
||||
value: priorityConfig.value,
|
||||
text: priorityConfig.label
|
||||
});
|
||||
if (priorityConfig.value === this.plugin.settings.defaultTaskPriority) {
|
||||
optEl.selected = true;
|
||||
this.priority = option.value as 'low' | 'normal' | 'high';
|
||||
this.priority = priorityConfig.value;
|
||||
}
|
||||
});
|
||||
|
||||
select.addEventListener('change', (e) => {
|
||||
this.priority = (e.target as HTMLSelectElement).value as 'low' | 'normal' | 'high';
|
||||
this.priority = (e.target as HTMLSelectElement).value;
|
||||
this.updateFilenamePreview();
|
||||
});
|
||||
});
|
||||
|
|
@ -212,22 +212,22 @@ export class TaskCreationModal extends Modal {
|
|||
this.createFormGroup(contentEl, 'Status', (container) => {
|
||||
const select = container.createEl('select');
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'open', text: 'Open' },
|
||||
{ value: 'in-progress', text: 'In progress' },
|
||||
{ value: 'done', text: 'Done' }
|
||||
];
|
||||
// Get custom statuses ordered by their order field
|
||||
const statuses = this.plugin.statusManager.getStatusesByOrder();
|
||||
|
||||
statusOptions.forEach(option => {
|
||||
const optEl = select.createEl('option', { value: option.value, text: option.text });
|
||||
if (option.value === this.plugin.settings.defaultTaskStatus) {
|
||||
statuses.forEach(statusConfig => {
|
||||
const optEl = select.createEl('option', {
|
||||
value: statusConfig.value,
|
||||
text: statusConfig.label
|
||||
});
|
||||
if (statusConfig.value === this.plugin.settings.defaultTaskStatus) {
|
||||
optEl.selected = true;
|
||||
this.status = option.value as 'open' | 'in-progress' | 'done';
|
||||
this.status = statusConfig.value;
|
||||
}
|
||||
});
|
||||
|
||||
select.addEventListener('change', (e) => {
|
||||
this.status = (e.target as HTMLSelectElement).value as 'open' | 'in-progress' | 'done';
|
||||
this.status = (e.target as HTMLSelectElement).value;
|
||||
this.updateFilenamePreview();
|
||||
});
|
||||
});
|
||||
|
|
@ -707,64 +707,81 @@ export class TaskCreationModal extends Modal {
|
|||
.filter(context => context.length > 0);
|
||||
}
|
||||
|
||||
// Create the YAML frontmatter
|
||||
const yaml: Partial<TaskFrontmatter> = {
|
||||
// Create task info object
|
||||
const taskInfo: Partial<TaskInfo> = {
|
||||
title: this.title,
|
||||
dateCreated: format(now, "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
dateModified: format(now, "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
status: this.status,
|
||||
tags: tagsArray,
|
||||
priority: this.priority,
|
||||
tags: tagsArray,
|
||||
};
|
||||
|
||||
// Add completedDate if status is done
|
||||
if (this.status === 'done') {
|
||||
yaml.completedDate = format(now, 'yyyy-MM-dd');
|
||||
// Add completedDate if status is completed
|
||||
if (this.plugin.statusManager.isCompletedStatus(this.status)) {
|
||||
taskInfo.completedDate = format(now, 'yyyy-MM-dd');
|
||||
}
|
||||
|
||||
// Add optional fields
|
||||
// Use field mapper to create the YAML frontmatter
|
||||
const yaml = this.plugin.fieldMapper.mapToFrontmatter(taskInfo);
|
||||
|
||||
// Add standard fields that aren't mapped
|
||||
yaml.dateCreated = format(now, "yyyy-MM-dd'T'HH:mm:ss");
|
||||
yaml.dateModified = format(now, "yyyy-MM-dd'T'HH:mm:ss");
|
||||
|
||||
// Add optional fields through field mapping
|
||||
if (this.dueDate) {
|
||||
yaml.due = this.dueDate;
|
||||
taskInfo.due = this.dueDate;
|
||||
}
|
||||
|
||||
if (contextsArray.length > 0) {
|
||||
yaml.contexts = contextsArray;
|
||||
taskInfo.contexts = contextsArray;
|
||||
}
|
||||
|
||||
if (this.timeEstimate > 0) {
|
||||
yaml.timeEstimate = this.timeEstimate;
|
||||
yaml.timeSpent = 0;
|
||||
yaml.timeEntries = [];
|
||||
taskInfo.timeEstimate = this.timeEstimate;
|
||||
taskInfo.timeSpent = 0;
|
||||
taskInfo.timeEntries = [];
|
||||
}
|
||||
|
||||
// Re-map with additional fields
|
||||
const finalYaml = this.plugin.fieldMapper.mapToFrontmatter(taskInfo);
|
||||
|
||||
// Preserve standard fields
|
||||
finalYaml.dateCreated = yaml.dateCreated;
|
||||
finalYaml.dateModified = yaml.dateModified;
|
||||
|
||||
// Add recurrence info if specified
|
||||
if (this.recurrence !== 'none') {
|
||||
yaml.recurrence = {
|
||||
taskInfo.recurrence = {
|
||||
frequency: this.recurrence
|
||||
};
|
||||
|
||||
if (this.recurrence === 'weekly' && this.daysOfWeek.length > 0) {
|
||||
yaml.recurrence.days_of_week = this.daysOfWeek;
|
||||
taskInfo.recurrence.days_of_week = this.daysOfWeek;
|
||||
}
|
||||
|
||||
if (this.recurrence === 'monthly' && this.dayOfMonth) {
|
||||
yaml.recurrence.day_of_month = parseInt(this.dayOfMonth);
|
||||
taskInfo.recurrence.day_of_month = parseInt(this.dayOfMonth);
|
||||
}
|
||||
|
||||
if (this.recurrence === 'yearly') {
|
||||
if (this.dayOfMonth) {
|
||||
yaml.recurrence.day_of_month = parseInt(this.dayOfMonth);
|
||||
taskInfo.recurrence.day_of_month = parseInt(this.dayOfMonth);
|
||||
}
|
||||
if (this.monthOfYear) {
|
||||
yaml.recurrence.month_of_year = parseInt(this.monthOfYear);
|
||||
taskInfo.recurrence.month_of_year = parseInt(this.monthOfYear);
|
||||
}
|
||||
}
|
||||
|
||||
yaml.complete_instances = [];
|
||||
taskInfo.complete_instances = [];
|
||||
}
|
||||
|
||||
// Create final YAML with all fields mapped
|
||||
const completeYaml = this.plugin.fieldMapper.mapToFrontmatter(taskInfo);
|
||||
completeYaml.dateCreated = format(now, "yyyy-MM-dd'T'HH:mm:ss");
|
||||
completeYaml.dateModified = format(now, "yyyy-MM-dd'T'HH:mm:ss");
|
||||
|
||||
// Prepare the file content
|
||||
const content = `---\n${YAML.stringify(yaml)}---\n\n# ${this.title}\n\n${this.details}`;
|
||||
const content = `---\n${YAML.stringify(completeYaml)}---\n\n# ${this.title}\n\n${this.details}`;
|
||||
|
||||
// Create the file
|
||||
await this.app.vault.create(taskFilePath, content);
|
||||
|
|
|
|||
182
src/services/FieldMapper.ts
Normal file
182
src/services/FieldMapper.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { FieldMapping, TaskInfo } from '../types';
|
||||
|
||||
/**
|
||||
* Service for mapping between internal field names and user-configured property names
|
||||
*/
|
||||
export class FieldMapper {
|
||||
constructor(private mapping: FieldMapping) {}
|
||||
|
||||
/**
|
||||
* Convert internal field name to user's property name
|
||||
*/
|
||||
toUserField(internalName: keyof FieldMapping): string {
|
||||
return this.mapping[internalName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert frontmatter object using mapping to internal task data
|
||||
*/
|
||||
mapFromFrontmatter(frontmatter: any, filePath: string): Partial<TaskInfo> {
|
||||
if (!frontmatter) return {};
|
||||
|
||||
const mapped: Partial<TaskInfo> = {
|
||||
path: filePath
|
||||
};
|
||||
|
||||
// Map each field if it exists in frontmatter
|
||||
if (frontmatter[this.mapping.title] !== undefined) {
|
||||
mapped.title = frontmatter[this.mapping.title];
|
||||
}
|
||||
|
||||
if (frontmatter[this.mapping.status] !== undefined) {
|
||||
mapped.status = frontmatter[this.mapping.status];
|
||||
}
|
||||
|
||||
if (frontmatter[this.mapping.priority] !== undefined) {
|
||||
mapped.priority = frontmatter[this.mapping.priority];
|
||||
}
|
||||
|
||||
if (frontmatter[this.mapping.due] !== undefined) {
|
||||
mapped.due = frontmatter[this.mapping.due];
|
||||
}
|
||||
|
||||
if (frontmatter[this.mapping.contexts] !== undefined) {
|
||||
const contexts = frontmatter[this.mapping.contexts];
|
||||
// Ensure contexts is always an array
|
||||
mapped.contexts = Array.isArray(contexts) ? contexts : [contexts];
|
||||
}
|
||||
|
||||
if (frontmatter[this.mapping.timeEstimate] !== undefined) {
|
||||
mapped.timeEstimate = frontmatter[this.mapping.timeEstimate];
|
||||
}
|
||||
|
||||
if (frontmatter[this.mapping.timeSpent] !== undefined) {
|
||||
mapped.timeSpent = frontmatter[this.mapping.timeSpent];
|
||||
}
|
||||
|
||||
if (frontmatter[this.mapping.completedDate] !== undefined) {
|
||||
mapped.completedDate = frontmatter[this.mapping.completedDate];
|
||||
}
|
||||
|
||||
if (frontmatter[this.mapping.recurrence] !== undefined) {
|
||||
mapped.recurrence = frontmatter[this.mapping.recurrence];
|
||||
}
|
||||
|
||||
// Handle tags array (includes archive tag)
|
||||
if (frontmatter.tags && Array.isArray(frontmatter.tags)) {
|
||||
mapped.tags = frontmatter.tags;
|
||||
mapped.archived = frontmatter.tags.includes(this.mapping.archiveTag);
|
||||
}
|
||||
|
||||
// Handle time entries
|
||||
if (frontmatter.timeEntries !== undefined) {
|
||||
mapped.timeEntries = frontmatter.timeEntries;
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert internal task data to frontmatter using mapping
|
||||
*/
|
||||
mapToFrontmatter(taskData: Partial<TaskInfo>): any {
|
||||
const frontmatter: any = {};
|
||||
|
||||
// Map each field if it exists in task data
|
||||
if (taskData.title !== undefined) {
|
||||
frontmatter[this.mapping.title] = taskData.title;
|
||||
}
|
||||
|
||||
if (taskData.status !== undefined) {
|
||||
frontmatter[this.mapping.status] = taskData.status;
|
||||
}
|
||||
|
||||
if (taskData.priority !== undefined) {
|
||||
frontmatter[this.mapping.priority] = taskData.priority;
|
||||
}
|
||||
|
||||
if (taskData.due !== undefined) {
|
||||
frontmatter[this.mapping.due] = taskData.due;
|
||||
}
|
||||
|
||||
if (taskData.contexts !== undefined) {
|
||||
frontmatter[this.mapping.contexts] = taskData.contexts;
|
||||
}
|
||||
|
||||
if (taskData.timeEstimate !== undefined) {
|
||||
frontmatter[this.mapping.timeEstimate] = taskData.timeEstimate;
|
||||
}
|
||||
|
||||
if (taskData.timeSpent !== undefined) {
|
||||
frontmatter[this.mapping.timeSpent] = taskData.timeSpent;
|
||||
}
|
||||
|
||||
if (taskData.completedDate !== undefined) {
|
||||
frontmatter[this.mapping.completedDate] = taskData.completedDate;
|
||||
}
|
||||
|
||||
if (taskData.recurrence !== undefined) {
|
||||
frontmatter[this.mapping.recurrence] = taskData.recurrence;
|
||||
}
|
||||
|
||||
// Handle tags (merge archive status into tags array)
|
||||
let tags = taskData.tags ? [...taskData.tags] : [];
|
||||
|
||||
if (taskData.archived === true && !tags.includes(this.mapping.archiveTag)) {
|
||||
tags.push(this.mapping.archiveTag);
|
||||
} else if (taskData.archived === false) {
|
||||
tags = tags.filter(tag => tag !== this.mapping.archiveTag);
|
||||
}
|
||||
|
||||
if (tags.length > 0) {
|
||||
frontmatter.tags = tags;
|
||||
}
|
||||
|
||||
// Handle time entries
|
||||
if (taskData.timeEntries !== undefined) {
|
||||
frontmatter.timeEntries = taskData.timeEntries;
|
||||
}
|
||||
|
||||
return frontmatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mapping configuration
|
||||
*/
|
||||
updateMapping(newMapping: FieldMapping): void {
|
||||
this.mapping = newMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current mapping
|
||||
*/
|
||||
getMapping(): FieldMapping {
|
||||
return { ...this.mapping };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a mapping has no empty field names
|
||||
*/
|
||||
static validateMapping(mapping: FieldMapping): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
const fields = Object.keys(mapping) as (keyof FieldMapping)[];
|
||||
for (const field of fields) {
|
||||
if (!mapping[field] || mapping[field].trim() === '') {
|
||||
errors.push(`Field "${field}" cannot be empty`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate values
|
||||
const values = Object.values(mapping);
|
||||
const uniqueValues = new Set(values);
|
||||
if (values.length !== uniqueValues.size) {
|
||||
errors.push('Field mappings must have unique property names');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
}
|
||||
212
src/services/PriorityManager.ts
Normal file
212
src/services/PriorityManager.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import { PriorityConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Service for managing custom task priorities
|
||||
*/
|
||||
export class PriorityManager {
|
||||
constructor(private priorities: PriorityConfig[]) {}
|
||||
|
||||
/**
|
||||
* Get priority configuration by value
|
||||
*/
|
||||
getPriorityConfig(value: string): PriorityConfig | undefined {
|
||||
return this.priorities.find(p => p.value === value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priorities ordered by weight (highest weight first)
|
||||
*/
|
||||
getPrioritiesByWeight(): PriorityConfig[] {
|
||||
return [...this.priorities].sort((a, b) => b.weight - a.weight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priorities ordered by weight (lowest weight first)
|
||||
*/
|
||||
getPrioritiesByWeightAsc(): PriorityConfig[] {
|
||||
return [...this.priorities].sort((a, b) => a.weight - b.weight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next priority in cycle (cycling by weight order)
|
||||
*/
|
||||
getNextPriority(currentPriority: string): string {
|
||||
const sortedPriorities = this.getPrioritiesByWeightAsc();
|
||||
const currentIndex = sortedPriorities.findIndex(p => p.value === currentPriority);
|
||||
|
||||
if (currentIndex === -1) {
|
||||
// Current priority not found, return first priority
|
||||
return sortedPriorities[0]?.value || 'normal';
|
||||
}
|
||||
|
||||
// Get next priority, cycling to first if at end
|
||||
const nextIndex = (currentIndex + 1) % sortedPriorities.length;
|
||||
return sortedPriorities[nextIndex].value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare priorities for sorting (higher weight = higher priority)
|
||||
* Returns negative if a has lower priority, positive if higher, 0 if equal
|
||||
*/
|
||||
comparePriorities(a: string, b: string): number {
|
||||
const priorityA = this.getPriorityConfig(a);
|
||||
const priorityB = this.getPriorityConfig(b);
|
||||
|
||||
// Default weight if priority not found
|
||||
const weightA = priorityA?.weight || 0;
|
||||
const weightB = priorityB?.weight || 0;
|
||||
|
||||
return weightB - weightA; // Higher weight comes first
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSS variables for priority colors
|
||||
*/
|
||||
getPriorityStyles(): string {
|
||||
const cssRules: string[] = [];
|
||||
|
||||
for (const priority of this.priorities) {
|
||||
const cssClass = `--priority-${priority.value.replace(/[^a-zA-Z0-9-]/g, '-')}-color`;
|
||||
cssRules.push(`${cssClass}: ${priority.color};`);
|
||||
}
|
||||
|
||||
return `:root { ${cssRules.join(' ')} }`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all priority configurations
|
||||
*/
|
||||
getAllPriorities(): PriorityConfig[] {
|
||||
return [...this.priorities];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update priority configurations
|
||||
*/
|
||||
updatePriorities(newPriorities: PriorityConfig[]): void {
|
||||
this.priorities = newPriorities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the highest priority value (highest weight)
|
||||
*/
|
||||
getHighestPriority(): string | undefined {
|
||||
const sorted = this.getPrioritiesByWeight();
|
||||
return sorted[0]?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lowest priority value (lowest weight)
|
||||
*/
|
||||
getLowestPriority(): string | undefined {
|
||||
const sorted = this.getPrioritiesByWeightAsc();
|
||||
return sorted[0]?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if priority A is higher than priority B
|
||||
*/
|
||||
isHigherPriority(a: string, b: string): boolean {
|
||||
return this.comparePriorities(a, b) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate priority configuration
|
||||
*/
|
||||
static validatePriorities(priorities: PriorityConfig[]): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
// At least 1 priority required
|
||||
if (priorities.length < 1) {
|
||||
errors.push('At least 1 priority is required');
|
||||
}
|
||||
|
||||
// Check for unique priority values
|
||||
const values = priorities.map(p => p.value);
|
||||
const uniqueValues = new Set(values);
|
||||
if (values.length !== uniqueValues.size) {
|
||||
errors.push('Priority values must be unique');
|
||||
}
|
||||
|
||||
// Check for unique IDs
|
||||
const ids = priorities.map(p => p.id);
|
||||
const uniqueIds = new Set(ids);
|
||||
if (ids.length !== uniqueIds.size) {
|
||||
errors.push('Priority IDs must be unique');
|
||||
}
|
||||
|
||||
// Check for unique weights
|
||||
const weights = priorities.map(p => p.weight);
|
||||
const uniqueWeights = new Set(weights);
|
||||
if (weights.length !== uniqueWeights.size) {
|
||||
errors.push('Priority weights must be unique');
|
||||
}
|
||||
|
||||
// Check for empty values and labels
|
||||
for (const priority of priorities) {
|
||||
if (!priority.value || priority.value.trim() === '') {
|
||||
errors.push('Priority values cannot be empty');
|
||||
break;
|
||||
}
|
||||
if (!priority.label || priority.label.trim() === '') {
|
||||
errors.push('Priority labels cannot be empty');
|
||||
break;
|
||||
}
|
||||
if (!priority.color || !priority.color.match(/^#[0-9a-fA-F]{6}$/)) {
|
||||
errors.push('Priority colors must be valid hex colors (#rrggbb)');
|
||||
break;
|
||||
}
|
||||
if (typeof priority.weight !== 'number' || priority.weight < 0) {
|
||||
errors.push('Priority weights must be non-negative numbers');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new unique priority ID
|
||||
*/
|
||||
static generatePriorityId(existingPriorities: PriorityConfig[]): string {
|
||||
const existingIds = new Set(existingPriorities.map(p => p.id));
|
||||
let counter = 1;
|
||||
let id = `priority-${counter}`;
|
||||
|
||||
while (existingIds.has(id)) {
|
||||
counter++;
|
||||
id = `priority-${counter}`;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new unique weight value
|
||||
*/
|
||||
static generatePriorityWeight(existingPriorities: PriorityConfig[]): number {
|
||||
const existingWeights = existingPriorities.map(p => p.weight);
|
||||
if (existingWeights.length === 0) return 1;
|
||||
|
||||
return Math.max(...existingWeights) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new priority with default values
|
||||
*/
|
||||
static createDefaultPriority(existingPriorities: PriorityConfig[]): PriorityConfig {
|
||||
const id = PriorityManager.generatePriorityId(existingPriorities);
|
||||
const weight = PriorityManager.generatePriorityWeight(existingPriorities);
|
||||
|
||||
return {
|
||||
id,
|
||||
value: 'new-priority',
|
||||
label: 'New Priority',
|
||||
color: '#808080',
|
||||
weight
|
||||
};
|
||||
}
|
||||
}
|
||||
179
src/services/StatusManager.ts
Normal file
179
src/services/StatusManager.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import { StatusConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Service for managing custom task statuses
|
||||
*/
|
||||
export class StatusManager {
|
||||
constructor(private statuses: StatusConfig[]) {}
|
||||
|
||||
/**
|
||||
* Get next status in cycle from current status
|
||||
*/
|
||||
getNextStatus(currentStatus: string): string {
|
||||
const sortedStatuses = this.getStatusesByOrder();
|
||||
const currentIndex = sortedStatuses.findIndex(s => s.value === currentStatus);
|
||||
|
||||
if (currentIndex === -1) {
|
||||
// Current status not found, return first status
|
||||
return sortedStatuses[0]?.value || 'open';
|
||||
}
|
||||
|
||||
// Get next status, cycling to first if at end
|
||||
const nextIndex = (currentIndex + 1) % sortedStatuses.length;
|
||||
return sortedStatuses[nextIndex].value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status configuration by value
|
||||
*/
|
||||
getStatusConfig(value: string): StatusConfig | undefined {
|
||||
return this.statuses.find(s => s.value === value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all completed status values
|
||||
*/
|
||||
getCompletedStatuses(): string[] {
|
||||
return this.statuses
|
||||
.filter(s => s.isCompleted)
|
||||
.map(s => s.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all non-completed status values
|
||||
*/
|
||||
getOpenStatuses(): string[] {
|
||||
return this.statuses
|
||||
.filter(s => !s.isCompleted)
|
||||
.map(s => s.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statuses ordered by their order field
|
||||
*/
|
||||
getStatusesByOrder(): StatusConfig[] {
|
||||
return [...this.statuses].sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a status value represents a completed task
|
||||
*/
|
||||
isCompletedStatus(statusValue: string): boolean {
|
||||
const status = this.getStatusConfig(statusValue);
|
||||
return status?.isCompleted || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSS variables for status colors
|
||||
*/
|
||||
getStatusStyles(): string {
|
||||
const cssRules: string[] = [];
|
||||
|
||||
for (const status of this.statuses) {
|
||||
const cssClass = `--status-${status.value.replace(/[^a-zA-Z0-9-]/g, '-')}-color`;
|
||||
cssRules.push(`${cssClass}: ${status.color};`);
|
||||
}
|
||||
|
||||
return `:root { ${cssRules.join(' ')} }`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all status configurations
|
||||
*/
|
||||
getAllStatuses(): StatusConfig[] {
|
||||
return [...this.statuses];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status configurations
|
||||
*/
|
||||
updateStatuses(newStatuses: StatusConfig[]): void {
|
||||
this.statuses = newStatuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate status configuration
|
||||
*/
|
||||
static validateStatuses(statuses: StatusConfig[]): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Minimum 2 statuses required
|
||||
if (statuses.length < 2) {
|
||||
errors.push('At least 2 statuses are required');
|
||||
}
|
||||
|
||||
// At least one completed status required
|
||||
const hasCompletedStatus = statuses.some(s => s.isCompleted);
|
||||
if (!hasCompletedStatus) {
|
||||
errors.push('At least one status must be marked as completed');
|
||||
}
|
||||
|
||||
// Check for unique status values
|
||||
const values = statuses.map(s => s.value);
|
||||
const uniqueValues = new Set(values);
|
||||
if (values.length !== uniqueValues.size) {
|
||||
errors.push('Status values must be unique');
|
||||
}
|
||||
|
||||
// Check for unique IDs
|
||||
const ids = statuses.map(s => s.id);
|
||||
const uniqueIds = new Set(ids);
|
||||
if (ids.length !== uniqueIds.size) {
|
||||
errors.push('Status IDs must be unique');
|
||||
}
|
||||
|
||||
// Check for empty values and labels
|
||||
for (const status of statuses) {
|
||||
if (!status.value || status.value.trim() === '') {
|
||||
errors.push('Status values cannot be empty');
|
||||
break;
|
||||
}
|
||||
if (!status.label || status.label.trim() === '') {
|
||||
errors.push('Status labels cannot be empty');
|
||||
break;
|
||||
}
|
||||
if (!status.color || !status.color.match(/^#[0-9a-fA-F]{6}$/)) {
|
||||
errors.push('Status colors must be valid hex colors (#rrggbb)');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new unique status ID
|
||||
*/
|
||||
static generateStatusId(existingStatuses: StatusConfig[]): string {
|
||||
const existingIds = new Set(existingStatuses.map(s => s.id));
|
||||
let counter = 1;
|
||||
let id = `status-${counter}`;
|
||||
|
||||
while (existingIds.has(id)) {
|
||||
counter++;
|
||||
id = `status-${counter}`;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new status with default values
|
||||
*/
|
||||
static createDefaultStatus(existingStatuses: StatusConfig[]): StatusConfig {
|
||||
const id = StatusManager.generateStatusId(existingStatuses);
|
||||
const order = Math.max(...existingStatuses.map(s => s.order), 0) + 1;
|
||||
|
||||
return {
|
||||
id,
|
||||
value: 'new-status',
|
||||
label: 'New Status',
|
||||
color: '#808080',
|
||||
isCompleted: false,
|
||||
order
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||
import TaskNotesPlugin from '../main';
|
||||
import { FieldMapping, StatusConfig, PriorityConfig } from '../types';
|
||||
import { StatusManager } from '../services/StatusManager';
|
||||
import { PriorityManager } from '../services/PriorityManager';
|
||||
|
||||
export interface TaskNotesSettings {
|
||||
dailyNotesFolder: string;
|
||||
tasksFolder: string; // Now just a default location for new tasks
|
||||
taskTag: string; // The tag that identifies tasks
|
||||
excludedFolders: string; // Comma-separated list of folders to exclude from Notes tab
|
||||
defaultTaskPriority: 'low' | 'normal' | 'high';
|
||||
defaultTaskStatus: 'open' | 'in-progress' | 'done';
|
||||
defaultTaskPriority: string; // Changed to string to support custom priorities
|
||||
defaultTaskStatus: string; // Changed to string to support custom statuses
|
||||
taskOrgFiltersCollapsed: boolean; // Save collapse state of task organization filters
|
||||
// Daily note settings
|
||||
dailyNoteTemplate: string; // Path to template file for daily notes
|
||||
|
|
@ -24,8 +27,81 @@ export interface TaskNotesSettings {
|
|||
pomodoroNotifications: boolean;
|
||||
pomodoroSoundEnabled: boolean;
|
||||
pomodoroSoundVolume: number; // 0-100
|
||||
// Customization settings
|
||||
fieldMapping: FieldMapping;
|
||||
customStatuses: StatusConfig[];
|
||||
customPriorities: PriorityConfig[];
|
||||
}
|
||||
|
||||
// Default field mapping maintains backward compatibility
|
||||
export const DEFAULT_FIELD_MAPPING: FieldMapping = {
|
||||
title: 'title',
|
||||
status: 'status',
|
||||
priority: 'priority',
|
||||
due: 'due',
|
||||
contexts: 'contexts',
|
||||
timeEstimate: 'timeEstimate',
|
||||
timeSpent: 'timeSpent',
|
||||
completedDate: 'completedDate',
|
||||
dateCreated: 'dateCreated',
|
||||
dateModified: 'dateModified',
|
||||
recurrence: 'recurrence',
|
||||
archiveTag: 'archived'
|
||||
};
|
||||
|
||||
// Default status configuration matches current hardcoded behavior
|
||||
export const DEFAULT_STATUSES: StatusConfig[] = [
|
||||
{
|
||||
id: 'open',
|
||||
value: 'open',
|
||||
label: 'Open',
|
||||
color: '#808080',
|
||||
isCompleted: false,
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'in-progress',
|
||||
value: 'in-progress',
|
||||
label: 'In progress',
|
||||
color: '#0066cc',
|
||||
isCompleted: false,
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'done',
|
||||
value: 'done',
|
||||
label: 'Done',
|
||||
color: '#00aa00',
|
||||
isCompleted: true,
|
||||
order: 3
|
||||
}
|
||||
];
|
||||
|
||||
// Default priority configuration matches current hardcoded behavior
|
||||
export const DEFAULT_PRIORITIES: PriorityConfig[] = [
|
||||
{
|
||||
id: 'low',
|
||||
value: 'low',
|
||||
label: 'Low',
|
||||
color: '#00aa00',
|
||||
weight: 1
|
||||
},
|
||||
{
|
||||
id: 'normal',
|
||||
value: 'normal',
|
||||
label: 'Normal',
|
||||
color: '#ffaa00',
|
||||
weight: 2
|
||||
},
|
||||
{
|
||||
id: 'high',
|
||||
value: 'high',
|
||||
label: 'High',
|
||||
color: '#ff0000',
|
||||
weight: 3
|
||||
}
|
||||
];
|
||||
|
||||
export const DEFAULT_SETTINGS: TaskNotesSettings = {
|
||||
dailyNotesFolder: 'TaskNotes/Daily',
|
||||
tasksFolder: 'TaskNotes/Tasks',
|
||||
|
|
@ -48,11 +124,17 @@ export const DEFAULT_SETTINGS: TaskNotesSettings = {
|
|||
pomodoroAutoStartWork: false,
|
||||
pomodoroNotifications: true,
|
||||
pomodoroSoundEnabled: true,
|
||||
pomodoroSoundVolume: 50
|
||||
pomodoroSoundVolume: 50,
|
||||
// Customization defaults
|
||||
fieldMapping: DEFAULT_FIELD_MAPPING,
|
||||
customStatuses: DEFAULT_STATUSES,
|
||||
customPriorities: DEFAULT_PRIORITIES
|
||||
};
|
||||
|
||||
export class TaskNotesSettingTab extends PluginSettingTab {
|
||||
plugin: TaskNotesPlugin;
|
||||
private activeTab: string = 'general';
|
||||
private tabContents: Record<string, HTMLElement> = {};
|
||||
|
||||
constructor(app: App, plugin: TaskNotesPlugin) {
|
||||
super(app, plugin);
|
||||
|
|
@ -63,19 +145,87 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
// General Settings
|
||||
new Setting(containerEl)
|
||||
.setName('Daily notes folder')
|
||||
.setDesc('Folder where daily notes will be stored')
|
||||
.addText(text => text
|
||||
.setPlaceholder('TaskNotes/Daily')
|
||||
.setValue(this.plugin.settings.dailyNotesFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dailyNotesFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
// Create tab navigation
|
||||
const tabNav = containerEl.createDiv('settings-tab-nav');
|
||||
tabNav.style.display = 'flex';
|
||||
tabNav.style.borderBottom = '1px solid var(--background-modifier-border)';
|
||||
tabNav.style.marginBottom = '20px';
|
||||
|
||||
new Setting(containerEl)
|
||||
const tabs = [
|
||||
{ id: 'general', name: 'General' },
|
||||
{ id: 'field-mapping', name: 'Field mapping' },
|
||||
{ id: 'statuses', name: 'Statuses' },
|
||||
{ id: 'priorities', name: 'Priorities' },
|
||||
{ id: 'daily-notes', name: 'Daily notes' },
|
||||
{ id: 'pomodoro', name: 'Pomodoro' }
|
||||
];
|
||||
|
||||
tabs.forEach(tab => {
|
||||
const tabButton = tabNav.createEl('button', {
|
||||
text: tab.name,
|
||||
cls: this.activeTab === tab.id ? 'settings-tab-button active' : 'settings-tab-button'
|
||||
});
|
||||
|
||||
tabButton.style.padding = '8px 16px';
|
||||
tabButton.style.border = 'none';
|
||||
tabButton.style.background = this.activeTab === tab.id ? 'var(--interactive-accent)' : 'transparent';
|
||||
tabButton.style.color = this.activeTab === tab.id ? 'var(--text-on-accent)' : 'var(--text-normal)';
|
||||
tabButton.style.cursor = 'pointer';
|
||||
tabButton.style.borderRadius = '4px 4px 0 0';
|
||||
|
||||
tabButton.addEventListener('click', () => {
|
||||
this.switchTab(tab.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Create tab content containers
|
||||
const tabContentsEl = containerEl.createDiv('settings-tab-contents');
|
||||
|
||||
// Create all tab content containers
|
||||
tabs.forEach(tab => {
|
||||
const tabContent = tabContentsEl.createDiv('settings-tab-content');
|
||||
tabContent.style.display = this.activeTab === tab.id ? 'block' : 'none';
|
||||
this.tabContents[tab.id] = tabContent;
|
||||
});
|
||||
|
||||
this.renderActiveTab();
|
||||
}
|
||||
|
||||
private switchTab(tabId: string): void {
|
||||
this.activeTab = tabId;
|
||||
this.display(); // Re-render the entire settings tab
|
||||
}
|
||||
|
||||
private renderActiveTab(): void {
|
||||
// Clear current tab content
|
||||
Object.values(this.tabContents).forEach(content => content.empty());
|
||||
|
||||
switch (this.activeTab) {
|
||||
case 'general':
|
||||
this.renderGeneralTab();
|
||||
break;
|
||||
case 'field-mapping':
|
||||
this.renderFieldMappingTab();
|
||||
break;
|
||||
case 'statuses':
|
||||
this.renderStatusesTab();
|
||||
break;
|
||||
case 'priorities':
|
||||
this.renderPrioritiesTab();
|
||||
break;
|
||||
case 'daily-notes':
|
||||
this.renderDailyNotesTab();
|
||||
break;
|
||||
case 'pomodoro':
|
||||
this.renderPomodoroTab();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private renderGeneralTab(): void {
|
||||
const container = this.tabContents['general'];
|
||||
|
||||
new Setting(container)
|
||||
.setName('Default tasks folder')
|
||||
.setDesc('Default folder for new tasks (tasks are identified by tag, not folder)')
|
||||
.addText(text => text
|
||||
|
|
@ -86,32 +236,7 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Excluded folders')
|
||||
.setDesc('Comma-separated list of folder paths to exclude from Notes tab (e.g., "Templates,Archive,Attachments")')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Templates,Archive')
|
||||
.setValue(this.plugin.settings.excludedFolders)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.excludedFolders = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Daily note template')
|
||||
.setDesc('Path to template file for daily notes (leave empty to use built-in template). Supports Obsidian template variables like {{title}}, {{date}}, {{date:format}}, {{time}}, etc.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Templates/Daily Note Template.md')
|
||||
.setValue(this.plugin.settings.dailyNoteTemplate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dailyNoteTemplate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Task Settings
|
||||
new Setting(containerEl).setName('Task defaults').setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Task tag')
|
||||
.setDesc('Tag that identifies notes as tasks (without #)')
|
||||
.addText(text => text
|
||||
|
|
@ -122,36 +247,56 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default task priority')
|
||||
.setDesc('Default priority for new tasks')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('low', 'Low')
|
||||
.addOption('normal', 'Normal')
|
||||
.addOption('high', 'High')
|
||||
.setValue(this.plugin.settings.defaultTaskPriority)
|
||||
.onChange(async (value: any) => {
|
||||
this.plugin.settings.defaultTaskPriority = value;
|
||||
new Setting(container)
|
||||
.setName('Excluded folders')
|
||||
.setDesc('Comma-separated list of folder paths to exclude from Notes tab')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Templates,Archive')
|
||||
.setValue(this.plugin.settings.excludedFolders)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.excludedFolders = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
// Task defaults section
|
||||
new Setting(container).setName('Task defaults').setHeading();
|
||||
|
||||
new Setting(container)
|
||||
.setName('Default task status')
|
||||
.setDesc('Default status for new tasks')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('open', 'Open')
|
||||
.addOption('in-progress', 'In progress')
|
||||
.addOption('done', 'Done')
|
||||
.setValue(this.plugin.settings.defaultTaskStatus)
|
||||
.onChange(async (value: any) => {
|
||||
this.plugin.settings.defaultTaskStatus = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
.addDropdown(dropdown => {
|
||||
// Populate with custom statuses
|
||||
this.plugin.settings.customStatuses.forEach(status => {
|
||||
dropdown.addOption(status.value, status.label);
|
||||
});
|
||||
return dropdown
|
||||
.setValue(this.plugin.settings.defaultTaskStatus)
|
||||
.onChange(async (value: any) => {
|
||||
this.plugin.settings.defaultTaskStatus = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(container)
|
||||
.setName('Default task priority')
|
||||
.setDesc('Default priority for new tasks')
|
||||
.addDropdown(dropdown => {
|
||||
// Populate with custom priorities
|
||||
this.plugin.settings.customPriorities.forEach(priority => {
|
||||
dropdown.addOption(priority.value, priority.label);
|
||||
});
|
||||
return dropdown
|
||||
.setValue(this.plugin.settings.defaultTaskPriority)
|
||||
.onChange(async (value: any) => {
|
||||
this.plugin.settings.defaultTaskPriority = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Task filename settings
|
||||
new Setting(container).setName('Task filenames').setHeading();
|
||||
|
||||
// Task Filename Settings
|
||||
new Setting(containerEl).setName('Task filenames').setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Filename format')
|
||||
.setDesc('How task filenames should be generated')
|
||||
.addDropdown(dropdown => dropdown
|
||||
|
|
@ -163,28 +308,390 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
.onChange(async (value: any) => {
|
||||
this.plugin.settings.taskFilenameFormat = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.updateCustomTemplateVisibility();
|
||||
this.renderActiveTab(); // Re-render to update visibility
|
||||
}));
|
||||
|
||||
const customTemplateSetting = new Setting(containerEl)
|
||||
.setName('Custom filename template')
|
||||
.setDesc('Template for custom filenames. Available variables: {title}, {date}, {time}, {priority}, {status}, {timestamp}, {year}, {month}, {day}, {hour}, {minute}, {second}')
|
||||
if (this.plugin.settings.taskFilenameFormat === 'custom') {
|
||||
new Setting(container)
|
||||
.setName('Custom filename template')
|
||||
.setDesc('Template for custom filenames. Available variables: {title}, {date}, {time}, {priority}, {status}, {timestamp}, etc.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('{date}-{title}')
|
||||
.setValue(this.plugin.settings.customFilenameTemplate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.customFilenameTemplate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private renderFieldMappingTab(): void {
|
||||
const container = this.tabContents['field-mapping'];
|
||||
|
||||
// Warning message
|
||||
const warning = container.createDiv();
|
||||
warning.style.padding = '12px';
|
||||
warning.style.marginBottom = '20px';
|
||||
warning.style.backgroundColor = 'var(--background-modifier-warning)';
|
||||
warning.style.borderRadius = '4px';
|
||||
warning.innerHTML = `
|
||||
<strong>⚠️ Warning:</strong> TaskNotes will read AND write using these property names.
|
||||
Changing these after creating tasks may cause inconsistencies.
|
||||
`;
|
||||
|
||||
container.createEl('h3', { text: 'Field mapping' });
|
||||
container.createEl('p', {
|
||||
text: 'Configure which frontmatter properties TaskNotes should use for each field.'
|
||||
});
|
||||
|
||||
// Create mapping table
|
||||
const table = container.createEl('table');
|
||||
table.style.width = '100%';
|
||||
table.style.borderCollapse = 'collapse';
|
||||
|
||||
const header = table.createEl('tr');
|
||||
header.createEl('th', { text: 'TaskNotes field' });
|
||||
header.createEl('th', { text: 'Your property name' });
|
||||
|
||||
const fieldMappings: Array<[keyof FieldMapping, string]> = [
|
||||
['title', 'Title'],
|
||||
['status', 'Status'],
|
||||
['priority', 'Priority'],
|
||||
['due', 'Due date'],
|
||||
['contexts', 'Contexts'],
|
||||
['timeEstimate', 'Time estimate'],
|
||||
['timeSpent', 'Time spent'],
|
||||
['completedDate', 'Completed date'],
|
||||
['dateCreated', 'Created date'],
|
||||
['dateModified', 'Modified date'],
|
||||
['recurrence', 'Recurrence'],
|
||||
['archiveTag', 'Archive tag']
|
||||
];
|
||||
|
||||
fieldMappings.forEach(([field, label]) => {
|
||||
const row = table.createEl('tr');
|
||||
const labelCell = row.createEl('td');
|
||||
labelCell.style.padding = '8px';
|
||||
labelCell.style.borderBottom = '1px solid var(--background-modifier-border)';
|
||||
labelCell.textContent = label;
|
||||
|
||||
const inputCell = row.createEl('td');
|
||||
inputCell.style.padding = '8px';
|
||||
inputCell.style.borderBottom = '1px solid var(--background-modifier-border)';
|
||||
|
||||
const input = inputCell.createEl('input', {
|
||||
type: 'text',
|
||||
value: this.plugin.settings.fieldMapping[field]
|
||||
});
|
||||
input.style.width = '100%';
|
||||
input.style.padding = '4px';
|
||||
|
||||
input.addEventListener('change', async () => {
|
||||
this.plugin.settings.fieldMapping[field] = input.value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Reset button
|
||||
new Setting(container)
|
||||
.setName('Reset to defaults')
|
||||
.setDesc('Reset all field mappings to default values')
|
||||
.addButton(button => button
|
||||
.setButtonText('Reset')
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.fieldMapping = { ...DEFAULT_FIELD_MAPPING };
|
||||
await this.plugin.saveSettings();
|
||||
this.renderActiveTab();
|
||||
}));
|
||||
}
|
||||
|
||||
private renderStatusesTab(): void {
|
||||
const container = this.tabContents['statuses'];
|
||||
|
||||
container.createEl('h3', { text: 'Task Statuses' });
|
||||
container.createEl('p', {
|
||||
text: 'Define the statuses available for your tasks. The order determines the cycling sequence.'
|
||||
});
|
||||
|
||||
// Status list
|
||||
const statusList = container.createDiv();
|
||||
statusList.style.marginBottom = '20px';
|
||||
|
||||
this.renderStatusList(statusList);
|
||||
|
||||
// Add status button
|
||||
new Setting(container)
|
||||
.setName('Add new status')
|
||||
.addButton(button => button
|
||||
.setButtonText('Add Status')
|
||||
.onClick(async () => {
|
||||
const newStatus = StatusManager.createDefaultStatus(this.plugin.settings.customStatuses);
|
||||
this.plugin.settings.customStatuses.push(newStatus);
|
||||
await this.plugin.saveSettings();
|
||||
this.renderActiveTab();
|
||||
}));
|
||||
}
|
||||
|
||||
private renderStatusList(container: HTMLElement): void {
|
||||
container.empty();
|
||||
|
||||
const sortedStatuses = [...this.plugin.settings.customStatuses].sort((a, b) => a.order - b.order);
|
||||
|
||||
sortedStatuses.forEach((status, index) => {
|
||||
const statusRow = container.createDiv();
|
||||
statusRow.style.display = 'flex';
|
||||
statusRow.style.alignItems = 'center';
|
||||
statusRow.style.marginBottom = '12px';
|
||||
statusRow.style.padding = '12px';
|
||||
statusRow.style.border = '1px solid var(--background-modifier-border)';
|
||||
statusRow.style.borderRadius = '4px';
|
||||
|
||||
// Color indicator
|
||||
const colorIndicator = statusRow.createDiv();
|
||||
colorIndicator.style.width = '20px';
|
||||
colorIndicator.style.height = '20px';
|
||||
colorIndicator.style.borderRadius = '50%';
|
||||
colorIndicator.style.backgroundColor = status.color;
|
||||
colorIndicator.style.marginRight = '12px';
|
||||
colorIndicator.style.flexShrink = '0';
|
||||
|
||||
// Status value input
|
||||
const valueInput = statusRow.createEl('input', {
|
||||
type: 'text',
|
||||
value: status.value
|
||||
});
|
||||
valueInput.style.marginRight = '12px';
|
||||
valueInput.style.width = '120px';
|
||||
|
||||
// Status label input
|
||||
const labelInput = statusRow.createEl('input', {
|
||||
type: 'text',
|
||||
value: status.label
|
||||
});
|
||||
labelInput.style.marginRight = '12px';
|
||||
labelInput.style.width = '120px';
|
||||
|
||||
// Color input
|
||||
const colorInput = statusRow.createEl('input', {
|
||||
type: 'color',
|
||||
value: status.color
|
||||
});
|
||||
colorInput.style.marginRight = '12px';
|
||||
colorInput.style.width = '40px';
|
||||
|
||||
// Completed checkbox
|
||||
const completedLabel = statusRow.createEl('label');
|
||||
completedLabel.style.marginRight = '12px';
|
||||
completedLabel.style.display = 'flex';
|
||||
completedLabel.style.alignItems = 'center';
|
||||
|
||||
const completedCheckbox = completedLabel.createEl('input', {
|
||||
type: 'checkbox'
|
||||
});
|
||||
completedCheckbox.checked = status.isCompleted;
|
||||
completedCheckbox.style.marginRight = '4px';
|
||||
|
||||
completedLabel.createSpan({ text: 'Completed' });
|
||||
|
||||
// Delete button
|
||||
const deleteButton = statusRow.createEl('button', {
|
||||
text: 'Delete'
|
||||
});
|
||||
deleteButton.style.marginLeft = 'auto';
|
||||
deleteButton.style.backgroundColor = 'var(--interactive-accent-rgb)';
|
||||
deleteButton.style.color = 'var(--text-on-accent)';
|
||||
deleteButton.style.border = 'none';
|
||||
deleteButton.style.padding = '4px 8px';
|
||||
deleteButton.style.borderRadius = '4px';
|
||||
deleteButton.style.cursor = 'pointer';
|
||||
|
||||
// Event listeners
|
||||
const updateStatus = async () => {
|
||||
status.value = valueInput.value;
|
||||
status.label = labelInput.value;
|
||||
status.color = colorInput.value;
|
||||
status.isCompleted = completedCheckbox.checked;
|
||||
await this.plugin.saveSettings();
|
||||
colorIndicator.style.backgroundColor = status.color;
|
||||
};
|
||||
|
||||
valueInput.addEventListener('change', updateStatus);
|
||||
labelInput.addEventListener('change', updateStatus);
|
||||
colorInput.addEventListener('change', updateStatus);
|
||||
completedCheckbox.addEventListener('change', updateStatus);
|
||||
|
||||
deleteButton.addEventListener('click', async () => {
|
||||
if (this.plugin.settings.customStatuses.length <= 2) {
|
||||
alert('You must have at least 2 statuses');
|
||||
return;
|
||||
}
|
||||
|
||||
const statusIndex = this.plugin.settings.customStatuses.findIndex(s => s.id === status.id);
|
||||
if (statusIndex !== -1) {
|
||||
this.plugin.settings.customStatuses.splice(statusIndex, 1);
|
||||
await this.plugin.saveSettings();
|
||||
this.renderActiveTab();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private renderPrioritiesTab(): void {
|
||||
const container = this.tabContents['priorities'];
|
||||
|
||||
container.createEl('h3', { text: 'Task priorities' });
|
||||
container.createEl('p', {
|
||||
text: 'Define the priority levels for your tasks. Higher weight = higher priority.'
|
||||
});
|
||||
|
||||
// Priority list
|
||||
const priorityList = container.createDiv();
|
||||
priorityList.style.marginBottom = '20px';
|
||||
|
||||
this.renderPriorityList(priorityList);
|
||||
|
||||
// Add priority button
|
||||
new Setting(container)
|
||||
.setName('Add new priority')
|
||||
.addButton(button => button
|
||||
.setButtonText('Add Priority')
|
||||
.onClick(async () => {
|
||||
const newPriority = PriorityManager.createDefaultPriority(this.plugin.settings.customPriorities);
|
||||
this.plugin.settings.customPriorities.push(newPriority);
|
||||
await this.plugin.saveSettings();
|
||||
this.renderActiveTab();
|
||||
}));
|
||||
}
|
||||
|
||||
private renderPriorityList(container: HTMLElement): void {
|
||||
container.empty();
|
||||
|
||||
const sortedPriorities = [...this.plugin.settings.customPriorities].sort((a, b) => b.weight - a.weight);
|
||||
|
||||
sortedPriorities.forEach((priority, index) => {
|
||||
const priorityRow = container.createDiv();
|
||||
priorityRow.style.display = 'flex';
|
||||
priorityRow.style.alignItems = 'center';
|
||||
priorityRow.style.marginBottom = '12px';
|
||||
priorityRow.style.padding = '12px';
|
||||
priorityRow.style.border = '1px solid var(--background-modifier-border)';
|
||||
priorityRow.style.borderRadius = '4px';
|
||||
|
||||
// Color indicator
|
||||
const colorIndicator = priorityRow.createDiv();
|
||||
colorIndicator.style.width = '20px';
|
||||
colorIndicator.style.height = '20px';
|
||||
colorIndicator.style.borderRadius = '50%';
|
||||
colorIndicator.style.backgroundColor = priority.color;
|
||||
colorIndicator.style.marginRight = '12px';
|
||||
colorIndicator.style.flexShrink = '0';
|
||||
|
||||
// Priority value input
|
||||
const valueInput = priorityRow.createEl('input', {
|
||||
type: 'text',
|
||||
value: priority.value
|
||||
});
|
||||
valueInput.style.marginRight = '12px';
|
||||
valueInput.style.width = '120px';
|
||||
|
||||
// Priority label input
|
||||
const labelInput = priorityRow.createEl('input', {
|
||||
type: 'text',
|
||||
value: priority.label
|
||||
});
|
||||
labelInput.style.marginRight = '12px';
|
||||
labelInput.style.width = '120px';
|
||||
|
||||
// Color input
|
||||
const colorInput = priorityRow.createEl('input', {
|
||||
type: 'color',
|
||||
value: priority.color
|
||||
});
|
||||
colorInput.style.marginRight = '12px';
|
||||
colorInput.style.width = '40px';
|
||||
|
||||
// Weight input
|
||||
const weightInput = priorityRow.createEl('input', {
|
||||
type: 'number',
|
||||
value: priority.weight.toString()
|
||||
});
|
||||
weightInput.style.marginRight = '12px';
|
||||
weightInput.style.width = '80px';
|
||||
|
||||
// Delete button
|
||||
const deleteButton = priorityRow.createEl('button', {
|
||||
text: 'Delete'
|
||||
});
|
||||
deleteButton.style.marginLeft = 'auto';
|
||||
deleteButton.style.backgroundColor = 'var(--interactive-accent-rgb)';
|
||||
deleteButton.style.color = 'var(--text-on-accent)';
|
||||
deleteButton.style.border = 'none';
|
||||
deleteButton.style.padding = '4px 8px';
|
||||
deleteButton.style.borderRadius = '4px';
|
||||
deleteButton.style.cursor = 'pointer';
|
||||
|
||||
// Event listeners
|
||||
const updatePriority = async () => {
|
||||
priority.value = valueInput.value;
|
||||
priority.label = labelInput.value;
|
||||
priority.color = colorInput.value;
|
||||
priority.weight = parseInt(weightInput.value) || 0;
|
||||
await this.plugin.saveSettings();
|
||||
colorIndicator.style.backgroundColor = priority.color;
|
||||
};
|
||||
|
||||
valueInput.addEventListener('change', updatePriority);
|
||||
labelInput.addEventListener('change', updatePriority);
|
||||
colorInput.addEventListener('change', updatePriority);
|
||||
weightInput.addEventListener('change', updatePriority);
|
||||
|
||||
deleteButton.addEventListener('click', async () => {
|
||||
if (this.plugin.settings.customPriorities.length <= 1) {
|
||||
alert('You must have at least 1 priority');
|
||||
return;
|
||||
}
|
||||
|
||||
const priorityIndex = this.plugin.settings.customPriorities.findIndex(p => p.id === priority.id);
|
||||
if (priorityIndex !== -1) {
|
||||
this.plugin.settings.customPriorities.splice(priorityIndex, 1);
|
||||
await this.plugin.saveSettings();
|
||||
this.renderActiveTab();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private renderDailyNotesTab(): void {
|
||||
const container = this.tabContents['daily-notes'];
|
||||
|
||||
new Setting(container)
|
||||
.setName('Daily notes folder')
|
||||
.setDesc('Folder where daily notes will be stored')
|
||||
.addText(text => text
|
||||
.setPlaceholder('{date}-{title}')
|
||||
.setValue(this.plugin.settings.customFilenameTemplate)
|
||||
.setPlaceholder('TaskNotes/Daily')
|
||||
.setValue(this.plugin.settings.dailyNotesFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.customFilenameTemplate = value;
|
||||
this.plugin.settings.dailyNotesFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Store reference for visibility toggle
|
||||
(this as any).customTemplateSetting = customTemplateSetting;
|
||||
this.updateCustomTemplateVisibility();
|
||||
|
||||
// Pomodoro Settings
|
||||
new Setting(containerEl).setName('Pomodoro timer').setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Daily note template')
|
||||
.setDesc('Path to template file for daily notes (leave empty to use built-in template). Supports Obsidian template variables like {{title}}, {{date}}, {{date:format}}, {{time}}, etc.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Templates/Daily Note Template.md')
|
||||
.setValue(this.plugin.settings.dailyNoteTemplate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dailyNoteTemplate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
|
||||
private renderPomodoroTab(): void {
|
||||
const container = this.tabContents['pomodoro'];
|
||||
|
||||
new Setting(container)
|
||||
.setName('Work duration')
|
||||
.setDesc('Duration of work intervals in minutes')
|
||||
.addText(text => text
|
||||
|
|
@ -198,7 +705,7 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Short break duration')
|
||||
.setDesc('Duration of short breaks in minutes')
|
||||
.addText(text => text
|
||||
|
|
@ -212,7 +719,7 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Long break duration')
|
||||
.setDesc('Duration of long breaks in minutes')
|
||||
.addText(text => text
|
||||
|
|
@ -226,7 +733,7 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Long break interval')
|
||||
.setDesc('Take a long break after this many pomodoros')
|
||||
.addText(text => text
|
||||
|
|
@ -240,7 +747,7 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Auto-start breaks')
|
||||
.setDesc('Automatically start break timer after work session')
|
||||
.addToggle(toggle => toggle
|
||||
|
|
@ -250,7 +757,7 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Auto-start work')
|
||||
.setDesc('Automatically start work timer after break')
|
||||
.addToggle(toggle => toggle
|
||||
|
|
@ -260,7 +767,7 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Enable notifications')
|
||||
.setDesc('Show notifications when pomodoro sessions complete')
|
||||
.addToggle(toggle => toggle
|
||||
|
|
@ -270,7 +777,7 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Enable sound')
|
||||
.setDesc('Play sound when pomodoro sessions complete')
|
||||
.addToggle(toggle => toggle
|
||||
|
|
@ -280,7 +787,7 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('Sound volume')
|
||||
.setDesc('Volume for completion sounds (0-100)')
|
||||
.addSlider(slider => slider
|
||||
|
|
@ -292,15 +799,4 @@ export class TaskNotesSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
|
||||
private updateCustomTemplateVisibility() {
|
||||
const customSetting = (this as any).customTemplateSetting as Setting;
|
||||
if (customSetting) {
|
||||
if (this.plugin.settings.taskFilenameFormat === 'custom') {
|
||||
customSetting.settingEl.style.display = '';
|
||||
} else {
|
||||
customSetting.settingEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/types.ts
53
src/types.ts
|
|
@ -146,4 +146,57 @@ export interface PomodoroState {
|
|||
pomodorosCompleted: number; // today's count
|
||||
currentStreak: number; // consecutive pomodoros
|
||||
totalMinutesToday: number; // total focused minutes today
|
||||
}
|
||||
|
||||
// Field mapping and customization types
|
||||
export interface FieldMapping {
|
||||
title: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
due: string;
|
||||
contexts: string;
|
||||
timeEstimate: string;
|
||||
timeSpent: string;
|
||||
completedDate: string;
|
||||
dateCreated: string;
|
||||
dateModified: string;
|
||||
recurrence: string;
|
||||
archiveTag: string; // For the archive tag in the tags array
|
||||
}
|
||||
|
||||
export interface StatusConfig {
|
||||
id: string; // Unique identifier
|
||||
value: string; // What gets written to YAML
|
||||
label: string; // What displays in UI
|
||||
color: string; // Hex color for UI elements
|
||||
isCompleted: boolean; // Whether this counts as "done"
|
||||
order: number; // Sort order (for cycling)
|
||||
}
|
||||
|
||||
export interface PriorityConfig {
|
||||
id: string; // Unique identifier
|
||||
value: string; // What gets written to YAML
|
||||
label: string; // What displays in UI
|
||||
color: string; // Hex color for indicators
|
||||
weight: number; // For sorting (higher = more important)
|
||||
}
|
||||
|
||||
// Template configuration for quick setup
|
||||
export interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
config: {
|
||||
fieldMapping: Partial<FieldMapping>;
|
||||
customStatuses: StatusConfig[];
|
||||
customPriorities: PriorityConfig[];
|
||||
};
|
||||
}
|
||||
|
||||
// Configuration export/import
|
||||
export interface ExportedConfig {
|
||||
version: string;
|
||||
fieldMapping: FieldMapping;
|
||||
customStatuses: StatusConfig[];
|
||||
customPriorities: PriorityConfig[];
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { TFile, Vault } from 'obsidian';
|
||||
import { FileIndex, IndexedFile, TaskInfo, NoteInfo, FileEventHandlers } from '../types';
|
||||
import { extractNoteInfo, extractTaskInfo, debounce } from './helpers';
|
||||
import { FieldMapper } from '../services/FieldMapper';
|
||||
import * as YAML from 'yaml';
|
||||
import { YAMLCache } from './YAMLCache';
|
||||
|
||||
|
|
@ -13,11 +14,12 @@ export class FileIndexer {
|
|||
public excludedFolders: string[];
|
||||
private dailyNotesPath: string;
|
||||
private dailyNoteTemplatePath: string;
|
||||
private fieldMapper: FieldMapper | null = null;
|
||||
|
||||
// Store event handlers for cleanup
|
||||
private eventHandlers: FileEventHandlers = {};
|
||||
|
||||
constructor(vault: Vault, taskTag: string, excludedFolders: string = '', dailyNotesPath: string = '', dailyNoteTemplatePath: string = '') {
|
||||
constructor(vault: Vault, taskTag: string, excludedFolders: string = '', dailyNotesPath: string = '', dailyNoteTemplatePath: string = '', fieldMapper?: FieldMapper) {
|
||||
this.vault = vault;
|
||||
this.taskTag = taskTag;
|
||||
this.excludedFolders = excludedFolders
|
||||
|
|
@ -27,6 +29,7 @@ export class FileIndexer {
|
|||
// Normalize daily notes path by removing leading/trailing slashes
|
||||
this.dailyNotesPath = dailyNotesPath.replace(/^\/+|\/+$/g, '');
|
||||
this.dailyNoteTemplatePath = dailyNoteTemplatePath;
|
||||
this.fieldMapper = fieldMapper || null;
|
||||
|
||||
// Register event listeners for file changes
|
||||
this.registerFileEvents();
|
||||
|
|
@ -39,6 +42,13 @@ export class FileIndexer {
|
|||
this.dailyNoteTemplatePath = newPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the field mapper (used when settings change)
|
||||
*/
|
||||
updateFieldMapper(fieldMapper: FieldMapper) {
|
||||
this.fieldMapper = fieldMapper;
|
||||
}
|
||||
|
||||
private registerFileEvents() {
|
||||
// Create debounced versions of file operations
|
||||
const debouncedUpdate = debounce((file: TFile) => {
|
||||
|
|
@ -245,7 +255,7 @@ export class FileIndexer {
|
|||
|
||||
// Use cachedRead for better performance
|
||||
const content = await this.vault.cachedRead(file);
|
||||
const taskInfo = extractTaskInfo(content, indexedFile.path);
|
||||
const taskInfo = extractTaskInfo(content, indexedFile.path, this.fieldMapper || undefined);
|
||||
|
||||
if (taskInfo) {
|
||||
// Cache the result in the index
|
||||
|
|
@ -471,7 +481,7 @@ export class FileIndexer {
|
|||
if (!(fileObj instanceof TFile)) return null;
|
||||
|
||||
const content = await this.vault.cachedRead(fileObj);
|
||||
return extractTaskInfo(content, file.path);
|
||||
return extractTaskInfo(content, file.path, this.fieldMapper || undefined);
|
||||
} else {
|
||||
return file.cachedInfo as TaskInfo;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { TaskNotesSettings } from '../settings/settings';
|
|||
|
||||
export interface FilenameContext {
|
||||
title: string;
|
||||
priority: 'low' | 'normal' | 'high';
|
||||
status: 'open' | 'in-progress' | 'done';
|
||||
priority: string;
|
||||
status: string;
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { normalizePath, TFile, Vault } from 'obsidian';
|
||||
import { format } from 'date-fns';
|
||||
import { TimeInfo } from '../types';
|
||||
import { TimeInfo, TaskInfo } from '../types';
|
||||
import * as YAML from 'yaml';
|
||||
import { YAMLCache } from './YAMLCache';
|
||||
import { FieldMapper } from '../services/FieldMapper';
|
||||
|
||||
/**
|
||||
* Creates a debounced version of a function
|
||||
|
|
@ -112,6 +113,66 @@ export function updateYamlFrontmatter<T = any>(content: string, key: string, upd
|
|||
return `---\n${updatedFrontmatter}---${restOfContent}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates task properties in content using field mapping
|
||||
*/
|
||||
export function updateTaskProperty(
|
||||
content: string,
|
||||
propertyUpdates: Partial<TaskInfo>,
|
||||
fieldMapper?: FieldMapper
|
||||
): string {
|
||||
// Check if the content has YAML frontmatter
|
||||
if (!content.startsWith('---')) {
|
||||
// If not, create new frontmatter
|
||||
const yamlObj: Record<string, any> = {};
|
||||
|
||||
if (fieldMapper) {
|
||||
// Use field mapper to create frontmatter
|
||||
const mappedFrontmatter = fieldMapper.mapToFrontmatter(propertyUpdates);
|
||||
Object.assign(yamlObj, mappedFrontmatter);
|
||||
} else {
|
||||
// Use legacy field names
|
||||
Object.assign(yamlObj, propertyUpdates);
|
||||
}
|
||||
|
||||
const yamlStr = YAML.stringify(yamlObj);
|
||||
return `---\n${yamlStr}---\n\n${content}`;
|
||||
}
|
||||
|
||||
// Find the end of the frontmatter
|
||||
const endOfFrontmatter = content.indexOf('---', 3);
|
||||
if (endOfFrontmatter === -1) return content;
|
||||
|
||||
// Extract the frontmatter
|
||||
const frontmatter = content.substring(3, endOfFrontmatter);
|
||||
const restOfContent = content.substring(endOfFrontmatter + 3); // Skip the closing ---
|
||||
|
||||
// Parse the frontmatter
|
||||
let yamlObj: Record<string, any> = {};
|
||||
try {
|
||||
yamlObj = YAML.parse(frontmatter) || {};
|
||||
} catch (e) {
|
||||
console.error('Error parsing YAML frontmatter:', e);
|
||||
return content;
|
||||
}
|
||||
|
||||
// Update properties
|
||||
if (fieldMapper) {
|
||||
// Use field mapper to update properties
|
||||
const mappedUpdates = fieldMapper.mapToFrontmatter(propertyUpdates);
|
||||
Object.assign(yamlObj, mappedUpdates);
|
||||
} else {
|
||||
// Use legacy field names
|
||||
Object.assign(yamlObj, propertyUpdates);
|
||||
}
|
||||
|
||||
// Stringify the frontmatter
|
||||
const updatedFrontmatter = YAML.stringify(yamlObj);
|
||||
|
||||
// Return the updated content
|
||||
return `---\n${updatedFrontmatter}---${restOfContent}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a daily note template
|
||||
*/
|
||||
|
|
@ -242,34 +303,13 @@ export function isSameDay(date1: Date, date2: Date): boolean {
|
|||
}
|
||||
|
||||
/**
|
||||
* Extracts task information from a task file's content
|
||||
* Extracts task information from a task file's content using field mapping
|
||||
*/
|
||||
export function extractTaskInfo(content: string, path: string): {
|
||||
title: string,
|
||||
status: string,
|
||||
priority: string,
|
||||
due?: string,
|
||||
export function extractTaskInfo(
|
||||
content: string,
|
||||
path: string,
|
||||
archived: boolean,
|
||||
tags?: string[],
|
||||
contexts?: string[],
|
||||
recurrence?: {
|
||||
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly',
|
||||
days_of_week?: string[],
|
||||
day_of_month?: number,
|
||||
month_of_year?: number
|
||||
},
|
||||
complete_instances?: string[],
|
||||
completedDate?: string,
|
||||
timeEstimate?: number,
|
||||
timeSpent?: number,
|
||||
timeEntries?: {
|
||||
startTime: string,
|
||||
endTime?: string,
|
||||
duration?: number,
|
||||
description?: string
|
||||
}[]
|
||||
} | null {
|
||||
fieldMapper?: FieldMapper
|
||||
): TaskInfo | null {
|
||||
// Try to extract task info from frontmatter
|
||||
if (content.startsWith('---')) {
|
||||
const endOfFrontmatter = content.indexOf('---', 3);
|
||||
|
|
@ -278,51 +318,76 @@ export function extractTaskInfo(content: string, path: string): {
|
|||
const yaml = YAMLCache.extractFrontmatter(content, path);
|
||||
|
||||
if (yaml) {
|
||||
// Check if task has archive tag
|
||||
const tags = yaml.tags || [];
|
||||
const archived = Array.isArray(tags) && tags.includes('archive');
|
||||
|
||||
// Extract recurrence info if present
|
||||
let recurrence = undefined;
|
||||
if (yaml.recurrence && typeof yaml.recurrence === 'object') {
|
||||
recurrence = {
|
||||
frequency: yaml.recurrence.frequency,
|
||||
days_of_week: yaml.recurrence.days_of_week,
|
||||
day_of_month: yaml.recurrence.day_of_month,
|
||||
month_of_year: yaml.recurrence.month_of_year
|
||||
if (fieldMapper) {
|
||||
// Use field mapper to extract task info
|
||||
const mappedTask = fieldMapper.mapFromFrontmatter(yaml, path);
|
||||
|
||||
// Ensure required fields have defaults
|
||||
const taskInfo: TaskInfo = {
|
||||
title: mappedTask.title || 'Untitled Task',
|
||||
status: mappedTask.status || 'open',
|
||||
priority: mappedTask.priority || 'normal',
|
||||
due: mappedTask.due,
|
||||
path,
|
||||
archived: mappedTask.archived || false,
|
||||
tags: mappedTask.tags || [],
|
||||
contexts: mappedTask.contexts || [],
|
||||
recurrence: mappedTask.recurrence,
|
||||
complete_instances: mappedTask.complete_instances,
|
||||
completedDate: mappedTask.completedDate,
|
||||
timeEstimate: mappedTask.timeEstimate,
|
||||
timeSpent: mappedTask.timeSpent,
|
||||
timeEntries: mappedTask.timeEntries
|
||||
};
|
||||
|
||||
return taskInfo;
|
||||
} else {
|
||||
// Fallback to legacy field names for backward compatibility
|
||||
const tags = yaml.tags || [];
|
||||
const archived = Array.isArray(tags) && tags.includes('archived');
|
||||
|
||||
// Extract recurrence info if present
|
||||
let recurrence = undefined;
|
||||
if (yaml.recurrence && typeof yaml.recurrence === 'object') {
|
||||
recurrence = {
|
||||
frequency: yaml.recurrence.frequency,
|
||||
days_of_week: yaml.recurrence.days_of_week,
|
||||
day_of_month: yaml.recurrence.day_of_month,
|
||||
month_of_year: yaml.recurrence.month_of_year
|
||||
};
|
||||
}
|
||||
|
||||
// Extract complete_instances array if present
|
||||
let complete_instances = undefined;
|
||||
if (yaml.complete_instances && Array.isArray(yaml.complete_instances)) {
|
||||
complete_instances = yaml.complete_instances;
|
||||
}
|
||||
|
||||
// Extract contexts
|
||||
const contexts = yaml.contexts || [];
|
||||
|
||||
return {
|
||||
title: yaml.title || 'Untitled Task',
|
||||
status: yaml.status || 'open',
|
||||
priority: yaml.priority || 'normal',
|
||||
due: yaml.due,
|
||||
path,
|
||||
archived,
|
||||
tags: Array.isArray(tags) ? [...tags] : [],
|
||||
contexts: Array.isArray(contexts) ? [...contexts] : [],
|
||||
recurrence,
|
||||
complete_instances,
|
||||
completedDate: yaml.completedDate,
|
||||
timeEstimate: yaml.timeEstimate,
|
||||
timeSpent: yaml.timeSpent,
|
||||
timeEntries: yaml.timeEntries?.map((entry: any) => ({
|
||||
start: entry.start || entry.startTime,
|
||||
end: entry.end || entry.endTime,
|
||||
duration: entry.duration,
|
||||
description: entry.description
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
// Extract complete_instances array if present
|
||||
let complete_instances = undefined;
|
||||
if (yaml.complete_instances && Array.isArray(yaml.complete_instances)) {
|
||||
complete_instances = yaml.complete_instances;
|
||||
}
|
||||
|
||||
// Extract contexts
|
||||
const contexts = yaml.contexts || [];
|
||||
|
||||
return {
|
||||
title: yaml.title || 'Untitled Task',
|
||||
status: yaml.status || 'open',
|
||||
priority: yaml.priority || 'normal',
|
||||
due: yaml.due,
|
||||
path,
|
||||
archived,
|
||||
tags: Array.isArray(tags) ? [...tags] : [],
|
||||
contexts: Array.isArray(contexts) ? [...contexts] : [],
|
||||
recurrence,
|
||||
complete_instances,
|
||||
completedDate: yaml.completedDate,
|
||||
timeEstimate: yaml.timeEstimate,
|
||||
timeSpent: yaml.timeSpent,
|
||||
timeEntries: yaml.timeEntries?.map((entry: any) => ({
|
||||
start: entry.start || entry.startTime,
|
||||
end: entry.end || entry.endTime,
|
||||
duration: entry.duration,
|
||||
description: entry.description
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -202,9 +202,29 @@ export class TaskListView extends ItemView {
|
|||
statusFilter.createEl('span', { text: 'Status: ' });
|
||||
const statusSelect = statusFilter.createEl('select', { cls: 'status-select' });
|
||||
|
||||
const statuses = ['All', 'Open', 'In progress', 'Done', 'Archived'];
|
||||
statuses.forEach(status => {
|
||||
const option = statusSelect.createEl('option', { value: status.toLowerCase(), text: status });
|
||||
// Create status options from custom statuses
|
||||
const statusOptions = ['All', 'Open (All non-completed)', 'Archived'];
|
||||
|
||||
// Add custom statuses
|
||||
this.plugin.statusManager.getAllStatuses().forEach(status => {
|
||||
statusOptions.splice(-1, 0, status.label); // Insert before 'Archived'
|
||||
});
|
||||
|
||||
statusOptions.forEach(statusText => {
|
||||
let value: string;
|
||||
if (statusText === 'All') {
|
||||
value = 'all';
|
||||
} else if (statusText === 'Open (All non-completed)') {
|
||||
value = 'open';
|
||||
} else if (statusText === 'Archived') {
|
||||
value = 'archived';
|
||||
} else {
|
||||
// Find the status config that matches this label
|
||||
const statusConfig = this.plugin.statusManager.getAllStatuses().find(s => s.label === statusText);
|
||||
value = statusConfig ? statusConfig.value : statusText.toLowerCase();
|
||||
}
|
||||
|
||||
const option = statusSelect.createEl('option', { value, text: statusText });
|
||||
});
|
||||
|
||||
// Context filter (moved to collapsible section)
|
||||
|
|
@ -406,17 +426,30 @@ export class TaskListView extends ItemView {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a safe CSS class name from a value
|
||||
*/
|
||||
private getSafeCSSClass(prefix: string, value: string): string {
|
||||
return `${prefix}-${value.replace(/[^a-zA-Z0-9-]/g, '-')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format group name for display
|
||||
*/
|
||||
private formatGroupName(groupName: string): string {
|
||||
// Check if it's a priority value
|
||||
const priorityConfig = this.plugin.priorityManager.getPriorityConfig(groupName);
|
||||
if (priorityConfig) {
|
||||
return `${priorityConfig.label} priority`;
|
||||
}
|
||||
|
||||
// Check if it's a status value
|
||||
const statusConfig = this.plugin.statusManager.getStatusConfig(groupName);
|
||||
if (statusConfig) {
|
||||
return statusConfig.label;
|
||||
}
|
||||
|
||||
switch (groupName) {
|
||||
case 'high':
|
||||
return 'High priority';
|
||||
case 'normal':
|
||||
return 'Normal priority';
|
||||
case 'low':
|
||||
return 'Low priority';
|
||||
case 'all':
|
||||
return 'All tasks';
|
||||
default:
|
||||
|
|
@ -445,14 +478,24 @@ export class TaskListView extends ItemView {
|
|||
? getEffectiveTaskStatus(task, this.plugin.selectedDate)
|
||||
: task.status;
|
||||
|
||||
const priorityClass = this.getSafeCSSClass('priority', task.priority);
|
||||
const taskItem = container.createDiv({
|
||||
cls: `task-item priority-${task.priority} ${isDueOnSelectedDate ? 'task-due-today' : ''} ${task.archived ? 'task-archived' : ''} ${task.recurrence ? 'task-recurring' : ''} tasknotes-card`
|
||||
cls: `task-item ${priorityClass} ${isDueOnSelectedDate ? 'task-due-today' : ''} ${task.archived ? 'task-archived' : ''} ${task.recurrence ? 'task-recurring' : ''} tasknotes-card`
|
||||
});
|
||||
|
||||
// Store reference to this task element for future updates
|
||||
taskItem.dataset.taskPath = task.path;
|
||||
this.taskElements.set(task.path, taskItem);
|
||||
|
||||
// Get priority configuration for styling and interactions
|
||||
const priorityConfig = this.plugin.priorityManager.getPriorityConfig(task.priority);
|
||||
|
||||
// Apply priority color for hover border styling
|
||||
if (priorityConfig) {
|
||||
taskItem.dataset.priorityColor = priorityConfig.color;
|
||||
taskItem.style.setProperty('--current-priority-color', priorityConfig.color);
|
||||
}
|
||||
|
||||
// Create header row (title and metadata)
|
||||
const taskHeader = taskItem.createDiv({ cls: 'task-header tasknotes-card-header' });
|
||||
|
||||
|
|
@ -463,15 +506,22 @@ export class TaskListView extends ItemView {
|
|||
const titleContainer = taskInfo.createDiv({ cls: 'task-title-container' });
|
||||
|
||||
// Priority indicator - clickable
|
||||
const priorityLabel = priorityConfig?.label || task.priority;
|
||||
|
||||
const priorityIndicator = titleContainer.createEl('button', {
|
||||
cls: `task-priority-indicator priority-${task.priority} clickable`,
|
||||
cls: `task-priority-indicator ${priorityClass} clickable`,
|
||||
attr: {
|
||||
'aria-label': `Priority: ${task.priority}. Click to change`,
|
||||
'aria-label': `Priority: ${priorityLabel}. Click to change`,
|
||||
'title': 'Click to change priority',
|
||||
'type': 'button'
|
||||
}
|
||||
});
|
||||
|
||||
// Apply custom priority color
|
||||
if (priorityConfig) {
|
||||
priorityIndicator.style.backgroundColor = priorityConfig.color;
|
||||
}
|
||||
|
||||
// Add click handler for priority indicator
|
||||
priorityIndicator.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -479,13 +529,11 @@ export class TaskListView extends ItemView {
|
|||
// Get fresh task data from cache
|
||||
const currentTask = this.getTaskFromCache(task.path) || task;
|
||||
|
||||
// Cycle through priorities: high -> normal -> low -> high
|
||||
const priorityCycle = ['high', 'normal', 'low'];
|
||||
const currentIndex = priorityCycle.indexOf(currentTask.priority);
|
||||
const nextPriority = priorityCycle[(currentIndex + 1) % priorityCycle.length];
|
||||
// Use PriorityManager to get next priority
|
||||
const nextPriority = this.plugin.priorityManager.getNextPriority(currentTask.priority);
|
||||
|
||||
const originalPriority = currentTask.priority;
|
||||
const updatedTask = { ...currentTask, priority: nextPriority as 'low' | 'normal' | 'high' };
|
||||
const updatedTask = { ...currentTask, priority: nextPriority };
|
||||
|
||||
// Optimistic UI update
|
||||
this.updateTaskElementInDOM(currentTask.path, updatedTask);
|
||||
|
|
@ -659,17 +707,26 @@ export class TaskListView extends ItemView {
|
|||
});
|
||||
|
||||
// Status badge - clickable for non-recurring tasks (in footer)
|
||||
const statusClass = this.getSafeCSSClass('task-status', effectiveStatus);
|
||||
const statusConfig = this.plugin.statusManager.getStatusConfig(effectiveStatus);
|
||||
const statusLabel = statusConfig?.label || effectiveStatus;
|
||||
|
||||
const statusBadge = dueDateSection.createEl('button', {
|
||||
cls: `task-status task-status-${effectiveStatus.replace(/\s+/g, '-').toLowerCase()} ${task.recurrence ? 'recurring-status' : ''} ${!task.recurrence ? 'clickable' : ''}`,
|
||||
text: effectiveStatus,
|
||||
cls: `task-status ${statusClass} ${task.recurrence ? 'recurring-status' : ''} ${!task.recurrence ? 'clickable' : ''}`,
|
||||
text: statusLabel,
|
||||
attr: {
|
||||
'aria-label': `Change status: ${effectiveStatus}`,
|
||||
'aria-label': `Change status: ${statusLabel}`,
|
||||
'title': task.recurrence ? `Status for ${format(this.plugin.selectedDate, 'MMMM d, yyyy')}` : 'Click to change status',
|
||||
'type': 'button',
|
||||
'disabled': task.recurrence ? 'true' : null
|
||||
}
|
||||
});
|
||||
|
||||
// Apply custom status color
|
||||
if (statusConfig) {
|
||||
statusBadge.style.backgroundColor = statusConfig.color;
|
||||
}
|
||||
|
||||
// Add click handler for status badge (non-recurring tasks only)
|
||||
if (!task.recurrence) {
|
||||
statusBadge.addEventListener('click', async (e) => {
|
||||
|
|
@ -678,16 +735,14 @@ export class TaskListView extends ItemView {
|
|||
// Get fresh task data from cache
|
||||
const currentTask = this.getTaskFromCache(task.path) || task;
|
||||
|
||||
// Cycle through statuses: open -> in-progress -> done -> open
|
||||
const statusCycle = ['open', 'in-progress', 'done'];
|
||||
const currentIndex = statusCycle.indexOf(currentTask.status.toLowerCase());
|
||||
const nextStatus = statusCycle[(currentIndex + 1) % statusCycle.length];
|
||||
// Use StatusManager to get next status
|
||||
const nextStatus = this.plugin.statusManager.getNextStatus(currentTask.status);
|
||||
|
||||
const originalStatus = currentTask.status;
|
||||
const updatedTask = { ...currentTask, status: nextStatus };
|
||||
|
||||
// Update completedDate
|
||||
if (nextStatus === 'done') {
|
||||
// Update completedDate based on whether new status is completed
|
||||
if (this.plugin.statusManager.isCompletedStatus(nextStatus)) {
|
||||
updatedTask.completedDate = format(new Date(), 'yyyy-MM-dd');
|
||||
} else {
|
||||
updatedTask.completedDate = undefined;
|
||||
|
|
@ -744,12 +799,13 @@ export class TaskListView extends ItemView {
|
|||
if (task.recurrence) {
|
||||
const recurringSection = taskFooter.createDiv({ cls: 'recurring-section' });
|
||||
|
||||
const isCompleted = this.plugin.statusManager.isCompletedStatus(effectiveStatus);
|
||||
const toggleButton = recurringSection.createEl('button', {
|
||||
cls: `task-toggle-button ${effectiveStatus === 'done' ? 'mark-incomplete' : 'mark-complete'}`,
|
||||
text: effectiveStatus === 'done' ? 'Mark incomplete' : 'Mark complete',
|
||||
cls: `task-toggle-button ${isCompleted ? 'mark-incomplete' : 'mark-complete'}`,
|
||||
text: isCompleted ? 'Mark incomplete' : 'Mark complete',
|
||||
attr: {
|
||||
'aria-label': `${effectiveStatus === 'done' ? 'Mark task incomplete' : 'Mark task complete'} for ${format(this.plugin.selectedDate, 'MMMM d, yyyy')}`,
|
||||
'aria-pressed': (effectiveStatus === 'done').toString()
|
||||
'aria-label': `${isCompleted ? 'Mark task incomplete' : 'Mark task complete'} for ${format(this.plugin.selectedDate, 'MMMM d, yyyy')}`,
|
||||
'aria-pressed': isCompleted.toString()
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -766,7 +822,7 @@ export class TaskListView extends ItemView {
|
|||
: currentTask.status;
|
||||
|
||||
try {
|
||||
const currentlyComplete = currentEffectiveStatus === 'done';
|
||||
const currentlyComplete = this.plugin.statusManager.isCompletedStatus(currentEffectiveStatus);
|
||||
const newComplete = !currentlyComplete;
|
||||
|
||||
const selectedDateStr = format(this.plugin.selectedDate, 'yyyy-MM-dd');
|
||||
|
|
@ -841,8 +897,8 @@ export class TaskListView extends ItemView {
|
|||
// Sort with tasks due on selected date first, then by normal sort criteria
|
||||
tasks.sort((a, b) => {
|
||||
// First, put completed tasks at the bottom
|
||||
const aIsDone = a.status.toLowerCase() === 'done';
|
||||
const bIsDone = b.status.toLowerCase() === 'done';
|
||||
const aIsDone = this.plugin.statusManager.isCompletedStatus(a.status);
|
||||
const bIsDone = this.plugin.statusManager.isCompletedStatus(b.status);
|
||||
|
||||
if (aIsDone && !bIsDone) return 1;
|
||||
if (!aIsDone && bIsDone) return -1;
|
||||
|
|
@ -863,9 +919,8 @@ export class TaskListView extends ItemView {
|
|||
if (a.due && !b.due) return -1;
|
||||
if (!a.due && b.due) return 1;
|
||||
|
||||
// Then sort by priority
|
||||
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
||||
return priorityOrder[a.priority as keyof typeof priorityOrder] - priorityOrder[b.priority as keyof typeof priorityOrder];
|
||||
// Then sort by priority using PriorityManager
|
||||
return this.plugin.priorityManager.comparePriorities(a.priority, b.priority);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -902,9 +957,8 @@ export class TaskListView extends ItemView {
|
|||
if (a.due && !b.due) return -1;
|
||||
if (!a.due && b.due) return 1;
|
||||
|
||||
// Then sort by priority
|
||||
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
||||
return priorityOrder[a.priority as keyof typeof priorityOrder] - priorityOrder[b.priority as keyof typeof priorityOrder];
|
||||
// Then sort by priority using PriorityManager
|
||||
return this.plugin.priorityManager.comparePriorities(a.priority, b.priority);
|
||||
});
|
||||
|
||||
// Update cache and timestamp - we need a fresh cache
|
||||
|
|
@ -940,8 +994,10 @@ export class TaskListView extends ItemView {
|
|||
|
||||
tasks.forEach(task => {
|
||||
if (task.contexts) {
|
||||
task.contexts.forEach(context => {
|
||||
if (context.trim()) {
|
||||
// Ensure contexts is always treated as an array
|
||||
const contextsArray = Array.isArray(task.contexts) ? task.contexts : [task.contexts];
|
||||
contextsArray.forEach(context => {
|
||||
if (typeof context === 'string' && context.trim()) {
|
||||
contextsSet.add(context.trim());
|
||||
}
|
||||
});
|
||||
|
|
@ -1038,22 +1094,34 @@ export class TaskListView extends ItemView {
|
|||
? getEffectiveTaskStatus(updatedTask, this.plugin.selectedDate)
|
||||
: updatedTask.status;
|
||||
|
||||
// Format display text properly
|
||||
let displayStatus = effectiveStatus;
|
||||
if (effectiveStatus === 'in-progress') {
|
||||
displayStatus = 'In progress';
|
||||
} else {
|
||||
displayStatus = effectiveStatus.charAt(0).toUpperCase() + effectiveStatus.slice(1);
|
||||
}
|
||||
const statusClass = this.getSafeCSSClass('task-status', effectiveStatus);
|
||||
const statusConfig = this.plugin.statusManager.getStatusConfig(effectiveStatus);
|
||||
const statusLabel = statusConfig?.label || effectiveStatus;
|
||||
|
||||
statusBadge.className = `task-status task-status-${effectiveStatus.replace(/\s+/g, '-').toLowerCase()} ${updatedTask.recurrence ? 'recurring-status' : ''} ${!updatedTask.recurrence ? 'clickable' : ''}`;
|
||||
statusBadge.textContent = displayStatus;
|
||||
statusBadge.className = `task-status ${statusClass} ${updatedTask.recurrence ? 'recurring-status' : ''} ${!updatedTask.recurrence ? 'clickable' : ''}`;
|
||||
statusBadge.textContent = statusLabel;
|
||||
|
||||
// Apply custom status color
|
||||
if (statusConfig) {
|
||||
statusBadge.style.backgroundColor = statusConfig.color;
|
||||
}
|
||||
}
|
||||
|
||||
// Update priority indicator
|
||||
const priorityIndicator = taskElement.querySelector('.task-priority-indicator') as HTMLElement;
|
||||
if (priorityIndicator) {
|
||||
priorityIndicator.className = `task-priority-indicator priority-${updatedTask.priority} clickable`;
|
||||
const priorityClass = this.getSafeCSSClass('priority', updatedTask.priority);
|
||||
const priorityConfig = this.plugin.priorityManager.getPriorityConfig(updatedTask.priority);
|
||||
priorityIndicator.className = `task-priority-indicator ${priorityClass} clickable`;
|
||||
|
||||
// Apply custom priority color
|
||||
if (priorityConfig) {
|
||||
priorityIndicator.style.backgroundColor = priorityConfig.color;
|
||||
|
||||
// Update hover border styling on the task item
|
||||
taskElement.dataset.priorityColor = priorityConfig.color;
|
||||
taskElement.style.setProperty('--current-priority-color', priorityConfig.color);
|
||||
}
|
||||
}
|
||||
|
||||
// Update archive status
|
||||
|
|
@ -1148,7 +1216,8 @@ export class TaskListView extends ItemView {
|
|||
(updatedTask.recurrence && isRecurringTaskDueOn(updatedTask, this.plugin.selectedDate))
|
||||
);
|
||||
|
||||
taskElement.className = `task-item priority-${updatedTask.priority} ${isDueOnSelectedDate ? 'task-due-today' : ''} ${updatedTask.archived ? 'task-archived' : ''} ${updatedTask.recurrence ? 'task-recurring' : ''} tasknotes-card`;
|
||||
const priorityClass = this.getSafeCSSClass('priority', updatedTask.priority);
|
||||
taskElement.className = `task-item ${priorityClass} ${isDueOnSelectedDate ? 'task-due-today' : ''} ${updatedTask.archived ? 'task-archived' : ''} ${updatedTask.recurrence ? 'task-recurring' : ''} tasknotes-card`;
|
||||
|
||||
// Add visual feedback for the update
|
||||
taskElement.classList.add('task-updated');
|
||||
|
|
@ -1185,10 +1254,7 @@ export class TaskListView extends ItemView {
|
|||
const sortedTasks = [...tasks].sort((a, b) => {
|
||||
switch (this.sortKey) {
|
||||
case 'priority':
|
||||
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
||||
const aPriority = priorityOrder[a.priority as keyof typeof priorityOrder] ?? 1;
|
||||
const bPriority = priorityOrder[b.priority as keyof typeof priorityOrder] ?? 1;
|
||||
return aPriority - bPriority;
|
||||
return this.plugin.priorityManager.comparePriorities(a.priority, b.priority);
|
||||
|
||||
case 'title':
|
||||
return a.title.localeCompare(b.title);
|
||||
|
|
@ -1248,13 +1314,17 @@ export class TaskListView extends ItemView {
|
|||
|
||||
case 'context':
|
||||
if (task.contexts && task.contexts.length > 0) {
|
||||
// Ensure contexts is an array
|
||||
const contextsArray = Array.isArray(task.contexts) ? task.contexts : [task.contexts];
|
||||
// For tasks with multiple contexts, create separate entries
|
||||
task.contexts.forEach(context => {
|
||||
const contextKey = context.trim();
|
||||
if (!grouped.has(contextKey)) {
|
||||
grouped.set(contextKey, []);
|
||||
contextsArray.forEach(context => {
|
||||
if (typeof context === 'string') {
|
||||
const contextKey = context.trim();
|
||||
if (!grouped.has(contextKey)) {
|
||||
grouped.set(contextKey, []);
|
||||
}
|
||||
grouped.get(contextKey)!.push(task);
|
||||
}
|
||||
grouped.get(contextKey)!.push(task);
|
||||
});
|
||||
return; // Skip the default grouping below
|
||||
} else {
|
||||
|
|
@ -1316,9 +1386,15 @@ export class TaskListView extends ItemView {
|
|||
|
||||
if (selectedStatus === 'all') {
|
||||
filteredTasks = nonArchivedTasks;
|
||||
} else {
|
||||
} else if (selectedStatus === 'open') {
|
||||
// Show all non-completed tasks
|
||||
filteredTasks = nonArchivedTasks.filter(task =>
|
||||
task.status.toLowerCase() === selectedStatus
|
||||
!this.plugin.statusManager.isCompletedStatus(task.status)
|
||||
);
|
||||
} else {
|
||||
// Show tasks with specific status value
|
||||
filteredTasks = nonArchivedTasks.filter(task =>
|
||||
task.status === selectedStatus
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1330,9 +1406,12 @@ export class TaskListView extends ItemView {
|
|||
return false; // Task has no contexts, exclude it
|
||||
}
|
||||
|
||||
// Ensure contexts is an array
|
||||
const contextsArray = Array.isArray(task.contexts) ? task.contexts : [task.contexts];
|
||||
|
||||
// Check if task has any of the selected contexts
|
||||
return task.contexts.some(context =>
|
||||
this.selectedContexts.has(context.trim())
|
||||
return contextsArray.some(context =>
|
||||
typeof context === 'string' && this.selectedContexts.has(context.trim())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1253,6 +1253,11 @@ button.processing::after {
|
|||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
/* Priority-based top border on hover - for custom priority colors */
|
||||
.task-item[data-priority-color]:hover {
|
||||
border-top: 3px solid var(--current-priority-color, var(--interactive-accent));
|
||||
}
|
||||
|
||||
/* Task update animation */
|
||||
.task-item.task-updated {
|
||||
animation: taskUpdate 1.5s ease;
|
||||
|
|
|
|||
Loading…
Reference in a new issue