diff --git a/src/components/StatusComponent.ts b/src/components/StatusComponent.ts index f9ef1ed2..c4e20abb 100644 --- a/src/components/StatusComponent.ts +++ b/src/components/StatusComponent.ts @@ -70,7 +70,7 @@ export class StatusComponent extends Component { // Create checkbox-like element for the status interactiveElement = createTaskCheckbox( status.text, - this.task, + { ...this.task, status: status.text } as any, statusEl ); } diff --git a/src/components/settings/FileSourceSettings.ts b/src/components/settings/FileSourceSettings.ts index 71dbee40..c91571ca 100644 --- a/src/components/settings/FileSourceSettings.ts +++ b/src/components/settings/FileSourceSettings.ts @@ -5,7 +5,7 @@ * and treated as tasks with various strategies and options. */ -import { Setting } from "obsidian"; +import { Setting, Notice } from "obsidian"; import type TaskProgressBarPlugin from "../../index"; import type { FileSourceConfiguration } from "../../types/file-source"; import { t } from "../../translations/helper"; @@ -716,6 +716,64 @@ function createStatusMappingSection( ); if (config.statusMapping && config.statusMapping.enabled) { + + // Sync mapping from Task Status Settings + new Setting(containerEl) + .setName(t("Sync from Task Status Settings")) + .setDesc(t("Populate FileSource status mapping from your checkbox status configuration")) + .addButton((button) => + button + .setButtonText(t("Sync now")) + .setCta() + .onClick(async () => { + try { + const orchestrator = (plugin as any).dataflowOrchestrator; + if (orchestrator?.updateSettings) { + // Delegate to orchestrator so in-memory FileSource mapping syncs immediately + orchestrator.updateSettings(plugin.settings); + new Notice(t("FileSource status mapping synced")); + } else { + // Fallback: derive symbol->metadata mapping from Task Status settings + const taskStatuses = (plugin.settings.taskStatuses || {}) as Record; + const symbolToType: Record = {}; + for (const [type, symbols] of Object.entries(taskStatuses)) { + const list = String(symbols).split("|").filter(Boolean); + for (const sym of list) { + if (sym === "/>") { symbolToType["/"] = type; symbolToType[">"] = type; continue; } + if (sym.length === 1) symbolToType[sym] = type; else { + for (const ch of sym) symbolToType[ch] = type; + } + } + } + const typeToMetadata: Record = { + completed: "completed", + inProgress: "in-progress", + planned: "planned", + abandoned: "cancelled", + notStarted: "not-started", + }; + plugin.settings.fileSource.statusMapping = plugin.settings.fileSource.statusMapping || { + enabled: true, + metadataToSymbol: {}, + symbolToMetadata: {}, + autoDetect: true, + caseSensitive: false, + }; + plugin.settings.fileSource.statusMapping.symbolToMetadata = {}; + for (const [symbol, type] of Object.entries(symbolToType)) { + const md = typeToMetadata[type]; + if (md) plugin.settings.fileSource.statusMapping.symbolToMetadata[symbol] = md; + } + await plugin.saveSettings(); + new Notice(t("FileSource status mapping synced")); + } + } catch (e) { + console.error("Failed to sync FileSource status mapping:", e); + new Notice(t("Failed to sync mapping")); + } + }) + ); + new Setting(containerEl) .setName(t("Case Sensitive Matching")) .setDesc(t("Enable case-sensitive matching for status values")) @@ -870,7 +928,7 @@ function createPerformanceSection( // Note: Worker Processing setting has been moved to IndexSettingsTab.ts > Performance Configuration section // This avoids duplication and provides centralized control for all worker processing - + new Setting(containerEl) .setName(t("Cache TTL")) .setDesc(t("Time-to-live for cached results in milliseconds (default: 300000 = 5 minutes)")) diff --git a/src/components/task-view/details.ts b/src/components/task-view/details.ts index 45f4a77c..0035d89d 100644 --- a/src/components/task-view/details.ts +++ b/src/components/task-view/details.ts @@ -55,6 +55,27 @@ export function getStatusText( return statusTextMap[status as keyof typeof statusTextMap] || "No status"; } +function mapTextStatusToSymbol(status: string): string { + if (!status) return " "; + if (status.length === 1) return status; // already a symbol mark + const map: Record = { + "completed": "x", + "done": "x", + "finished": "x", + "in-progress": "/", + "in progress": "/", + "doing": "/", + "planned": "?", + "todo": "?", + "cancelled": "-", + "canceled": "-", + "not-started": " ", + "not started": " ", + }; + const key = status.toLowerCase(); + return map[key] ?? status; +} + export function createTaskCheckbox( status: string, task: Task, @@ -64,10 +85,9 @@ export function createTaskCheckbox( cls: "task-list-item-checkbox", type: "checkbox", }); - checkbox.dataset.task = status; - if (status !== " ") { - checkbox.checked = true; - } + const symbol = mapTextStatusToSymbol(status); + checkbox.dataset.task = symbol; + checkbox.checked = symbol !== " "; return checkbox; } diff --git a/src/dataflow/Orchestrator.ts b/src/dataflow/Orchestrator.ts index 77d00d04..3ef753d0 100644 --- a/src/dataflow/Orchestrator.ts +++ b/src/dataflow/Orchestrator.ts @@ -138,6 +138,12 @@ export class DataflowOrchestrator { this.plugin.settings.fileSource, this.fileFilterManager ); + // Keep FileSource status mapping in sync with Task Status settings + try { + this.fileSource.syncStatusMappingFromSettings(this.plugin.settings.taskStatuses); + } catch (e) { + console.warn("[DataflowOrchestrator] Failed to sync FileSource status mapping on init", e); + } } } @@ -612,6 +618,14 @@ export class DataflowOrchestrator { this.fileFilterManager ); this.fileSource.initialize(); + // Sync status mapping from Task Status settings on creation + try { + if (settings?.taskStatuses) { + this.fileSource.syncStatusMappingFromSettings(settings.taskStatuses); + } + } catch (e) { + console.warn("[DataflowOrchestrator] Failed to sync FileSource status mapping on settings create", e); + } } else if (!settings?.fileSource?.enabled && this.fileSource) { // Disable FileSource if it exists but is disabled this.fileSource.cleanup(); @@ -621,6 +635,15 @@ export class DataflowOrchestrator { this.fileSource.updateConfig(settings.fileSource); } + // Always try syncing status mapping when settings update and FileSource is active + if (this.fileSource && settings?.taskStatuses) { + try { + this.fileSource.syncStatusMappingFromSettings(settings.taskStatuses); + } catch (e) { + console.warn("[DataflowOrchestrator] Failed to sync FileSource status mapping on settings update", e); + } + } + // Update FileFilterManager if (settings?.fileFilter) { if (!this.fileFilterManager) { diff --git a/src/dataflow/api/WriteAPI.ts b/src/dataflow/api/WriteAPI.ts index a4f59d3c..cac9c4c2 100644 --- a/src/dataflow/api/WriteAPI.ts +++ b/src/dataflow/api/WriteAPI.ts @@ -293,20 +293,166 @@ export class WriteAPI { const preferFrontmatterTitle = cfg?.preferFrontmatterTitle ?? true; const customContentField = (this.plugin.settings?.fileSource?.fileTaskProperties as any)?.customContentField as string | undefined; + + console.log("[WriteAPI][FileSource] updateFileSourceTask start", { + taskId, + contentSource, + preferFrontmatterTitle, + customContentField, + updates + }); + + + // Apply frontmatter updates for non-content fields (status, completed, metadata) + try { + const md = (updates.metadata ?? {}) as any; + const hasFrontmatterUpdates = ( + updates.status !== undefined || + updates.completed !== undefined || + md.priority !== undefined || + md.tags !== undefined || + md.project !== undefined || + md.context !== undefined || + md.area !== undefined || + md.dueDate !== undefined || + md.startDate !== undefined || + md.scheduledDate !== undefined + ); + + if (hasFrontmatterUpdates) { + // Announce start of a write operation for frontmatter updates + emit(this.app, Events.WRITE_OPERATION_START, { path: file.path, taskId }); + + const formatDate = (val: any): any => { + if (val === undefined || val === null) return val; + if (typeof val === "number") { + // Write as YYYY-MM-DD for frontmatter consistency + return new Date(val).toISOString().split("T")[0]; + } + if (val instanceof Date) { + return val.toISOString().split("T")[0]; + } + return val; // assume already a string + }; + + await this.app.fileManager.processFrontMatter(file, (fm) => { + // Top-level status/completed + if (updates.status !== undefined) { + // If status mapping is enabled, prefer writing human-readable metadata value; + // otherwise write the raw symbol. + const fsMapping = this.plugin.settings?.fileSource?.statusMapping; + let statusToWrite: string = updates.status as any; + if (fsMapping?.enabled) { + // Try explicit symbol->metadata mapping first + const mapped = fsMapping.symbolToMetadata?.[statusToWrite]; + if (mapped) { + statusToWrite = mapped; + console.log("[WriteAPI][FileSource] mapped via symbolToMetadata", { mapped }); + } else { + console.log("[WriteAPI][FileSource] fallback mapping from taskStatuses", this.plugin.settings?.taskStatuses); + // Derive from Task Status settings as a fallback + const taskStatuses = (this.plugin.settings?.taskStatuses || {}) as Record; + const listByType = Object.entries(taskStatuses).map(([type, symbols]) => ({ type, symbols: String(symbols) })); + for (const entry of listByType) { + const parts = entry.symbols.split("|").filter(Boolean); + for (const sym of parts) { + if (sym === statusToWrite || (sym.length > 1 && sym.includes(statusToWrite))) { + // Map types to canonical metadata values + const typeToMetadata: Record = { + completed: "completed", + inProgress: "in-progress", + planned: "planned", + abandoned: "cancelled", + notStarted: "not-started", + }; + const md = typeToMetadata[entry.type]; + if (md) { + statusToWrite = md; + console.log("[WriteAPI][FileSource] mapped via taskStatuses", { type: entry.type, md }); + } + break; + } + } + if (statusToWrite !== (updates.status as any)) break; + } + } + } + (fm as any).status = statusToWrite; + } + if (updates.completed !== undefined) { + (fm as any).completed = updates.completed; + } + + // Metadata fields + if (md.priority !== undefined) { + (fm as any).priority = md.priority; + console.log("[WriteAPI][FileSource] wrote fm.priority", { priority: md.priority }); + } + if (md.tags !== undefined) { + (fm as any).tags = Array.isArray(md.tags) + ? md.tags + : (typeof md.tags === "string" ? [md.tags] : md.tags); + console.log("[WriteAPI][FileSource] wrote fm.tags", { tags: (fm as any).tags }); + } + if (md.project !== undefined) { + (fm as any).project = md.project; + console.log("[WriteAPI][FileSource] wrote fm.project", { project: md.project }); + } + if (md.context !== undefined) { + (fm as any).context = md.context; + console.log("[WriteAPI][FileSource] wrote fm.context", { context: md.context }); + } + if (md.area !== undefined) { + (fm as any).area = md.area; + console.log("[WriteAPI][FileSource] wrote fm.area", { area: md.area }); + } + if (md.dueDate !== undefined) { + (fm as any).dueDate = formatDate(md.dueDate); + console.log("[WriteAPI][FileSource] wrote fm.dueDate", { dueDate: (fm as any).dueDate }); + } + if (md.startDate !== undefined) { + (fm as any).startDate = formatDate(md.startDate); + console.log("[WriteAPI][FileSource] wrote fm.startDate", { startDate: (fm as any).startDate }); + } + if (md.scheduledDate !== undefined) { + (fm as any).scheduledDate = formatDate(md.scheduledDate); + console.log("[WriteAPI][FileSource] wrote fm.scheduledDate", { scheduledDate: (fm as any).scheduledDate }); + } + }); + + // Announce completion of frontmatter write operation + emit(this.app, Events.WRITE_OPERATION_COMPLETE, { path: file.path, taskId }); + } + } catch (error) { + console.error("WriteAPI: Error updating file-source task frontmatter:", error); + return { success: false, error: String(error) }; + } + // Handle content/title change - if (updates.content && updates.content !== originalTask.content) { + const shouldWriteContent = typeof updates.content === "string"; + console.log("[WriteAPI][FileSource] content change gate", { + originalContent: originalTask.content, + updatesContent: updates.content, + shouldWriteContent + }); + if (shouldWriteContent) { try { // Announce start of a write operation emit(this.app, Events.WRITE_OPERATION_START, { path: file.path, taskId }); + console.log("[WriteAPI][FileSource] content branch", { contentSource, preferFrontmatterTitle, customContentField }); switch (contentSource) { case "title": { if (preferFrontmatterTitle) { await this.app.fileManager.processFrontMatter(file, (fm) => { (fm as any).title = updates.content; }); + console.log("[WriteAPI][FileSource] wrote fm.title (branch: title)", { title: updates.content }); + const cacheAfter = this.app.metadataCache.getFileCache(file); + console.log("[WriteAPI][FileSource] cache fm.title after write (branch: title)", { title: cacheAfter?.frontmatter?.title }); } else { newFilePath = await this.renameFile(file, updates.content!); + console.log("[WriteAPI][FileSource] renamed file (branch: title)", { newFilePath }); } break; } @@ -319,14 +465,35 @@ export class WriteAPI { await this.app.fileManager.processFrontMatter(file, (fm) => { (fm as any)[customContentField] = updates.content; }); + console.log("[WriteAPI][FileSource] wrote fm[customContentField] (branch: custom)", { field: customContentField, value: updates.content }); + const cacheAfter = this.app.metadataCache.getFileCache(file); + console.log("[WriteAPI][FileSource] cache fm[customContentField] after write (branch: custom)", { field: customContentField, value: cacheAfter?.frontmatter?.[customContentField] }); + } else if (preferFrontmatterTitle) { + await this.app.fileManager.processFrontMatter(file, (fm) => { + (fm as any).title = updates.content; + }); + console.log("[WriteAPI][FileSource] wrote fm.title (branch: custom fallback)", { title: updates.content }); + const cacheAfter2 = this.app.metadataCache.getFileCache(file); + console.log("[WriteAPI][FileSource] cache fm.title after write (branch: custom fallback)", { title: cacheAfter2?.frontmatter?.title }); } else { newFilePath = await this.renameFile(file, updates.content!); + console.log("[WriteAPI][FileSource] renamed file (branch: custom fallback)", { newFilePath }); } break; } case "filename": default: { - newFilePath = await this.renameFile(file, updates.content!); + if (preferFrontmatterTitle) { + await this.app.fileManager.processFrontMatter(file, (fm) => { + (fm as any).title = updates.content; + }); + console.log("[WriteAPI][FileSource] wrote fm.title (branch: filename/default)", { title: updates.content }); + const cacheAfter = this.app.metadataCache.getFileCache(file); + console.log("[WriteAPI][FileSource] cache fm.title after write (branch: filename/default)", { title: cacheAfter?.frontmatter?.title }); + } else { + newFilePath = await this.renameFile(file, updates.content!); + console.log("[WriteAPI][FileSource] renamed file (branch: filename/default)", { newFilePath }); + } break; } } diff --git a/src/dataflow/sources/FileSource.ts b/src/dataflow/sources/FileSource.ts index 7e40157b..f5b0433e 100644 --- a/src/dataflow/sources/FileSource.ts +++ b/src/dataflow/sources/FileSource.ts @@ -339,7 +339,7 @@ export class FileSource { // Check frontmatter template references if (config.checkTemplateMetadata && fileCache?.frontmatter) { const frontmatter = fileCache.frontmatter; - return frontmatter.template === templatePath || + return frontmatter.template === templatePath || frontmatter.templateFile === templatePath || frontmatter.templatePath === templatePath; } @@ -576,7 +576,7 @@ export class FileSource { // Check if file matches any template path const matchesTemplate = templateConfig.templatePaths.some(templatePath => { // Simple path matching - could be enhanced with more sophisticated matching - return filePath.includes(templatePath) || + return filePath.includes(templatePath) || (fileCache?.frontmatter?.template === templatePath) || (fileCache?.frontmatter?.templateFile === templatePath); }); @@ -603,23 +603,41 @@ export class FileSource { switch (config.contentSource) { case 'filename': + // If user prefers frontmatter title, show it over filename + if (config.preferFrontmatterTitle && fileCache?.frontmatter?.title) { + return fileCache.frontmatter.title as string; + } return config.stripExtension ? fileNameWithoutExt : fileName; case 'title': // Always prefer frontmatter title if available, fallback to filename without extension - return fileCache?.frontmatter?.title || fileNameWithoutExt; + return (fileCache?.frontmatter?.title as string) || fileNameWithoutExt; case 'h1': const h1 = fileCache?.headings?.find(h => h.level === 1); - return h1?.heading || fileNameWithoutExt; + return (h1?.heading as string) || fileNameWithoutExt; case 'custom': if (config.customContentField && fileCache?.frontmatter) { - return fileCache.frontmatter[config.customContentField] || fileNameWithoutExt; + const val = fileCache.frontmatter[config.customContentField]; + if (val) return val as string; + // If custom field not present, optionally prefer frontmatter title + if (config.preferFrontmatterTitle && fileCache.frontmatter.title) { + return fileCache.frontmatter.title as string; + } + return fileNameWithoutExt; + } + // No custom field specified: optionally prefer frontmatter title + if (config.preferFrontmatterTitle && fileCache?.frontmatter?.title) { + return fileCache.frontmatter.title as string; } return fileNameWithoutExt; default: + // Default to respecting preferFrontmatterTitle when available + if (config.preferFrontmatterTitle && fileCache?.frontmatter?.title) { + return fileCache.frontmatter.title as string; + } return config.stripExtension ? fileNameWithoutExt : fileName; } } @@ -636,18 +654,34 @@ export class FileSource { const config = this.config.getConfig(); const frontmatter = fileCache?.frontmatter || {}; - // Get status from frontmatter and map it to symbol if needed - let status = frontmatter.status || config.fileTaskProperties.defaultStatus; - - // Apply status mapping if enabled - if (config.statusMapping && config.statusMapping.enabled && frontmatter.status) { - // Try to map the metadata value to a symbol - const mappedStatus = this.config.mapMetadataToSymbol(frontmatter.status); - if (mappedStatus !== frontmatter.status) { - // Mapping was successful - status = mappedStatus; - console.log(`[FileSource] Mapped status '${frontmatter.status}' to '${mappedStatus}' for ${filePath}`); + // Derive status from frontmatter and eagerly map textual metadata to a symbol + const rawStatus = frontmatter.status ?? ""; + const toSymbol = (val: string): string => { + if (!val) return config.fileTaskProperties.defaultStatus; + // Already a single-character mark + if (val.length === 1) return val; + const sm = this.config.getConfig().statusMapping; + const target = sm.caseSensitive ? val : String(val).toLowerCase(); + // Try configured metadata->symbol table first + for (const [k, sym] of Object.entries(sm.metadataToSymbol || {})) { + const key = sm.caseSensitive ? k : k.toLowerCase(); + if (key === target) return sym; } + // Fallback to common defaults to be robust + const defaults: Record = { + "completed": "x", "done": "x", "finished": "x", + "in-progress": "/", "in progress": "/", "doing": "/", + "planned": "?", "todo": "?", + "cancelled": "-", "canceled": "-", + "not-started": " ", "not started": " ", + }; + const norm = String(val).toLowerCase(); + if (defaults[norm] !== undefined) return defaults[norm]; + return config.fileTaskProperties.defaultStatus; + }; + let status = rawStatus ? toSymbol(rawStatus) : config.fileTaskProperties.defaultStatus; + if (rawStatus && status !== rawStatus) { + console.log(`[FileSource] Mapped status '${rawStatus}' to '${status}' for ${filePath}`); } // Extract standard task metadata @@ -897,6 +931,17 @@ export class FileSource { this.config.updateConfig(config); } + /** + * Sync FileSource status mapping from plugin TaskStatus settings + */ + public syncStatusMappingFromSettings(taskStatuses: Record): void { + try { + this.config.syncWithTaskStatuses(taskStatuses); + } catch (e) { + console.warn("[FileSource] Failed to sync status mapping from settings", e); + } + } + /** * Alias for updateConfiguration to match expected interface */ diff --git a/src/dataflow/sources/FileSourceConfig.ts b/src/dataflow/sources/FileSourceConfig.ts index 14aed4f4..bd147535 100644 --- a/src/dataflow/sources/FileSourceConfig.ts +++ b/src/dataflow/sources/FileSourceConfig.ts @@ -432,30 +432,36 @@ export class FileSourceConfig { if (!this.config.statusMapping.autoDetect) { return; } - + // Extract symbols from task status configuration const symbolToType: Record = {}; - + const typeToSymbols: Record = {}; + for (const [type, symbols] of Object.entries(taskStatuses)) { const symbolList = symbols.split('|').filter(s => s); + typeToSymbols[type] = typeToSymbols[type] || []; for (const symbol of symbolList) { // Handle potential pattern like '/>' being split into '/' and '>' if (symbol.length === 1 || symbol === '/>') { if (symbol === '/>') { symbolToType['/'] = type; symbolToType['>'] = type; + typeToSymbols[type].push('/'); + typeToSymbols[type].push('>'); } else { symbolToType[symbol] = type; + typeToSymbols[type].push(symbol); } } else { // For multi-character symbols, add each character separately for (const char of symbol) { symbolToType[char] = type; + typeToSymbols[type].push(char); } } } } - + // Update symbol to metadata mappings based on type const typeToMetadata: Record = { 'completed': 'completed', @@ -464,16 +470,32 @@ export class FileSourceConfig { 'abandoned': 'cancelled', 'notStarted': 'not-started' }; - + for (const [symbol, type] of Object.entries(symbolToType)) { if (typeToMetadata[type]) { this.config.statusMapping.symbolToMetadata[symbol] = typeToMetadata[type]; } } - + + // Also update metadata->symbol so metadata strings map back to the user's preferred symbol + const preferredFallback: Record = { + completed: 'x', + inProgress: '/', + planned: '?', + abandoned: '-', + notStarted: ' ' + }; + for (const [type, mdValue] of Object.entries(typeToMetadata)) { + const symbols = typeToSymbols[type] || []; + const preferred = symbols[0] || preferredFallback[type]; + if (mdValue && preferred !== undefined) { + this.config.statusMapping.metadataToSymbol[mdValue] = preferred; + } + } + this.notifyListeners(); } - + /** * Create a configuration preset for common use cases */ diff --git a/src/index.ts b/src/index.ts index af80ba34..c901ab21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -326,14 +326,6 @@ export default class TaskProgressBarPlugin extends Plugin { }) ); - // Add a ribbon icon for opening the TaskView - this.addRibbonIcon( - "task-genius", - t("Open Task Genius view"), - () => { - this.activateTaskView(); - } - ); // Add a command to open the TaskView this.addCommand({ id: "open-task-genius-view", @@ -368,6 +360,16 @@ export default class TaskProgressBarPlugin extends Plugin { addIcon("abandoned", getStatusIcon("abandoned")); addIcon("notStarted", getStatusIcon("notStarted")); + + // Add a ribbon icon for opening the TaskView + this.addRibbonIcon( + "task-genius", + t("Open Task Genius view"), + () => { + this.activateTaskView(); + } + ); + // Initialize dataflow orchestrator (primary architecture) try { console.log( diff --git a/src/managers/file-task-manager.ts b/src/managers/file-task-manager.ts index 61109e1c..8c8f5fcd 100644 --- a/src/managers/file-task-manager.ts +++ b/src/managers/file-task-manager.ts @@ -217,17 +217,21 @@ export class FileTaskManagerImpl implements FileTaskManager { */ fileTaskToPropertyUpdates( task: FileTask, - mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING + mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING, + excludeContent: boolean = false ): Record { const updates: Record = {}; // Update content property based on configuration - const config = this.fileSourceConfig?.fileTaskProperties; - if (config?.contentSource && config.contentSource !== 'filename') { - // Only update content property if it's not handled by file renaming - const shouldUpdateProperty = this.shouldUpdateContentProperty(config); - if (shouldUpdateProperty) { - updates[mapping.contentProperty] = task.content; + // Skip content if it was already handled separately (e.g., in handleContentUpdate) + if (!excludeContent) { + const config = this.fileSourceConfig?.fileTaskProperties; + if (config?.contentSource && config.contentSource !== 'filename') { + // Only update content property if it's not handled by file renaming + const shouldUpdateProperty = this.shouldUpdateContentProperty(config); + if (shouldUpdateProperty) { + updates[mapping.contentProperty] = task.content; + } } } // Note: If contentSource is 'filename', content updates are handled by file renaming @@ -317,10 +321,12 @@ export class FileTaskManagerImpl implements FileTaskManager { ): Promise { // Merge updates into the task const updatedTask = { ...task, ...updates }; + let contentHandledSeparately = false; // Handle content changes - re-extract time components if content changed if (updates.content && updates.content !== task.content) { await this.handleContentUpdate(task, updates.content); + contentHandledSeparately = true; // Re-extract time components from updated content const enhancedMetadata = this.extractTimeComponents(updates.content); @@ -352,8 +358,12 @@ export class FileTaskManagerImpl implements FileTaskManager { } } - // Convert to property updates (excluding content which is handled separately) - const propertyUpdates = this.fileTaskToPropertyUpdates(updatedTask); + // Convert to property updates (excluding content if it was handled separately) + const propertyUpdates = this.fileTaskToPropertyUpdates( + updatedTask, + DEFAULT_FILE_TASK_MAPPING, + contentHandledSeparately + ); console.log( `[FileTaskManager] Updating file task ${task.content} with properties:`, @@ -363,7 +373,12 @@ export class FileTaskManagerImpl implements FileTaskManager { // Update properties through the source entry for (const [key, value] of Object.entries(propertyUpdates)) { try { - task.sourceEntry.updateProperty(key, value); + // Note: updateProperty might be async, so we await it + if (typeof task.sourceEntry.updateProperty === 'function') { + await Promise.resolve(task.sourceEntry.updateProperty(key, value)); + } else { + console.error(`updateProperty method not available on source entry for key: ${key}`); + } } catch (error) { console.error(`Failed to update property ${key}:`, error); } @@ -445,10 +460,15 @@ export class FileTaskManagerImpl implements FileTaskManager { ): Promise { try { // Update the title property in frontmatter through the source entry - task.sourceEntry.updateProperty('title', newTitle); - console.log( - `[FileTaskManager] Updated frontmatter title for ${task.filePath} to: ${newTitle}` - ); + // Note: updateProperty might be async, so we await it + if (typeof task.sourceEntry.updateProperty === 'function') { + await Promise.resolve(task.sourceEntry.updateProperty('title', newTitle)); + console.log( + `[FileTaskManager] Updated frontmatter title for ${task.filePath} to: ${newTitle}` + ); + } else { + throw new Error('updateProperty method not available on source entry'); + } } catch (error) { console.error(`[FileTaskManager] Failed to update frontmatter title:`, error); // Fallback to file renaming if frontmatter update fails @@ -520,10 +540,15 @@ export class FileTaskManagerImpl implements FileTaskManager { ): Promise { try { // Update the custom field in frontmatter through the source entry - task.sourceEntry.updateProperty(fieldName, newContent); - console.log( - `[FileTaskManager] Updated custom field '${fieldName}' for ${task.filePath} to: ${newContent}` - ); + // Note: updateProperty might be async, so we await it + if (typeof task.sourceEntry.updateProperty === 'function') { + await Promise.resolve(task.sourceEntry.updateProperty(fieldName, newContent)); + console.log( + `[FileTaskManager] Updated custom field '${fieldName}' for ${task.filePath} to: ${newContent}` + ); + } else { + throw new Error('updateProperty method not available on source entry'); + } } catch (error) { console.error(`[FileTaskManager] Failed to update custom field '${fieldName}':`, error); }