mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(filesource): add status mapping between checkboxes and file metadata
- Implement bidirectional status mapping in FileSourceConfig with auto-detection from task status settings - Add sync button in FileSourceSettings to populate mapping from checkbox configuration - Enhance WriteAPI to convert between task symbols and metadata values during updates - Fix preferFrontmatterTitle logic to properly respect user preference across content sources - Add comprehensive logging for status mapping operations - Update FileTaskManager to handle status updates with proper symbol/metadata conversion - Ensure status changes persist correctly in both frontmatter and inline formats BREAKING CHANGE: FileSource status updates now require proper mapping configuration for non-standard status values
This commit is contained in:
parent
52573bfcd0
commit
9f671ab3d7
9 changed files with 419 additions and 57 deletions
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
const symbolToType: Record<string, string> = {};
|
||||
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<string, string> = {
|
||||
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)"))
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
"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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
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<string, string> = {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
"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<string, string>): 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -432,30 +432,36 @@ export class FileSourceConfig {
|
|||
if (!this.config.statusMapping.autoDetect) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Extract symbols from task status configuration
|
||||
const symbolToType: Record<string, string> = {};
|
||||
|
||||
const typeToSymbols: Record<string, string[]> = {};
|
||||
|
||||
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<string, string> = {
|
||||
'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<string, string> = {
|
||||
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
|
||||
*/
|
||||
|
|
|
|||
18
src/index.ts
18
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(
|
||||
|
|
|
|||
|
|
@ -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<string, any> {
|
||||
const updates: Record<string, any> = {};
|
||||
|
||||
// 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<void> {
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue