mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(filesource): implement file-based task recognition system
Add comprehensive FileSource feature that enables files to serve as tasks based on metadata properties, tags, templates, and path patterns. This creates a unified task management experience where files can represent both projects and actionable items. Key features: - Multiple recognition strategies (metadata, tags, templates, paths) - Flexible task property extraction from file metadata - Dual project-task file capabilities - Settings UI with live preview and validation - Integration with existing dataflow architecture - Configurable task content sources (filename, title, custom fields) - Support for custom status characters and priority levels The implementation follows established patterns from ObsidianSource and IcsSource, ensuring consistency with the existing codebase architecture.
This commit is contained in:
parent
b600490a1c
commit
691952a6b3
10 changed files with 2595 additions and 97 deletions
|
|
@ -4,6 +4,7 @@ import { BaseHabitData } from "../types/habit-card";
|
|||
import type { RootFilterState } from "../components/task-filter/ViewTaskFilter";
|
||||
import { IcsManagerConfig } from "../types/ics";
|
||||
import { TimeParsingConfig } from "../services/time-parsing-service";
|
||||
import type { FileSourceConfiguration } from "../types/file-source";
|
||||
|
||||
// Interface for individual project review settings (If still needed, otherwise remove)
|
||||
// Keep it for now, in case it's used elsewhere, but it's not part of TaskProgressBarSettings anymore
|
||||
|
|
@ -682,7 +683,7 @@ export interface TaskProgressBarSettings {
|
|||
// Enhanced Project Configuration
|
||||
projectConfig: ProjectConfiguration;
|
||||
|
||||
// File Parsing Configuration
|
||||
// File Parsing Configuration (DEPRECATED - use fileSource instead)
|
||||
fileParsingConfig: FileParsingConfiguration;
|
||||
|
||||
// Date Settings
|
||||
|
|
@ -694,8 +695,9 @@ export interface TaskProgressBarSettings {
|
|||
// Focus all tasks behind heading
|
||||
focusHeading: string;
|
||||
|
||||
// View Settings (Updated Structure)
|
||||
enableView: boolean;
|
||||
// Indexer and View Settings
|
||||
enableIndexer: boolean; // Enable Task Genius indexer for whole vault scanning
|
||||
enableView: boolean; // Enable Task Genius sidebar views
|
||||
enableInlineEditor: boolean; // Enable inline editing in task views
|
||||
enableDynamicMetadataPositioning: boolean; // Enable intelligent metadata positioning based on content length
|
||||
defaultViewMode: "list" | "tree"; // Global default view mode for all views
|
||||
|
|
@ -760,6 +762,9 @@ export interface TaskProgressBarSettings {
|
|||
skipOnboarding?: boolean;
|
||||
completedAt?: string;
|
||||
};
|
||||
|
||||
// FileSource Settings
|
||||
fileSource: FileSourceConfiguration;
|
||||
}
|
||||
|
||||
/** Define the default settings */
|
||||
|
|
@ -990,7 +995,6 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
|
|||
inheritFromFrontmatterForSubtasks: false,
|
||||
},
|
||||
|
||||
// Enhanced Project Configuration
|
||||
projectConfig: {
|
||||
enableEnhancedProject: false,
|
||||
pathMappings: [],
|
||||
|
|
@ -1051,8 +1055,9 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
|
|||
// Focus all tasks behind heading
|
||||
focusHeading: "",
|
||||
|
||||
// View Defaults (Updated Structure)
|
||||
enableView: true,
|
||||
// Indexer and View Defaults
|
||||
enableIndexer: true, // Enable indexer by default
|
||||
enableView: true, // Enable view by default
|
||||
enableInlineEditor: true, // Enable inline editing by default
|
||||
enableDynamicMetadataPositioning: true, // Enable intelligent metadata positioning by default
|
||||
defaultViewMode: "list", // Global default view mode for all views
|
||||
|
|
@ -1484,6 +1489,102 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
|
|||
configMode: 'beginner',
|
||||
skipOnboarding: false,
|
||||
completedAt: ""
|
||||
},
|
||||
|
||||
// FileSource Defaults - Import from FileSourceConfig
|
||||
fileSource: {
|
||||
enabled: false,
|
||||
recognitionStrategies: {
|
||||
metadata: {
|
||||
enabled: true,
|
||||
taskFields: ["dueDate", "status", "priority", "assigned"],
|
||||
requireAllFields: false
|
||||
},
|
||||
tags: {
|
||||
enabled: true,
|
||||
taskTags: ["#task", "#actionable", "#todo"],
|
||||
matchMode: "exact"
|
||||
},
|
||||
templates: {
|
||||
enabled: false,
|
||||
templatePaths: ["Templates/Task Template.md"],
|
||||
checkTemplateMetadata: true
|
||||
},
|
||||
paths: {
|
||||
enabled: false,
|
||||
taskPaths: ["Projects/", "Tasks/"],
|
||||
matchMode: "prefix"
|
||||
}
|
||||
},
|
||||
fileTaskProperties: {
|
||||
contentSource: "filename",
|
||||
stripExtension: true,
|
||||
defaultStatus: " ",
|
||||
defaultPriority: undefined
|
||||
},
|
||||
relationships: {
|
||||
enableChildRelationships: true,
|
||||
enableMetadataInheritance: true,
|
||||
inheritanceFields: ["project", "priority", "context"]
|
||||
},
|
||||
performance: {
|
||||
enableWorkerProcessing: true,
|
||||
enableCaching: true,
|
||||
cacheTTL: 300000
|
||||
},
|
||||
advanced: {
|
||||
excludePatterns: ["**/.obsidian/**", "**/node_modules/**"],
|
||||
maxFileSize: 1048576
|
||||
},
|
||||
statusMapping: {
|
||||
enabled: true,
|
||||
metadataToSymbol: {
|
||||
// Completed variants
|
||||
'completed': 'x',
|
||||
'done': 'x',
|
||||
'finished': 'x',
|
||||
'complete': 'x',
|
||||
'checked': 'x',
|
||||
'x': 'x',
|
||||
|
||||
// In Progress variants
|
||||
'in-progress': '/',
|
||||
'in progress': '/',
|
||||
'inprogress': '/',
|
||||
'doing': '/',
|
||||
'working': '/',
|
||||
'/': '/',
|
||||
|
||||
// Planned variants
|
||||
'planned': '?',
|
||||
'todo': '?',
|
||||
'pending': '?',
|
||||
'?': '?',
|
||||
|
||||
// Abandoned variants
|
||||
'cancelled': '-',
|
||||
'canceled': '-',
|
||||
'abandoned': '-',
|
||||
'-': '-',
|
||||
|
||||
// Not Started variants
|
||||
'not-started': ' ',
|
||||
'not started': ' ',
|
||||
'new': ' ',
|
||||
' ': ' '
|
||||
},
|
||||
symbolToMetadata: {
|
||||
'x': 'completed',
|
||||
'X': 'completed',
|
||||
'/': 'in-progress',
|
||||
'>': 'in-progress',
|
||||
'?': 'planned',
|
||||
'-': 'cancelled',
|
||||
' ': 'not-started'
|
||||
},
|
||||
autoDetect: true,
|
||||
caseSensitive: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
610
src/components/settings/FileSourceSettings.ts
Normal file
610
src/components/settings/FileSourceSettings.ts
Normal file
|
|
@ -0,0 +1,610 @@
|
|||
/**
|
||||
* FileSourceSettings - UI component for File as Task configuration
|
||||
*
|
||||
* Provides a settings interface for configuring how files can be recognized
|
||||
* and treated as tasks with various strategies and options.
|
||||
*/
|
||||
|
||||
import { Setting } from "obsidian";
|
||||
import type TaskProgressBarPlugin from "../../index";
|
||||
import type { FileSourceConfiguration } from "../../types/file-source";
|
||||
import { t } from "../../translations/helper";
|
||||
|
||||
/**
|
||||
* Create File as Task settings UI
|
||||
*/
|
||||
export function createFileSourceSettings(
|
||||
containerEl: HTMLElement,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
): void {
|
||||
const config = plugin.settings.fileSource;
|
||||
|
||||
// Main FileSource enable/disable toggle
|
||||
createEnableToggle(containerEl, plugin, config);
|
||||
|
||||
if (config.enabled) {
|
||||
// Recognition strategies section
|
||||
createRecognitionStrategiesSection(containerEl, plugin, config);
|
||||
|
||||
// File task properties section
|
||||
createFileTaskPropertiesSection(containerEl, plugin, config);
|
||||
|
||||
// Status mapping section
|
||||
createStatusMappingSection(containerEl, plugin, config);
|
||||
|
||||
// Performance section
|
||||
createPerformanceSection(containerEl, plugin, config);
|
||||
|
||||
// Advanced section
|
||||
createAdvancedSection(containerEl, plugin, config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the main enable/disable toggle
|
||||
*/
|
||||
function createEnableToggle(
|
||||
containerEl: HTMLElement,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
config: FileSourceConfiguration,
|
||||
): void {
|
||||
// Don't create duplicate header since we're now embedded in IndexSettingsTab
|
||||
const desc = containerEl.createEl("p");
|
||||
desc.innerHTML =
|
||||
t(
|
||||
"Enable files to be recognized as tasks based on their metadata properties. ",
|
||||
) +
|
||||
`<strong>${t("Note:")}</strong> ` +
|
||||
t(
|
||||
"This is an advanced feature that extends file metadata parsing with multiple recognition strategies.",
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Enable File as Task"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Allow files to be recognized and treated as tasks based on their properties",
|
||||
),
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(config.enabled).onChange(async (value) => {
|
||||
plugin.settings.fileSource.enabled = value;
|
||||
await plugin.saveSettings();
|
||||
|
||||
// Refresh the settings display
|
||||
containerEl.empty();
|
||||
createFileSourceSettings(containerEl, plugin);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create recognition strategies section
|
||||
*/
|
||||
function createRecognitionStrategiesSection(
|
||||
containerEl: HTMLElement,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
config: FileSourceConfiguration,
|
||||
): void {
|
||||
new Setting(containerEl)
|
||||
.setHeading()
|
||||
.setName(t("Recognition Strategies"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Configure how files are recognized as tasks. At least one strategy must be enabled.",
|
||||
),
|
||||
);
|
||||
|
||||
// Metadata strategy
|
||||
const metadataContainer = containerEl.createDiv(
|
||||
"file-source-strategy-container",
|
||||
);
|
||||
|
||||
new Setting(metadataContainer)
|
||||
.setName(t("Metadata-based Recognition"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Recognize files as tasks if they have specific frontmatter fields",
|
||||
),
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(config.recognitionStrategies.metadata.enabled)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.recognitionStrategies.metadata.enabled =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
if (config.recognitionStrategies.metadata.enabled) {
|
||||
new Setting(metadataContainer)
|
||||
.setName(t("Task Fields"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Comma-separated list of metadata fields that indicate a task",
|
||||
),
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("dueDate, status, priority, assigned")
|
||||
.setValue(
|
||||
config.recognitionStrategies.metadata.taskFields.join(
|
||||
", ",
|
||||
),
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.recognitionStrategies.metadata.taskFields =
|
||||
value
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(metadataContainer)
|
||||
.setName(t("Require All Fields"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Require all specified fields to be present (otherwise any field is sufficient)",
|
||||
),
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(
|
||||
config.recognitionStrategies.metadata.requireAllFields,
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.recognitionStrategies.metadata.requireAllFields =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Tag strategy
|
||||
const tagContainer = containerEl.createDiv(
|
||||
"file-source-strategy-container",
|
||||
);
|
||||
|
||||
new Setting(tagContainer)
|
||||
.setName(t("Tag-based Recognition"))
|
||||
.setDesc(t("Recognize files as tasks if they have specific tags"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(config.recognitionStrategies.tags.enabled)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.recognitionStrategies.tags.enabled =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
if (config.recognitionStrategies.tags.enabled) {
|
||||
new Setting(tagContainer)
|
||||
.setName(t("Task Tags"))
|
||||
.setDesc(t("Comma-separated list of tags that indicate a task"))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("#task, #actionable, #todo")
|
||||
.setValue(
|
||||
config.recognitionStrategies.tags.taskTags.join(", "),
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.recognitionStrategies.tags.taskTags =
|
||||
value
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(tagContainer)
|
||||
.setName(t("Tag Matching Mode"))
|
||||
.setDesc(t("How tags should be matched against file tags"))
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("exact", t("Exact match"))
|
||||
.addOption("prefix", t("Prefix match"))
|
||||
.addOption("contains", t("Contains match"))
|
||||
.setValue(config.recognitionStrategies.tags.matchMode)
|
||||
.onChange(
|
||||
async (value: "exact" | "prefix" | "contains") => {
|
||||
plugin.settings.fileSource.recognitionStrategies.tags.matchMode =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Template and Path strategies (Phase 2 placeholders)
|
||||
const futureContainer = containerEl.createDiv(
|
||||
"file-source-future-strategies",
|
||||
);
|
||||
futureContainer.createEl("p", {
|
||||
text: t(
|
||||
"Template-based and Path-based recognition strategies will be available in a future update.",
|
||||
),
|
||||
attr: { style: "color: var(--text-muted); font-style: italic;" },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file task properties section
|
||||
*/
|
||||
function createFileTaskPropertiesSection(
|
||||
containerEl: HTMLElement,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
config: FileSourceConfiguration,
|
||||
): void {
|
||||
new Setting(containerEl)
|
||||
.setHeading()
|
||||
.setName(t("Task Properties for Files"));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Task Title Source"))
|
||||
.setDesc(
|
||||
t(
|
||||
"What should be used as the task title when a file becomes a task",
|
||||
),
|
||||
)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("filename", t("Filename"))
|
||||
.addOption("title", t("Frontmatter title"))
|
||||
.addOption("h1", t("First H1 heading"))
|
||||
.addOption("custom", t("Custom metadata field"))
|
||||
.setValue(config.fileTaskProperties.contentSource)
|
||||
.onChange(
|
||||
async (value: "filename" | "title" | "h1" | "custom") => {
|
||||
plugin.settings.fileSource.fileTaskProperties.contentSource =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
|
||||
// Refresh to show/hide custom field input
|
||||
containerEl.empty();
|
||||
createFileSourceSettings(containerEl, plugin);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (config.fileTaskProperties.contentSource === "custom") {
|
||||
new Setting(containerEl)
|
||||
.setName(t("Custom Content Field"))
|
||||
.setDesc(t("Name of the metadata field to use as task content"))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("taskContent")
|
||||
.setValue(
|
||||
config.fileTaskProperties.customContentField || "",
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.fileTaskProperties.customContentField =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (config.fileTaskProperties.contentSource === "filename") {
|
||||
new Setting(containerEl)
|
||||
.setName(t("Strip File Extension"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Remove the .md extension from filename when using as task content",
|
||||
),
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(config.fileTaskProperties.stripExtension)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.fileTaskProperties.stripExtension =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Default Task Status"))
|
||||
.setDesc(t("Default status for newly created file tasks"))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(" ")
|
||||
.setValue(config.fileTaskProperties.defaultStatus)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.fileTaskProperties.defaultStatus =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create status mapping section
|
||||
*/
|
||||
function createStatusMappingSection(
|
||||
containerEl: HTMLElement,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
config: FileSourceConfiguration,
|
||||
): void {
|
||||
new Setting(containerEl)
|
||||
.setName(t("Status Mapping"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Map between human-readable metadata values (e.g., 'completed') and task symbols (e.g., 'x').",
|
||||
),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Enable Status Mapping"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Automatically convert between metadata status values and task symbols",
|
||||
),
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(config.statusMapping.enabled)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.statusMapping.enabled = value;
|
||||
await plugin.saveSettings();
|
||||
|
||||
// Refresh to show/hide mapping options
|
||||
containerEl.empty();
|
||||
createFileSourceSettings(containerEl, plugin);
|
||||
}),
|
||||
);
|
||||
|
||||
if (config.statusMapping.enabled) {
|
||||
new Setting(containerEl)
|
||||
.setName(t("Case Sensitive Matching"))
|
||||
.setDesc(t("Enable case-sensitive matching for status values"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(config.statusMapping.caseSensitive)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.statusMapping.caseSensitive =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Auto-detect Status Mappings"))
|
||||
.setDesc(t("Automatically sync with task status configuration"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(config.statusMapping.autoDetect)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.statusMapping.autoDetect =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
// Common status mappings display
|
||||
const mappingsContainer = containerEl.createDiv(
|
||||
"file-source-status-mappings",
|
||||
);
|
||||
mappingsContainer.createEl("h5", { text: t("Common Mappings") });
|
||||
|
||||
const mappingsList = mappingsContainer.createEl("div", {
|
||||
cls: "status-mapping-list",
|
||||
});
|
||||
|
||||
// Show some example mappings
|
||||
const examples = [
|
||||
{ metadata: "completed", symbol: "x" },
|
||||
{ metadata: "in-progress", symbol: "/" },
|
||||
{ metadata: "planned", symbol: "?" },
|
||||
{ metadata: "cancelled", symbol: "-" },
|
||||
{ metadata: "not-started", symbol: " " },
|
||||
];
|
||||
|
||||
const table = mappingsList.createEl("table", {
|
||||
cls: "status-mapping-table",
|
||||
});
|
||||
const thead = table.createEl("thead");
|
||||
const headerRow = thead.createEl("tr");
|
||||
headerRow.createEl("th", { text: t("Metadata Value") });
|
||||
headerRow.createEl("th", { text: "→" });
|
||||
headerRow.createEl("th", { text: t("Task Symbol") });
|
||||
|
||||
const tbody = table.createEl("tbody");
|
||||
examples.forEach((example) => {
|
||||
const row = tbody.createEl("tr");
|
||||
row.createEl("td", { text: example.metadata });
|
||||
row.createEl("td", { text: "→" });
|
||||
row.createEl("td", {
|
||||
text: example.symbol === " " ? "(space)" : example.symbol,
|
||||
});
|
||||
});
|
||||
|
||||
// Add custom mapping management UI
|
||||
containerEl.createEl("h5", { text: t("Custom Mappings") });
|
||||
|
||||
const customMappingDesc = containerEl.createEl("p");
|
||||
customMappingDesc.textContent = t(
|
||||
"Add custom status mappings for your workflow.",
|
||||
);
|
||||
|
||||
// Add mapping input
|
||||
new Setting(containerEl)
|
||||
.setName(t("Add Custom Mapping"))
|
||||
.setDesc(t("Enter metadata value and symbol (e.g., 'done:x')"))
|
||||
.addText((text) =>
|
||||
text.setPlaceholder("done:x").onChange(async (value) => {
|
||||
if (value.includes(":")) {
|
||||
const [metadata, symbol] = value.split(":", 2);
|
||||
if (metadata && symbol) {
|
||||
plugin.settings.fileSource.statusMapping.metadataToSymbol[
|
||||
metadata
|
||||
] = symbol;
|
||||
|
||||
// Also update reverse mapping if not exists
|
||||
if (
|
||||
!plugin.settings.fileSource.statusMapping
|
||||
.symbolToMetadata[symbol]
|
||||
) {
|
||||
plugin.settings.fileSource.statusMapping.symbolToMetadata[
|
||||
symbol
|
||||
] = metadata;
|
||||
}
|
||||
|
||||
await plugin.saveSettings();
|
||||
text.setValue("");
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText(t("Add"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
// Trigger the text change event with the current value
|
||||
const textInput = containerEl.querySelector(
|
||||
".setting-item:last-child input[type='text']",
|
||||
) as HTMLInputElement;
|
||||
if (textInput) {
|
||||
textInput.dispatchEvent(new Event("change"));
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Note about Task Status Settings integration
|
||||
const integrationNote = containerEl.createDiv(
|
||||
"setting-item-description",
|
||||
);
|
||||
integrationNote.innerHTML =
|
||||
`<strong>${t("Note:")}</strong> ` +
|
||||
t("Status mappings work with your Task Status Settings. ") +
|
||||
t(
|
||||
"The symbols defined here should match those in your checkbox status configuration.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create performance section
|
||||
*/
|
||||
function createPerformanceSection(
|
||||
containerEl: HTMLElement,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
config: FileSourceConfiguration,
|
||||
): void {
|
||||
new Setting(containerEl).setHeading().setName(t("Performance"));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Enable Caching"))
|
||||
.setDesc(t("Cache file task results to improve performance"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(config.performance.enableCaching)
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.performance.enableCaching =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Enable Worker Processing"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Use background workers for better performance (will be implemented in Phase 4)",
|
||||
),
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(config.performance.enableWorkerProcessing)
|
||||
.setDisabled(true) // Disabled until Phase 4
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.performance.enableWorkerProcessing =
|
||||
value;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create advanced section
|
||||
*/
|
||||
function createAdvancedSection(
|
||||
containerEl: HTMLElement,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
config: FileSourceConfiguration,
|
||||
): void {
|
||||
new Setting(containerEl).setHeading().setName(t("Advanced"));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Exclude Patterns"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Comma-separated patterns for files to exclude from being recognized as tasks",
|
||||
),
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("**/.obsidian/**, **/node_modules/**")
|
||||
.setValue(config.advanced.excludePatterns.join(", "))
|
||||
.onChange(async (value) => {
|
||||
plugin.settings.fileSource.advanced.excludePatterns = value
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Maximum File Size"))
|
||||
.setDesc(t("Maximum file size in bytes to process (default: 1MB)"))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("1048576")
|
||||
.setValue(config.advanced.maxFileSize.toString())
|
||||
.onChange(async (value) => {
|
||||
const size = parseInt(value) || 1048576;
|
||||
plugin.settings.fileSource.advanced.maxFileSize = size;
|
||||
await plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
// Statistics section
|
||||
const statsContainer = containerEl.createDiv("file-source-stats");
|
||||
statsContainer.createEl("h5", { text: t("File as Task Status") });
|
||||
|
||||
const statusText = config.enabled
|
||||
? t("File as Task is enabled and monitoring files")
|
||||
: t("File as Task is disabled");
|
||||
|
||||
statsContainer.createEl("p", { text: statusText });
|
||||
|
||||
if (config.enabled) {
|
||||
const strategiesText = getEnabledStrategiesText(config);
|
||||
statsContainer.createEl("p", {
|
||||
text: t("Active strategies: ") + strategiesText,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text description of enabled strategies
|
||||
*/
|
||||
function getEnabledStrategiesText(config: FileSourceConfiguration): string {
|
||||
const enabled: string[] = [];
|
||||
|
||||
if (config.recognitionStrategies.metadata.enabled)
|
||||
enabled.push(t("Metadata"));
|
||||
if (config.recognitionStrategies.tags.enabled) enabled.push(t("Tags"));
|
||||
if (config.recognitionStrategies.templates.enabled)
|
||||
enabled.push(t("Templates"));
|
||||
if (config.recognitionStrategies.paths.enabled) enabled.push(t("Paths"));
|
||||
|
||||
return enabled.length > 0 ? enabled.join(", ") : t("None");
|
||||
}
|
||||
825
src/dataflow/sources/FileSource.ts
Normal file
825
src/dataflow/sources/FileSource.ts
Normal file
|
|
@ -0,0 +1,825 @@
|
|||
/**
|
||||
* FileSource - Main implementation for FileSource feature
|
||||
*
|
||||
* This source integrates files as tasks into the dataflow architecture.
|
||||
* It follows the same patterns as ObsidianSource and IcsSource.
|
||||
*/
|
||||
|
||||
import type { App, TFile, EventRef, CachedMetadata } from "obsidian";
|
||||
import type { Task } from "../../types/task";
|
||||
import type {
|
||||
FileSourceConfiguration,
|
||||
FileSourceTaskMetadata,
|
||||
FileSourceStats,
|
||||
UpdateDecision,
|
||||
FileTaskCache,
|
||||
RecognitionStrategy
|
||||
} from "../../types/file-source";
|
||||
|
||||
import { Events, emit, Seq, on } from "../events/Events";
|
||||
import { FileSourceConfig } from "./FileSourceConfig";
|
||||
|
||||
/**
|
||||
* FileSource - Independent event source for file-based tasks
|
||||
*
|
||||
* Subscribes to file events and transforms qualifying files into tasks
|
||||
* following the established dataflow patterns.
|
||||
*/
|
||||
export class FileSource {
|
||||
private config: FileSourceConfig;
|
||||
private isInitialized = false;
|
||||
private lastUpdateSeq = 0;
|
||||
|
||||
// Event references for cleanup
|
||||
private eventRefs: EventRef[] = [];
|
||||
|
||||
// Cache for tracking file task state
|
||||
private fileTaskCache = new Map<string, FileTaskCache>();
|
||||
|
||||
// Debouncing for rapid changes
|
||||
private pendingUpdates = new Map<string, NodeJS.Timeout>();
|
||||
private readonly DEBOUNCE_DELAY = 300; // ms
|
||||
|
||||
// Statistics tracking
|
||||
private stats: FileSourceStats = {
|
||||
initialized: false,
|
||||
trackedFileCount: 0,
|
||||
recognitionBreakdown: {
|
||||
metadata: 0,
|
||||
tag: 0,
|
||||
template: 0,
|
||||
path: 0
|
||||
},
|
||||
lastUpdate: 0,
|
||||
lastUpdateSeq: 0
|
||||
};
|
||||
|
||||
constructor(
|
||||
private app: App,
|
||||
initialConfig?: Partial<FileSourceConfiguration>
|
||||
) {
|
||||
this.config = new FileSourceConfig(initialConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize FileSource and start listening for events
|
||||
*/
|
||||
initialize(): void {
|
||||
if (this.isInitialized) return;
|
||||
if (!this.config.isEnabled()) return;
|
||||
|
||||
console.log("[FileSource] Initializing FileSource...");
|
||||
|
||||
// Subscribe to configuration changes
|
||||
this.config.onChange((newConfig) => {
|
||||
this.handleConfigChange(newConfig);
|
||||
});
|
||||
|
||||
// Subscribe to file events
|
||||
this.subscribeToFileEvents();
|
||||
|
||||
// Perform initial scan of existing files
|
||||
this.performInitialScan();
|
||||
|
||||
this.isInitialized = true;
|
||||
this.stats.initialized = true;
|
||||
|
||||
console.log(`[FileSource] Initialized with strategies: ${this.config.getEnabledStrategies().join(', ')}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to relevant file events
|
||||
*/
|
||||
private subscribeToFileEvents(): void {
|
||||
// Subscribe to FILE_UPDATED events from ObsidianSource
|
||||
this.eventRefs.push(
|
||||
on(this.app, Events.FILE_UPDATED, (payload) => {
|
||||
if (payload?.path) {
|
||||
this.handleFileUpdate(payload.path, payload.reason);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Subscribe to more granular events if they exist
|
||||
// These would be added to Events.ts later in Phase 2
|
||||
this.eventRefs.push(
|
||||
on(this.app, "task-genius:file-metadata-changed" as any, (payload) => {
|
||||
if (payload?.path) {
|
||||
this.handleFileMetadataChange(payload.path);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.eventRefs.push(
|
||||
on(this.app, "task-genius:file-content-changed" as any, (payload) => {
|
||||
if (payload?.path) {
|
||||
this.handleFileContentChange(payload.path);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file update events with debouncing
|
||||
*/
|
||||
private handleFileUpdate(filePath: string, reason: string): void {
|
||||
if (!this.isInitialized || !this.config.isEnabled()) return;
|
||||
if (!this.isRelevantFile(filePath)) return;
|
||||
|
||||
// Clear existing timeout for this file
|
||||
const existingTimeout = this.pendingUpdates.get(filePath);
|
||||
if (existingTimeout) {
|
||||
clearTimeout(existingTimeout);
|
||||
}
|
||||
|
||||
// Set new debounced timeout
|
||||
const timeout = setTimeout(async () => {
|
||||
this.pendingUpdates.delete(filePath);
|
||||
|
||||
try {
|
||||
await this.processFileUpdate(filePath, reason);
|
||||
} catch (error) {
|
||||
console.error(`[FileSource] Error processing file update for ${filePath}:`, error);
|
||||
}
|
||||
}, this.DEBOUNCE_DELAY);
|
||||
|
||||
this.pendingUpdates.set(filePath, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle granular metadata changes (Phase 2 enhancement)
|
||||
*/
|
||||
private handleFileMetadataChange(filePath: string): void {
|
||||
if (!this.shouldUpdateFileTask(filePath, 'metadata')) return;
|
||||
this.handleFileUpdate(filePath, 'frontmatter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle granular content changes (Phase 2 enhancement)
|
||||
*/
|
||||
private handleFileContentChange(filePath: string): void {
|
||||
if (!this.shouldUpdateFileTask(filePath, 'content')) return;
|
||||
this.handleFileUpdate(filePath, 'modify');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a file update and determine if it should be a file task
|
||||
*/
|
||||
private async processFileUpdate(filePath: string, reason: string): Promise<void> {
|
||||
if (reason === 'delete') {
|
||||
await this.removeFileTask(filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldBeTask = await this.shouldCreateFileTask(filePath);
|
||||
const existingCache = this.fileTaskCache.get(filePath);
|
||||
const wasTask = existingCache?.fileTaskExists ?? false;
|
||||
|
||||
if (shouldBeTask && !wasTask) {
|
||||
// File should become a task
|
||||
await this.createFileTask(filePath);
|
||||
} else if (shouldBeTask && wasTask) {
|
||||
// File is already a task, check if it needs updating
|
||||
await this.updateFileTask(filePath);
|
||||
} else if (!shouldBeTask && wasTask) {
|
||||
// File should no longer be a task
|
||||
await this.removeFileTask(filePath);
|
||||
}
|
||||
// else: File is not and should not be a task, do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file should be treated as a task
|
||||
*/
|
||||
async shouldCreateFileTask(filePath: string): Promise<boolean> {
|
||||
if (!this.isRelevantFile(filePath)) return false;
|
||||
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
|
||||
if (!file) return false;
|
||||
|
||||
try {
|
||||
const fileContent = await this.app.vault.cachedRead(file);
|
||||
const fileCache = this.app.metadataCache.getFileCache(file);
|
||||
|
||||
return this.evaluateRecognitionStrategies(filePath, fileContent, fileCache);
|
||||
} catch (error) {
|
||||
console.error(`[FileSource] Error reading file ${filePath}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate all enabled recognition strategies
|
||||
*/
|
||||
private evaluateRecognitionStrategies(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null
|
||||
): boolean {
|
||||
const config = this.config.getConfig();
|
||||
const { recognitionStrategies } = config;
|
||||
|
||||
// Check metadata strategy
|
||||
if (recognitionStrategies.metadata.enabled) {
|
||||
if (this.matchesMetadataStrategy(filePath, fileContent, fileCache, recognitionStrategies.metadata)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check tag strategy
|
||||
if (recognitionStrategies.tags.enabled) {
|
||||
if (this.matchesTagStrategy(filePath, fileContent, fileCache, recognitionStrategies.tags)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check template strategy (Phase 2)
|
||||
if (recognitionStrategies.templates.enabled) {
|
||||
if (this.matchesTemplateStrategy(filePath, fileContent, fileCache, recognitionStrategies.templates)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check path strategy (Phase 2)
|
||||
if (recognitionStrategies.paths.enabled) {
|
||||
if (this.matchesPathStrategy(filePath, fileContent, fileCache, recognitionStrategies.paths)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file matches metadata strategy
|
||||
*/
|
||||
private matchesMetadataStrategy(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null,
|
||||
config: any
|
||||
): boolean {
|
||||
if (!fileCache?.frontmatter) return false;
|
||||
|
||||
const { taskFields, requireAllFields } = config;
|
||||
const frontmatter = fileCache.frontmatter;
|
||||
|
||||
const matchingFields = taskFields.filter((field: string) =>
|
||||
frontmatter.hasOwnProperty(field) && frontmatter[field] !== undefined
|
||||
);
|
||||
|
||||
if (requireAllFields) {
|
||||
return matchingFields.length === taskFields.length;
|
||||
} else {
|
||||
return matchingFields.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file matches tag strategy
|
||||
*/
|
||||
private matchesTagStrategy(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null,
|
||||
config: any
|
||||
): boolean {
|
||||
if (!fileCache?.tags) return false;
|
||||
|
||||
const { taskTags, matchMode } = config;
|
||||
const fileTags = fileCache.tags.map(tag => tag.tag);
|
||||
|
||||
return taskTags.some((taskTag: string) => {
|
||||
return fileTags.some(fileTag => {
|
||||
switch (matchMode) {
|
||||
case 'exact':
|
||||
return fileTag === taskTag;
|
||||
case 'prefix':
|
||||
return fileTag.startsWith(taskTag);
|
||||
case 'contains':
|
||||
return fileTag.includes(taskTag);
|
||||
default:
|
||||
return fileTag === taskTag;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file matches template strategy (stub for Phase 2)
|
||||
*/
|
||||
private matchesTemplateStrategy(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null,
|
||||
config: any
|
||||
): boolean {
|
||||
// TODO: Implement in Phase 2
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file matches path strategy (stub for Phase 2)
|
||||
*/
|
||||
private matchesPathStrategy(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null,
|
||||
config: any
|
||||
): boolean {
|
||||
// TODO: Implement in Phase 2
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new file task
|
||||
*/
|
||||
async createFileTask(filePath: string): Promise<Task<FileSourceTaskMetadata> | null> {
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
|
||||
if (!file) return null;
|
||||
|
||||
try {
|
||||
const fileContent = await this.app.vault.cachedRead(file);
|
||||
const fileCache = this.app.metadataCache.getFileCache(file);
|
||||
|
||||
const fileTask = await this.buildFileTask(filePath, fileContent, fileCache);
|
||||
if (!fileTask) return null;
|
||||
|
||||
// Update cache
|
||||
this.updateFileTaskCache(filePath, fileTask);
|
||||
|
||||
// Update statistics
|
||||
this.updateStatistics(fileTask.metadata.recognitionStrategy, 1);
|
||||
|
||||
// Emit file task event
|
||||
this.emitFileTaskUpdate('created', fileTask);
|
||||
|
||||
return fileTask;
|
||||
} catch (error) {
|
||||
console.error(`[FileSource] Error creating file task for ${filePath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing file task
|
||||
*/
|
||||
async updateFileTask(filePath: string): Promise<Task<FileSourceTaskMetadata> | null> {
|
||||
// For Phase 1, just recreate the task
|
||||
// Phase 2 will add smart update detection
|
||||
return await this.createFileTask(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a file task
|
||||
*/
|
||||
async removeFileTask(filePath: string): Promise<void> {
|
||||
const existingCache = this.fileTaskCache.get(filePath);
|
||||
if (!existingCache?.fileTaskExists) return;
|
||||
|
||||
// Remove from cache
|
||||
this.fileTaskCache.delete(filePath);
|
||||
|
||||
// Update statistics
|
||||
this.stats.trackedFileCount = Math.max(0, this.stats.trackedFileCount - 1);
|
||||
|
||||
// Emit removal event
|
||||
const seq = Seq.next();
|
||||
this.lastUpdateSeq = seq;
|
||||
|
||||
emit(this.app, "task-genius:file-task-removed" as any, {
|
||||
filePath,
|
||||
timestamp: Date.now(),
|
||||
seq
|
||||
});
|
||||
|
||||
console.log(`[FileSource] Removed file task: ${filePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a file task from file data
|
||||
*/
|
||||
private async buildFileTask(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null
|
||||
): Promise<Task<FileSourceTaskMetadata> | null> {
|
||||
const config = this.config.getConfig();
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
|
||||
if (!file) return null;
|
||||
|
||||
// Determine which strategy matched
|
||||
const strategy = this.getMatchingStrategy(filePath, fileContent, fileCache);
|
||||
if (!strategy) return null;
|
||||
|
||||
// Generate task content based on configuration
|
||||
const content = this.generateTaskContent(filePath, fileContent, fileCache);
|
||||
|
||||
// Extract metadata from frontmatter
|
||||
const metadata = this.extractTaskMetadata(filePath, fileContent, fileCache, strategy);
|
||||
|
||||
// Create the file task
|
||||
const fileTask: Task<FileSourceTaskMetadata> = {
|
||||
id: `file-source:${filePath}`,
|
||||
content,
|
||||
filePath,
|
||||
line: 0, // File tasks are at line 0
|
||||
completed: metadata.status === 'x' || metadata.status === 'X',
|
||||
status: metadata.status || config.fileTaskProperties.defaultStatus,
|
||||
originalMarkdown: `[${content}](${filePath})`,
|
||||
metadata: {
|
||||
...metadata,
|
||||
source: "file-source",
|
||||
recognitionStrategy: strategy.name,
|
||||
recognitionCriteria: strategy.criteria,
|
||||
fileTimestamps: {
|
||||
created: file.stat.ctime,
|
||||
modified: file.stat.mtime
|
||||
},
|
||||
childTasks: [], // Will be populated in Phase 3
|
||||
tags: metadata.tags || [],
|
||||
children: [] // Required by StandardTaskMetadata
|
||||
}
|
||||
};
|
||||
|
||||
return fileTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the matching recognition strategy for a file
|
||||
*/
|
||||
private getMatchingStrategy(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null
|
||||
): { name: RecognitionStrategy; criteria: string } | null {
|
||||
const config = this.config.getConfig();
|
||||
|
||||
if (config.recognitionStrategies.metadata.enabled &&
|
||||
this.matchesMetadataStrategy(filePath, fileContent, fileCache, config.recognitionStrategies.metadata)) {
|
||||
return { name: "metadata", criteria: "frontmatter" };
|
||||
}
|
||||
|
||||
if (config.recognitionStrategies.tags.enabled &&
|
||||
this.matchesTagStrategy(filePath, fileContent, fileCache, config.recognitionStrategies.tags)) {
|
||||
return { name: "tag", criteria: "file-tags" };
|
||||
}
|
||||
|
||||
// TODO: Add template and path strategies in Phase 2
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate task content based on configuration
|
||||
*/
|
||||
private generateTaskContent(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null
|
||||
): string {
|
||||
const config = this.config.getConfig().fileTaskProperties;
|
||||
const fileName = filePath.split('/').pop() || filePath;
|
||||
|
||||
switch (config.contentSource) {
|
||||
case 'filename':
|
||||
return config.stripExtension ?
|
||||
fileName.replace(/\.[^/.]+$/, "") : fileName;
|
||||
|
||||
case 'title':
|
||||
return fileCache?.frontmatter?.title || fileName;
|
||||
|
||||
case 'h1':
|
||||
const h1 = fileCache?.headings?.find(h => h.level === 1);
|
||||
return h1?.heading || fileName;
|
||||
|
||||
case 'custom':
|
||||
if (config.customContentField && fileCache?.frontmatter) {
|
||||
return fileCache.frontmatter[config.customContentField] || fileName;
|
||||
}
|
||||
return fileName;
|
||||
|
||||
default:
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract task metadata from file
|
||||
*/
|
||||
private extractTaskMetadata(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null,
|
||||
strategy: { name: RecognitionStrategy; criteria: string }
|
||||
): Partial<FileSourceTaskMetadata> {
|
||||
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.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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract standard task metadata
|
||||
const metadata: Partial<FileSourceTaskMetadata> = {
|
||||
dueDate: this.parseDate(frontmatter.dueDate || frontmatter.due),
|
||||
startDate: this.parseDate(frontmatter.startDate || frontmatter.start),
|
||||
scheduledDate: this.parseDate(frontmatter.scheduledDate || frontmatter.scheduled),
|
||||
priority: frontmatter.priority || config.fileTaskProperties.defaultPriority,
|
||||
project: frontmatter.project,
|
||||
context: frontmatter.context,
|
||||
area: frontmatter.area,
|
||||
tags: fileCache?.tags?.map(tag => tag.tag) || [],
|
||||
status: status,
|
||||
children: []
|
||||
};
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a task symbol back to metadata value for file updates
|
||||
* This will be used in Phase 3 when implementing file task updates
|
||||
*/
|
||||
private mapSymbolToFileMetadata(symbol: string): string {
|
||||
const config = this.config.getConfig();
|
||||
|
||||
if (!config.statusMapping.enabled) {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
// Map symbol back to preferred metadata value
|
||||
return this.config.mapSymbolToMetadata(symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse date from various formats
|
||||
*/
|
||||
private parseDate(dateValue: any): number | undefined {
|
||||
if (!dateValue) return undefined;
|
||||
|
||||
if (typeof dateValue === 'number') {
|
||||
return dateValue;
|
||||
}
|
||||
|
||||
if (typeof dateValue === 'string') {
|
||||
const parsed = Date.parse(dateValue);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
if (dateValue instanceof Date) {
|
||||
return dateValue.getTime();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update file task cache
|
||||
*/
|
||||
private updateFileTaskCache(filePath: string, task: Task<FileSourceTaskMetadata>): void {
|
||||
const frontmatterHash = this.generateFrontmatterHash(filePath);
|
||||
|
||||
this.fileTaskCache.set(filePath, {
|
||||
fileTaskExists: true,
|
||||
frontmatterHash,
|
||||
childTaskIds: new Set(task.metadata.childTasks || []),
|
||||
lastUpdated: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate hash for frontmatter for change detection
|
||||
*/
|
||||
private generateFrontmatterHash(filePath: string): string {
|
||||
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
|
||||
if (!file) return '';
|
||||
|
||||
const fileCache = this.app.metadataCache.getFileCache(file);
|
||||
if (!fileCache?.frontmatter) return '';
|
||||
|
||||
// Simple hash of frontmatter JSON
|
||||
const frontmatterStr = JSON.stringify(fileCache.frontmatter, Object.keys(fileCache.frontmatter).sort());
|
||||
return this.simpleHash(frontmatterStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple hash function
|
||||
*/
|
||||
private simpleHash(str: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return hash.toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file needs updating (stub for Phase 2)
|
||||
*/
|
||||
private shouldUpdateFileTask(filePath: string, changeType: 'metadata' | 'content'): boolean {
|
||||
// Simple check for Phase 1 - always update if file is tracked
|
||||
return this.fileTaskCache.has(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update statistics
|
||||
*/
|
||||
private updateStatistics(strategy: RecognitionStrategy, delta: number): void {
|
||||
this.stats.recognitionBreakdown[strategy] += delta;
|
||||
this.stats.trackedFileCount += delta;
|
||||
this.stats.lastUpdate = Date.now();
|
||||
this.stats.lastUpdateSeq = this.lastUpdateSeq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit file task update event
|
||||
*/
|
||||
private emitFileTaskUpdate(
|
||||
action: 'created' | 'updated' | 'removed',
|
||||
task: Task<FileSourceTaskMetadata>
|
||||
): void {
|
||||
const seq = Seq.next();
|
||||
this.lastUpdateSeq = seq;
|
||||
|
||||
emit(this.app, "task-genius:file-task-updated" as any, {
|
||||
action,
|
||||
task,
|
||||
timestamp: Date.now(),
|
||||
seq
|
||||
});
|
||||
|
||||
console.log(`[FileSource] File task ${action}: ${task.filePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file is relevant for processing
|
||||
*/
|
||||
private isRelevantFile(filePath: string): boolean {
|
||||
const config = this.config.getConfig().advanced;
|
||||
|
||||
// Check exclude patterns
|
||||
for (const pattern of config.excludePatterns) {
|
||||
// Simple pattern matching (could be enhanced with glob patterns in Phase 2)
|
||||
if (filePath.includes(pattern.replace('**/', '').replace('/**', ''))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only process markdown files for now
|
||||
if (!filePath.endsWith('.md')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip system/hidden files
|
||||
if (filePath.startsWith('.') || filePath.includes('/.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform initial scan of existing files
|
||||
*/
|
||||
private async performInitialScan(): Promise<void> {
|
||||
console.log("[FileSource] Performing initial scan...");
|
||||
|
||||
const mdFiles = this.app.vault.getMarkdownFiles();
|
||||
let scannedCount = 0;
|
||||
let taskCount = 0;
|
||||
|
||||
for (const file of mdFiles) {
|
||||
if (this.isRelevantFile(file.path)) {
|
||||
try {
|
||||
const shouldBeTask = await this.shouldCreateFileTask(file.path);
|
||||
if (shouldBeTask) {
|
||||
await this.createFileTask(file.path);
|
||||
taskCount++;
|
||||
}
|
||||
scannedCount++;
|
||||
} catch (error) {
|
||||
console.error(`[FileSource] Error scanning ${file.path}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[FileSource] Initial scan complete: ${scannedCount} files scanned, ${taskCount} file tasks created`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle configuration changes
|
||||
*/
|
||||
private handleConfigChange(newConfig: FileSourceConfiguration): void {
|
||||
if (!newConfig.enabled && this.isInitialized) {
|
||||
// FileSource is being disabled
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (newConfig.enabled && !this.isInitialized) {
|
||||
// FileSource is being enabled
|
||||
this.initialize();
|
||||
return;
|
||||
}
|
||||
|
||||
// Configuration changed while active - might need to rescan
|
||||
// This is a Phase 2 enhancement
|
||||
console.log("[FileSource] Configuration updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current statistics
|
||||
*/
|
||||
getStats(): FileSourceStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all file tasks (stub for Phase 2)
|
||||
*/
|
||||
async getAllFileTasks(): Promise<Task<FileSourceTaskMetadata>[]> {
|
||||
// This will be implemented properly in Phase 3 with Repository integration
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
*/
|
||||
updateConfiguration(config: Partial<FileSourceConfiguration>): void {
|
||||
this.config.updateConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh of all file tasks
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
if (!this.isInitialized || !this.config.isEnabled()) return;
|
||||
|
||||
console.log("[FileSource] Manual refresh triggered");
|
||||
|
||||
// Clear cache and re-scan
|
||||
this.fileTaskCache.clear();
|
||||
this.stats.trackedFileCount = 0;
|
||||
this.stats.recognitionBreakdown = { metadata: 0, tag: 0, template: 0, path: 0 };
|
||||
|
||||
await this.performInitialScan();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup and destroy FileSource
|
||||
*/
|
||||
destroy(): void {
|
||||
if (!this.isInitialized) return;
|
||||
|
||||
console.log("[FileSource] Destroying FileSource...");
|
||||
|
||||
// Clear all debouncing timeouts
|
||||
for (const timeout of this.pendingUpdates.values()) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
this.pendingUpdates.clear();
|
||||
|
||||
// Clear event listeners
|
||||
for (const ref of this.eventRefs) {
|
||||
this.app.vault.offref(ref);
|
||||
}
|
||||
this.eventRefs = [];
|
||||
|
||||
// Clear cache
|
||||
this.fileTaskCache.clear();
|
||||
|
||||
// Reset statistics
|
||||
this.stats = {
|
||||
initialized: false,
|
||||
trackedFileCount: 0,
|
||||
recognitionBreakdown: { metadata: 0, tag: 0, template: 0, path: 0 },
|
||||
lastUpdate: 0,
|
||||
lastUpdateSeq: 0
|
||||
};
|
||||
|
||||
// Emit cleanup event
|
||||
emit(this.app, "task-genius:file-task-removed" as any, {
|
||||
filePath: null, // Indicates all file tasks removed
|
||||
timestamp: Date.now(),
|
||||
seq: Seq.next(),
|
||||
destroyed: true
|
||||
});
|
||||
|
||||
this.isInitialized = false;
|
||||
|
||||
console.log("[FileSource] Cleanup complete");
|
||||
}
|
||||
}
|
||||
560
src/dataflow/sources/FileSourceConfig.ts
Normal file
560
src/dataflow/sources/FileSourceConfig.ts
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
/**
|
||||
* FileSourceConfig - Configuration management for FileSource
|
||||
*
|
||||
* Handles configuration validation, defaults, and update management
|
||||
* for the FileSource feature.
|
||||
*/
|
||||
|
||||
import type {
|
||||
FileSourceConfiguration,
|
||||
MetadataRecognitionConfig,
|
||||
TagRecognitionConfig,
|
||||
TemplateRecognitionConfig,
|
||||
PathRecognitionConfig,
|
||||
FileTaskPropertiesConfig,
|
||||
RelationshipsConfig,
|
||||
PerformanceConfig,
|
||||
AdvancedConfig,
|
||||
StatusMappingConfig
|
||||
} from "../../types/file-source";
|
||||
|
||||
/** Default configuration for metadata-based recognition */
|
||||
export const DEFAULT_METADATA_CONFIG: MetadataRecognitionConfig = {
|
||||
enabled: true,
|
||||
taskFields: ["dueDate", "status", "priority", "assigned"],
|
||||
requireAllFields: false
|
||||
};
|
||||
|
||||
/** Default configuration for tag-based recognition */
|
||||
export const DEFAULT_TAG_CONFIG: TagRecognitionConfig = {
|
||||
enabled: true,
|
||||
taskTags: ["#task", "#actionable", "#todo"],
|
||||
matchMode: "exact"
|
||||
};
|
||||
|
||||
/** Default configuration for template-based recognition */
|
||||
export const DEFAULT_TEMPLATE_CONFIG: TemplateRecognitionConfig = {
|
||||
enabled: false,
|
||||
templatePaths: ["Templates/Task Template.md"],
|
||||
checkTemplateMetadata: true
|
||||
};
|
||||
|
||||
/** Default configuration for path-based recognition */
|
||||
export const DEFAULT_PATH_CONFIG: PathRecognitionConfig = {
|
||||
enabled: false,
|
||||
taskPaths: ["Projects/", "Tasks/"],
|
||||
matchMode: "prefix"
|
||||
};
|
||||
|
||||
/** Default configuration for file task properties */
|
||||
export const DEFAULT_FILE_TASK_PROPERTIES: FileTaskPropertiesConfig = {
|
||||
contentSource: "filename",
|
||||
stripExtension: true,
|
||||
defaultStatus: " ",
|
||||
defaultPriority: undefined
|
||||
};
|
||||
|
||||
/** Default configuration for relationships */
|
||||
export const DEFAULT_RELATIONSHIPS_CONFIG: RelationshipsConfig = {
|
||||
enableChildRelationships: true,
|
||||
enableMetadataInheritance: true,
|
||||
inheritanceFields: ["project", "priority", "context"]
|
||||
};
|
||||
|
||||
/** Default configuration for performance */
|
||||
export const DEFAULT_PERFORMANCE_CONFIG: PerformanceConfig = {
|
||||
enableWorkerProcessing: true,
|
||||
enableCaching: true,
|
||||
cacheTTL: 300000 // 5 minutes
|
||||
};
|
||||
|
||||
/** Default configuration for advanced features */
|
||||
export const DEFAULT_ADVANCED_CONFIG: AdvancedConfig = {
|
||||
excludePatterns: ["**/.obsidian/**", "**/node_modules/**"],
|
||||
maxFileSize: 1048576 // 1MB
|
||||
};
|
||||
|
||||
/** Default status mapping configuration */
|
||||
export const DEFAULT_STATUS_MAPPING_CONFIG: StatusMappingConfig = {
|
||||
enabled: true,
|
||||
metadataToSymbol: {
|
||||
// Completed variants
|
||||
'completed': 'x',
|
||||
'done': 'x',
|
||||
'finished': 'x',
|
||||
'complete': 'x',
|
||||
'checked': 'x',
|
||||
'resolved': 'x',
|
||||
'closed': 'x',
|
||||
'x': 'x',
|
||||
'X': 'x',
|
||||
|
||||
// In Progress variants
|
||||
'in-progress': '/',
|
||||
'in progress': '/',
|
||||
'inprogress': '/',
|
||||
'doing': '/',
|
||||
'working': '/',
|
||||
'active': '/',
|
||||
'started': '/',
|
||||
'ongoing': '/',
|
||||
'/': '/',
|
||||
'>': '/',
|
||||
|
||||
// Planned variants
|
||||
'planned': '?',
|
||||
'todo': '?',
|
||||
'pending': '?',
|
||||
'scheduled': '?',
|
||||
'queued': '?',
|
||||
'waiting': '?',
|
||||
'later': '?',
|
||||
'?': '?',
|
||||
|
||||
// Abandoned variants
|
||||
'cancelled': '-',
|
||||
'canceled': '-',
|
||||
'abandoned': '-',
|
||||
'dropped': '-',
|
||||
'skipped': '-',
|
||||
'deferred': '-',
|
||||
'wontfix': '-',
|
||||
"won't fix": '-',
|
||||
'-': '-',
|
||||
|
||||
// Not Started variants
|
||||
'not-started': ' ',
|
||||
'not started': ' ',
|
||||
'notstarted': ' ',
|
||||
'new': ' ',
|
||||
'open': ' ',
|
||||
'created': ' ',
|
||||
'unstarted': ' ',
|
||||
' ': ' '
|
||||
},
|
||||
symbolToMetadata: {
|
||||
'x': 'completed',
|
||||
'X': 'completed',
|
||||
'/': 'in-progress',
|
||||
'>': 'in-progress',
|
||||
'?': 'planned',
|
||||
'-': 'cancelled',
|
||||
' ': 'not-started'
|
||||
},
|
||||
autoDetect: true,
|
||||
caseSensitive: false
|
||||
};
|
||||
|
||||
/** Complete default FileSource configuration */
|
||||
export const DEFAULT_FILE_SOURCE_CONFIG: FileSourceConfiguration = {
|
||||
enabled: false, // Disabled by default for backward compatibility
|
||||
recognitionStrategies: {
|
||||
metadata: DEFAULT_METADATA_CONFIG,
|
||||
tags: DEFAULT_TAG_CONFIG,
|
||||
templates: DEFAULT_TEMPLATE_CONFIG,
|
||||
paths: DEFAULT_PATH_CONFIG
|
||||
},
|
||||
fileTaskProperties: DEFAULT_FILE_TASK_PROPERTIES,
|
||||
relationships: DEFAULT_RELATIONSHIPS_CONFIG,
|
||||
performance: DEFAULT_PERFORMANCE_CONFIG,
|
||||
advanced: DEFAULT_ADVANCED_CONFIG,
|
||||
statusMapping: DEFAULT_STATUS_MAPPING_CONFIG
|
||||
};
|
||||
|
||||
/**
|
||||
* FileSourceConfig - Manages FileSource configuration
|
||||
*/
|
||||
export class FileSourceConfig {
|
||||
private config: FileSourceConfiguration;
|
||||
private listeners: Array<(config: FileSourceConfiguration) => void> = [];
|
||||
|
||||
constructor(initialConfig?: Partial<FileSourceConfiguration>) {
|
||||
this.config = this.mergeWithDefaults(initialConfig || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current configuration
|
||||
*/
|
||||
getConfig(): FileSourceConfiguration {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration with partial updates
|
||||
*/
|
||||
updateConfig(updates: Partial<FileSourceConfiguration>): void {
|
||||
const newConfig = this.mergeWithDefaults(updates);
|
||||
const hasChanged = JSON.stringify(newConfig) !== JSON.stringify(this.config);
|
||||
|
||||
if (hasChanged) {
|
||||
this.config = newConfig;
|
||||
this.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to configuration changes
|
||||
*/
|
||||
onChange(listener: (config: FileSourceConfiguration) => void): () => void {
|
||||
this.listeners.push(listener);
|
||||
return () => {
|
||||
const index = this.listeners.indexOf(listener);
|
||||
if (index > -1) {
|
||||
this.listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if FileSource is enabled
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.config.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recognition strategies that are enabled
|
||||
*/
|
||||
getEnabledStrategies(): string[] {
|
||||
const strategies: string[] = [];
|
||||
const { recognitionStrategies } = this.config;
|
||||
|
||||
if (recognitionStrategies.metadata.enabled) strategies.push("metadata");
|
||||
if (recognitionStrategies.tags.enabled) strategies.push("tags");
|
||||
if (recognitionStrategies.templates.enabled) strategies.push("templates");
|
||||
if (recognitionStrategies.paths.enabled) strategies.push("paths");
|
||||
|
||||
return strategies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration and return any errors
|
||||
*/
|
||||
validateConfig(config: Partial<FileSourceConfiguration>): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validate recognition strategies
|
||||
if (config.recognitionStrategies) {
|
||||
const strategies = config.recognitionStrategies;
|
||||
|
||||
// Check if at least one strategy is enabled when FileSource is enabled
|
||||
if (config.enabled !== false) {
|
||||
const hasEnabledStrategy = Object.values(strategies).some(strategy =>
|
||||
strategy && strategy.enabled
|
||||
);
|
||||
if (!hasEnabledStrategy) {
|
||||
errors.push("At least one recognition strategy must be enabled");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate metadata strategy
|
||||
if (strategies.metadata?.taskFields?.length === 0) {
|
||||
errors.push("Metadata strategy requires at least one task field");
|
||||
}
|
||||
|
||||
// Validate tag strategy
|
||||
if (strategies.tags?.taskTags?.length === 0) {
|
||||
errors.push("Tag strategy requires at least one task tag");
|
||||
}
|
||||
|
||||
// Validate template strategy
|
||||
if (strategies.templates?.templatePaths?.length === 0) {
|
||||
errors.push("Template strategy requires at least one template path");
|
||||
}
|
||||
|
||||
// Validate path strategy
|
||||
if (strategies.paths?.taskPaths?.length === 0) {
|
||||
errors.push("Path strategy requires at least one task path");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate file task properties
|
||||
if (config.fileTaskProperties) {
|
||||
const props = config.fileTaskProperties;
|
||||
|
||||
if (props.contentSource === "custom" && !props.customContentField) {
|
||||
errors.push("Custom content source requires customContentField to be specified");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate performance config
|
||||
if (config.performance) {
|
||||
const perf = config.performance;
|
||||
|
||||
if (perf.cacheTTL && perf.cacheTTL < 0) {
|
||||
errors.push("Cache TTL must be a positive number");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate advanced config
|
||||
if (config.advanced) {
|
||||
const advanced = config.advanced;
|
||||
|
||||
if (advanced.maxFileSize && advanced.maxFileSize < 1024) {
|
||||
errors.push("Maximum file size must be at least 1KB");
|
||||
}
|
||||
|
||||
if (advanced.customRecognitionFunction) {
|
||||
// Basic validation for custom function
|
||||
try {
|
||||
// Check if it's valid JavaScript function syntax
|
||||
new Function('filePath', 'fileContent', 'fileCache', advanced.customRecognitionFunction);
|
||||
} catch (error) {
|
||||
errors.push("Custom recognition function has invalid syntax");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate status mapping config
|
||||
if (config.statusMapping?.enabled) {
|
||||
const mapping = config.statusMapping;
|
||||
|
||||
if (Object.keys(mapping.metadataToSymbol || {}).length === 0) {
|
||||
errors.push("Status mapping requires at least one metadata to symbol mapping");
|
||||
}
|
||||
|
||||
if (Object.keys(mapping.symbolToMetadata || {}).length === 0) {
|
||||
errors.push("Status mapping requires at least one symbol to metadata mapping");
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge partial configuration with defaults
|
||||
*/
|
||||
private mergeWithDefaults(partial: Partial<FileSourceConfiguration>): FileSourceConfiguration {
|
||||
return {
|
||||
enabled: partial.enabled ?? DEFAULT_FILE_SOURCE_CONFIG.enabled,
|
||||
recognitionStrategies: {
|
||||
metadata: {
|
||||
...DEFAULT_METADATA_CONFIG,
|
||||
...partial.recognitionStrategies?.metadata
|
||||
},
|
||||
tags: {
|
||||
...DEFAULT_TAG_CONFIG,
|
||||
...partial.recognitionStrategies?.tags
|
||||
},
|
||||
templates: {
|
||||
...DEFAULT_TEMPLATE_CONFIG,
|
||||
...partial.recognitionStrategies?.templates
|
||||
},
|
||||
paths: {
|
||||
...DEFAULT_PATH_CONFIG,
|
||||
...partial.recognitionStrategies?.paths
|
||||
}
|
||||
},
|
||||
fileTaskProperties: {
|
||||
...DEFAULT_FILE_TASK_PROPERTIES,
|
||||
...partial.fileTaskProperties
|
||||
},
|
||||
relationships: {
|
||||
...DEFAULT_RELATIONSHIPS_CONFIG,
|
||||
...partial.relationships
|
||||
},
|
||||
performance: {
|
||||
...DEFAULT_PERFORMANCE_CONFIG,
|
||||
...partial.performance
|
||||
},
|
||||
advanced: {
|
||||
...DEFAULT_ADVANCED_CONFIG,
|
||||
...partial.advanced
|
||||
},
|
||||
statusMapping: {
|
||||
...DEFAULT_STATUS_MAPPING_CONFIG,
|
||||
...partial.statusMapping
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all listeners of configuration changes
|
||||
*/
|
||||
private notifyListeners(): void {
|
||||
this.listeners.forEach(listener => {
|
||||
try {
|
||||
listener(this.config);
|
||||
} catch (error) {
|
||||
console.error('FileSourceConfig: Error in change listener:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a metadata status value to a task symbol
|
||||
* @param metadataValue The metadata value (e.g., "completed", "in-progress")
|
||||
* @returns The corresponding task symbol (e.g., "x", "/") or the original value if no mapping exists
|
||||
*/
|
||||
mapMetadataToSymbol(metadataValue: string): string {
|
||||
const { statusMapping } = this.config;
|
||||
|
||||
if (!statusMapping.enabled) {
|
||||
return metadataValue;
|
||||
}
|
||||
|
||||
// Handle case sensitivity
|
||||
const lookupValue = statusMapping.caseSensitive
|
||||
? metadataValue
|
||||
: metadataValue.toLowerCase();
|
||||
|
||||
// Find matching key in the mapping (case-insensitive search if needed)
|
||||
for (const [key, symbol] of Object.entries(statusMapping.metadataToSymbol)) {
|
||||
const compareKey = statusMapping.caseSensitive ? key : key.toLowerCase();
|
||||
if (compareKey === lookupValue) {
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
|
||||
// Return original value if no mapping found
|
||||
return metadataValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a task symbol to a metadata status value
|
||||
* @param symbol The task symbol (e.g., "x", "/")
|
||||
* @returns The corresponding metadata value (e.g., "completed", "in-progress") or the original symbol if no mapping exists
|
||||
*/
|
||||
mapSymbolToMetadata(symbol: string): string {
|
||||
const { statusMapping } = this.config;
|
||||
|
||||
if (!statusMapping.enabled) {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
// Direct lookup for symbols (usually case-sensitive)
|
||||
return statusMapping.symbolToMetadata[symbol] || symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a recognized status (either metadata value or symbol)
|
||||
* @param value The value to check
|
||||
* @returns True if the value is recognized as a status
|
||||
*/
|
||||
isRecognizedStatus(value: string): boolean {
|
||||
const { statusMapping } = this.config;
|
||||
|
||||
if (!statusMapping.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lookupValue = statusMapping.caseSensitive
|
||||
? value
|
||||
: value.toLowerCase();
|
||||
|
||||
// Check if it's a known metadata value
|
||||
for (const key of Object.keys(statusMapping.metadataToSymbol)) {
|
||||
const compareKey = statusMapping.caseSensitive ? key : key.toLowerCase();
|
||||
if (compareKey === lookupValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a known symbol
|
||||
return value in statusMapping.symbolToMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync status mappings with current task status configuration
|
||||
* @param taskStatuses The current task status configuration from settings
|
||||
*/
|
||||
syncWithTaskStatuses(taskStatuses: Record<string, string>): void {
|
||||
if (!this.config.statusMapping.autoDetect) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract symbols from task status configuration
|
||||
const symbolToType: Record<string, string> = {};
|
||||
|
||||
for (const [type, symbols] of Object.entries(taskStatuses)) {
|
||||
const symbolList = symbols.split('|').filter(s => s);
|
||||
for (const symbol of symbolList) {
|
||||
// Handle potential pattern like '/>' being split into '/' and '>'
|
||||
if (symbol.length === 1 || symbol === '/>') {
|
||||
if (symbol === '/>') {
|
||||
symbolToType['/'] = type;
|
||||
symbolToType['>'] = type;
|
||||
} else {
|
||||
symbolToType[symbol] = type;
|
||||
}
|
||||
} else {
|
||||
// For multi-character symbols, add each character separately
|
||||
for (const char of symbol) {
|
||||
symbolToType[char] = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update symbol to metadata mappings based on type
|
||||
const typeToMetadata: Record<string, string> = {
|
||||
'completed': 'completed',
|
||||
'inProgress': 'in-progress',
|
||||
'planned': 'planned',
|
||||
'abandoned': 'cancelled',
|
||||
'notStarted': 'not-started'
|
||||
};
|
||||
|
||||
for (const [symbol, type] of Object.entries(symbolToType)) {
|
||||
if (typeToMetadata[type]) {
|
||||
this.config.statusMapping.symbolToMetadata[symbol] = typeToMetadata[type];
|
||||
}
|
||||
}
|
||||
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a configuration preset for common use cases
|
||||
*/
|
||||
static createPreset(presetName: 'basic' | 'metadata-only' | 'tag-only' | 'full'): Partial<FileSourceConfiguration> {
|
||||
switch (presetName) {
|
||||
case 'basic':
|
||||
return {
|
||||
enabled: true,
|
||||
recognitionStrategies: {
|
||||
metadata: { ...DEFAULT_METADATA_CONFIG, enabled: true },
|
||||
tags: { ...DEFAULT_TAG_CONFIG, enabled: false },
|
||||
templates: { ...DEFAULT_TEMPLATE_CONFIG, enabled: false },
|
||||
paths: { ...DEFAULT_PATH_CONFIG, enabled: false }
|
||||
}
|
||||
};
|
||||
|
||||
case 'metadata-only':
|
||||
return {
|
||||
enabled: true,
|
||||
recognitionStrategies: {
|
||||
metadata: { ...DEFAULT_METADATA_CONFIG, enabled: true },
|
||||
tags: { ...DEFAULT_TAG_CONFIG, enabled: false },
|
||||
templates: { ...DEFAULT_TEMPLATE_CONFIG, enabled: false },
|
||||
paths: { ...DEFAULT_PATH_CONFIG, enabled: false }
|
||||
}
|
||||
};
|
||||
|
||||
case 'tag-only':
|
||||
return {
|
||||
enabled: true,
|
||||
recognitionStrategies: {
|
||||
metadata: { ...DEFAULT_METADATA_CONFIG, enabled: false },
|
||||
tags: { ...DEFAULT_TAG_CONFIG, enabled: true },
|
||||
templates: { ...DEFAULT_TEMPLATE_CONFIG, enabled: false },
|
||||
paths: { ...DEFAULT_PATH_CONFIG, enabled: false }
|
||||
}
|
||||
};
|
||||
|
||||
case 'full':
|
||||
return {
|
||||
enabled: true,
|
||||
recognitionStrategies: {
|
||||
metadata: { ...DEFAULT_METADATA_CONFIG, enabled: true },
|
||||
tags: { ...DEFAULT_TAG_CONFIG, enabled: true },
|
||||
templates: { ...DEFAULT_TEMPLATE_CONFIG, enabled: false },
|
||||
paths: { ...DEFAULT_PATH_CONFIG, enabled: false }
|
||||
}
|
||||
};
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/index.ts
11
src/index.ts
|
|
@ -267,8 +267,11 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
this.rebuildProgressManager = new RebuildProgressManager();
|
||||
|
||||
// Initialize task management systems
|
||||
if (this.settings.enableView) {
|
||||
this.loadViews();
|
||||
if (this.settings.enableIndexer) {
|
||||
// Initialize indexer-dependent features
|
||||
if (this.settings.enableView) {
|
||||
this.loadViews();
|
||||
}
|
||||
|
||||
addIcon("task-genius", getTaskGeniusIcon());
|
||||
addIcon("completed", getStatusIcon("completed"));
|
||||
|
|
@ -474,7 +477,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
}
|
||||
});
|
||||
|
||||
if (this.settings.enableView) {
|
||||
if (this.settings.enableIndexer) {
|
||||
// Check for version changes and handle rebuild if needed
|
||||
this.initializeTaskManagerWithVersionCheck().catch((error) => {
|
||||
console.error(
|
||||
|
|
@ -711,7 +714,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
},
|
||||
});
|
||||
|
||||
if (this.settings.enableView) {
|
||||
if (this.settings.enableIndexer) {
|
||||
// Add command to refresh the task index
|
||||
this.addCommand({
|
||||
id: "refresh-task-index",
|
||||
|
|
|
|||
249
src/setting.ts
249
src/setting.ts
|
|
@ -13,6 +13,7 @@ import "./styles/setting.css";
|
|||
import "./styles/setting-v2.css";
|
||||
import "./styles/beta-warning.css";
|
||||
import "./styles/settings-search.css";
|
||||
import "./styles/settings-migration.css";
|
||||
import {
|
||||
renderAboutSettingsTab,
|
||||
renderBetaTestSettingsTab,
|
||||
|
|
@ -28,6 +29,7 @@ import {
|
|||
renderProjectSettingsTab,
|
||||
renderRewardSettingsTab,
|
||||
renderTimelineSidebarSettingsTab,
|
||||
renderIndexSettingsTab,
|
||||
IcsSettingsComponent,
|
||||
} from "./components/settings";
|
||||
import { renderFileFilterSettingsTab } from "./components/settings/FileFilterSettingsTab";
|
||||
|
|
@ -55,9 +57,15 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
icon: "settings",
|
||||
category: "core",
|
||||
},
|
||||
{
|
||||
id: "index",
|
||||
name: t("Index & Sources"),
|
||||
icon: "database",
|
||||
category: "core",
|
||||
},
|
||||
{
|
||||
id: "view-settings",
|
||||
name: t("Views & Index"),
|
||||
name: t("Views"),
|
||||
icon: "layout",
|
||||
category: "core",
|
||||
},
|
||||
|
|
@ -100,7 +108,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
id: "project",
|
||||
name: t("Projects"),
|
||||
icon: "folder-open",
|
||||
category: "management",
|
||||
category: "core",
|
||||
},
|
||||
|
||||
// Workflow & Automation
|
||||
|
|
@ -209,7 +217,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
if (this.searchComponent) {
|
||||
this.searchComponent.destroy();
|
||||
}
|
||||
this.searchComponent = new SettingsSearchComponent(this, this.containerEl);
|
||||
this.searchComponent = new SettingsSearchComponent(
|
||||
this,
|
||||
this.containerEl,
|
||||
);
|
||||
}
|
||||
|
||||
// Tabs management with categories
|
||||
|
|
@ -301,7 +312,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
tab.name +
|
||||
(tab.id === "about"
|
||||
? " v" + this.plugin.manifest.version
|
||||
: "")
|
||||
: ""),
|
||||
);
|
||||
|
||||
// Add click handler
|
||||
|
|
@ -334,7 +345,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
|
||||
// Show active section, hide others
|
||||
const sections = this.containerEl.querySelectorAll(
|
||||
".settings-tab-section"
|
||||
".settings-tab-section",
|
||||
);
|
||||
sections.forEach((section) => {
|
||||
if (section.getAttribute("data-tab-id") === tabId) {
|
||||
|
|
@ -348,10 +359,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
|
||||
// Handle tab container and header visibility based on selected tab
|
||||
const tabsContainer = this.containerEl.querySelector(
|
||||
".settings-tabs-categorized-container"
|
||||
".settings-tabs-categorized-container",
|
||||
);
|
||||
const settingsHeader = this.containerEl.querySelector(
|
||||
".task-genius-settings-header"
|
||||
".task-genius-settings-header",
|
||||
);
|
||||
|
||||
if (tabId === "general") {
|
||||
|
|
@ -377,7 +388,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
tabId,
|
||||
"Active sections:",
|
||||
this.containerEl.querySelectorAll(".settings-tab-section-active")
|
||||
.length
|
||||
.length,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -389,7 +400,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
private createTabSection(tabId: string): HTMLElement {
|
||||
// Get the sections container
|
||||
const sectionsContainer = this.containerEl.querySelector(
|
||||
".settings-tab-sections"
|
||||
".settings-tab-sections",
|
||||
);
|
||||
if (!sectionsContainer) return this.containerEl;
|
||||
|
||||
|
|
@ -497,6 +508,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
const projectSection = this.createTabSection("project");
|
||||
this.displayProjectSettings(projectSection);
|
||||
|
||||
// Index Settings Tab
|
||||
const indexSection = this.createTabSection("index");
|
||||
this.displayIndexSettings(indexSection);
|
||||
|
||||
// View Settings Tab
|
||||
const viewSettingsSection = this.createTabSection("view-settings");
|
||||
this.displayViewSettings(viewSettingsSection);
|
||||
|
|
@ -585,6 +600,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
renderViewSettingsTab(this, containerEl);
|
||||
}
|
||||
|
||||
private displayIndexSettings(containerEl: HTMLElement): void {
|
||||
renderIndexSettingsTab(this, containerEl);
|
||||
}
|
||||
|
||||
private displayProjectSettings(containerEl: HTMLElement): void {
|
||||
renderProjectSettingsTab(this, containerEl);
|
||||
}
|
||||
|
|
@ -596,16 +615,14 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
() => {
|
||||
this.currentTab = "general";
|
||||
this.display();
|
||||
}
|
||||
},
|
||||
);
|
||||
icsSettingsComponent.display();
|
||||
}
|
||||
|
||||
private displayMcpSettings(containerEl: HTMLElement): void {
|
||||
renderMcpIntegrationSettingsTab(
|
||||
containerEl,
|
||||
this.plugin,
|
||||
() => this.applySettingsUpdate()
|
||||
renderMcpIntegrationSettingsTab(containerEl, this.plugin, () =>
|
||||
this.applySettingsUpdate(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -638,7 +655,9 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Main enable/disable setting
|
||||
new Setting(timerSection)
|
||||
.setName("Enable Task Timer")
|
||||
.setDesc("Enable task timer functionality for tracking time spent on tasks")
|
||||
.setDesc(
|
||||
"Enable task timer functionality for tracking time spent on tasks",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.taskTimer?.enabled || false)
|
||||
|
|
@ -649,10 +668,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
metadataDetection: {
|
||||
frontmatter: "task-timer",
|
||||
folders: [],
|
||||
tags: []
|
||||
tags: [],
|
||||
},
|
||||
timeFormat: "{h}hrs{m}mins",
|
||||
blockRefPrefix: "timer"
|
||||
blockRefPrefix: "timer",
|
||||
};
|
||||
}
|
||||
this.plugin.settings.taskTimer.enabled = value;
|
||||
|
|
@ -668,7 +687,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Metadata detection section
|
||||
const metadataSection = timerSection.createDiv();
|
||||
metadataSection.addClass("task-timer-metadata-section");
|
||||
|
||||
|
||||
const metadataHeading = metadataSection.createEl("h3");
|
||||
metadataHeading.setText("Metadata Detection");
|
||||
metadataHeading.addClass("task-timer-section-heading");
|
||||
|
|
@ -676,29 +695,45 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Frontmatter field setting
|
||||
new Setting(metadataSection)
|
||||
.setName("Frontmatter field")
|
||||
.setDesc("Field name in frontmatter to check for enabling task timer (e.g., 'task-timer: true')")
|
||||
.setDesc(
|
||||
"Field name in frontmatter to check for enabling task timer (e.g., 'task-timer: true')",
|
||||
)
|
||||
.addText((text) => {
|
||||
text
|
||||
.setValue(this.plugin.settings.taskTimer?.metadataDetection?.frontmatter || "task-timer")
|
||||
.onChange(async (value) => {
|
||||
if (this.plugin.settings.taskTimer?.metadataDetection) {
|
||||
this.plugin.settings.taskTimer.metadataDetection.frontmatter = value;
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
});
|
||||
text.setValue(
|
||||
this.plugin.settings.taskTimer?.metadataDetection
|
||||
?.frontmatter || "task-timer",
|
||||
).onChange(async (value) => {
|
||||
if (this.plugin.settings.taskTimer?.metadataDetection) {
|
||||
this.plugin.settings.taskTimer.metadataDetection.frontmatter =
|
||||
value;
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Folder paths setting
|
||||
new Setting(metadataSection)
|
||||
.setName("Folder paths")
|
||||
.setDesc("Comma-separated list of folder paths where task timer should be enabled")
|
||||
.setDesc(
|
||||
"Comma-separated list of folder paths where task timer should be enabled",
|
||||
)
|
||||
.addTextArea((textArea) => {
|
||||
textArea
|
||||
.setValue(this.plugin.settings.taskTimer?.metadataDetection?.folders?.join(", ") || "")
|
||||
.setValue(
|
||||
this.plugin.settings.taskTimer?.metadataDetection?.folders?.join(
|
||||
", ",
|
||||
) || "",
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
if (this.plugin.settings.taskTimer?.metadataDetection) {
|
||||
this.plugin.settings.taskTimer.metadataDetection.folders =
|
||||
value.split(",").map(f => f.trim()).filter(f => f);
|
||||
if (
|
||||
this.plugin.settings.taskTimer
|
||||
?.metadataDetection
|
||||
) {
|
||||
this.plugin.settings.taskTimer.metadataDetection.folders =
|
||||
value
|
||||
.split(",")
|
||||
.map((f) => f.trim())
|
||||
.filter((f) => f);
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
});
|
||||
|
|
@ -711,11 +746,21 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
.setDesc("Comma-separated list of tags that enable task timer")
|
||||
.addTextArea((textArea) => {
|
||||
textArea
|
||||
.setValue(this.plugin.settings.taskTimer?.metadataDetection?.tags?.join(", ") || "")
|
||||
.setValue(
|
||||
this.plugin.settings.taskTimer?.metadataDetection?.tags?.join(
|
||||
", ",
|
||||
) || "",
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
if (this.plugin.settings.taskTimer?.metadataDetection) {
|
||||
this.plugin.settings.taskTimer.metadataDetection.tags =
|
||||
value.split(",").map(t => t.trim()).filter(t => t);
|
||||
if (
|
||||
this.plugin.settings.taskTimer
|
||||
?.metadataDetection
|
||||
) {
|
||||
this.plugin.settings.taskTimer.metadataDetection.tags =
|
||||
value
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t);
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
});
|
||||
|
|
@ -725,7 +770,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Time format section
|
||||
const formatSection = timerSection.createDiv();
|
||||
formatSection.addClass("task-timer-format-section");
|
||||
|
||||
|
||||
const formatHeading = formatSection.createEl("h3");
|
||||
formatHeading.setText("Time Format");
|
||||
formatHeading.addClass("task-timer-section-heading");
|
||||
|
|
@ -733,36 +778,39 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Time format template setting
|
||||
new Setting(formatSection)
|
||||
.setName("Time format template")
|
||||
.setDesc("Template for displaying completed task time. Use {h} for hours, {m} for minutes, {s} for seconds")
|
||||
.setDesc(
|
||||
"Template for displaying completed task time. Use {h} for hours, {m} for minutes, {s} for seconds",
|
||||
)
|
||||
.addText((text) => {
|
||||
text
|
||||
.setValue(this.plugin.settings.taskTimer?.timeFormat || "{h}hrs{m}mins")
|
||||
.onChange(async (value) => {
|
||||
if (this.plugin.settings.taskTimer) {
|
||||
this.plugin.settings.taskTimer.timeFormat = value;
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
});
|
||||
text.setValue(
|
||||
this.plugin.settings.taskTimer?.timeFormat ||
|
||||
"{h}hrs{m}mins",
|
||||
).onChange(async (value) => {
|
||||
if (this.plugin.settings.taskTimer) {
|
||||
this.plugin.settings.taskTimer.timeFormat = value;
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Format examples
|
||||
const examplesDiv = formatSection.createDiv();
|
||||
examplesDiv.addClass("task-timer-examples");
|
||||
|
||||
|
||||
const examplesTitle = examplesDiv.createDiv();
|
||||
examplesTitle.addClass("task-timer-examples-title");
|
||||
examplesTitle.setText("Format Examples:");
|
||||
|
||||
|
||||
const examplesList = examplesDiv.createEl("ul");
|
||||
|
||||
|
||||
const examples = [
|
||||
{ format: "{h}hrs{m}mins", result: "2hrs30mins" },
|
||||
{ format: "{h}h {m}m {s}s", result: "2h 30m 45s" },
|
||||
{ format: "{h}:{m}:{s}", result: "2:30:45" },
|
||||
{ format: "({m}mins)", result: "(150mins)" }
|
||||
{ format: "({m}mins)", result: "(150mins)" },
|
||||
];
|
||||
|
||||
examples.forEach(example => {
|
||||
|
||||
examples.forEach((example) => {
|
||||
const listItem = examplesList.createEl("li");
|
||||
const codeEl = listItem.createEl("code");
|
||||
codeEl.setText(example.format);
|
||||
|
|
@ -772,7 +820,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Block reference section
|
||||
const blockRefSection = timerSection.createDiv();
|
||||
blockRefSection.addClass("task-timer-blockref-section");
|
||||
|
||||
|
||||
const blockRefHeading = blockRefSection.createEl("h3");
|
||||
blockRefHeading.setText("Block References");
|
||||
blockRefHeading.addClass("task-timer-section-heading");
|
||||
|
|
@ -780,43 +828,64 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Block reference prefix setting
|
||||
new Setting(blockRefSection)
|
||||
.setName("Block reference prefix")
|
||||
.setDesc("Prefix for generated block reference IDs (e.g., 'timer' creates ^timer-123456-7890)")
|
||||
.setDesc(
|
||||
"Prefix for generated block reference IDs (e.g., 'timer' creates ^timer-123456-7890)",
|
||||
)
|
||||
.addText((text) => {
|
||||
text
|
||||
.setValue(this.plugin.settings.taskTimer?.blockRefPrefix || "timer")
|
||||
.onChange(async (value) => {
|
||||
if (this.plugin.settings.taskTimer) {
|
||||
this.plugin.settings.taskTimer.blockRefPrefix = value;
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
});
|
||||
text.setValue(
|
||||
this.plugin.settings.taskTimer?.blockRefPrefix ||
|
||||
"timer",
|
||||
).onChange(async (value) => {
|
||||
if (this.plugin.settings.taskTimer) {
|
||||
this.plugin.settings.taskTimer.blockRefPrefix =
|
||||
value;
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Commands section
|
||||
const commandsSection = timerSection.createDiv();
|
||||
commandsSection.addClass("task-timer-commands-section");
|
||||
|
||||
|
||||
const commandsHeading = commandsSection.createEl("h3");
|
||||
commandsHeading.setText("Data Management");
|
||||
commandsHeading.addClass("task-timer-section-heading");
|
||||
|
||||
const commandsDesc = commandsSection.createDiv();
|
||||
commandsDesc.addClass("task-timer-commands-desc");
|
||||
|
||||
|
||||
const descParagraph = commandsDesc.createEl("p");
|
||||
descParagraph.setText("Use the command palette to access timer data management:");
|
||||
|
||||
descParagraph.setText(
|
||||
"Use the command palette to access timer data management:",
|
||||
);
|
||||
|
||||
const commandsList = commandsDesc.createEl("ul");
|
||||
|
||||
|
||||
const commands = [
|
||||
{ name: "Export task timer data", desc: "Export all timer data to JSON" },
|
||||
{ name: "Import task timer data", desc: "Import timer data from JSON file" },
|
||||
{ name: "Export task timer data (YAML)", desc: "Export to YAML format" },
|
||||
{ name: "Create task timer backup", desc: "Create a backup of active timers" },
|
||||
{ name: "Show task timer statistics", desc: "Display timer usage statistics" }
|
||||
{
|
||||
name: "Export task timer data",
|
||||
desc: "Export all timer data to JSON",
|
||||
},
|
||||
{
|
||||
name: "Import task timer data",
|
||||
desc: "Import timer data from JSON file",
|
||||
},
|
||||
{
|
||||
name: "Export task timer data (YAML)",
|
||||
desc: "Export to YAML format",
|
||||
},
|
||||
{
|
||||
name: "Create task timer backup",
|
||||
desc: "Create a backup of active timers",
|
||||
},
|
||||
{
|
||||
name: "Show task timer statistics",
|
||||
desc: "Display timer usage statistics",
|
||||
},
|
||||
];
|
||||
|
||||
commands.forEach(command => {
|
||||
|
||||
commands.forEach((command) => {
|
||||
const listItem = commandsList.createEl("li");
|
||||
const strongEl = listItem.createEl("strong");
|
||||
strongEl.setText(command.name);
|
||||
|
|
@ -839,12 +908,14 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
const warningEl = experimentalSection.createDiv();
|
||||
warningEl.addClass("experimental-warning");
|
||||
warningEl.createEl("strong").setText("⚠️ Warning: ");
|
||||
warningEl.appendText("These features are experimental and may not be stable. Use at your own risk.");
|
||||
warningEl.appendText(
|
||||
"These features are experimental and may not be stable. Use at your own risk.",
|
||||
);
|
||||
|
||||
// Dataflow Settings
|
||||
const dataflowSection = experimentalSection.createDiv();
|
||||
dataflowSection.addClass("experimental-dataflow-section");
|
||||
|
||||
|
||||
const dataflowHeading = dataflowSection.createEl("h3");
|
||||
dataflowHeading.setText("New Dataflow Architecture");
|
||||
dataflowHeading.addClass("experimental-subsection-heading");
|
||||
|
|
@ -852,24 +923,34 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Enable dataflow setting
|
||||
new Setting(dataflowSection)
|
||||
.setName("Enable Dataflow Architecture")
|
||||
.setDesc("Enable the new dataflow-based task processing system. This is an experimental feature that replaces the current TaskManager.")
|
||||
.setDesc(
|
||||
"Enable the new dataflow-based task processing system. This is an experimental feature that replaces the current TaskManager.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.experimental?.dataflowEnabled || false)
|
||||
.setValue(
|
||||
this.plugin.settings.experimental?.dataflowEnabled ||
|
||||
false,
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
if (!this.plugin.settings.experimental) {
|
||||
this.plugin.settings.experimental = {
|
||||
dataflowEnabled: false
|
||||
dataflowEnabled: false,
|
||||
};
|
||||
}
|
||||
this.plugin.settings.experimental.dataflowEnabled = value;
|
||||
this.plugin.settings.experimental.dataflowEnabled =
|
||||
value;
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Show restart notice
|
||||
if (value) {
|
||||
const restartNotice = dataflowSection.createDiv();
|
||||
restartNotice.addClass("experimental-restart-notice");
|
||||
restartNotice.setText("⚠️ A restart may be required for the dataflow architecture to take full effect.");
|
||||
restartNotice.addClass(
|
||||
"experimental-restart-notice",
|
||||
);
|
||||
restartNotice.setText(
|
||||
"⚠️ A restart may be required for the dataflow architecture to take full effect.",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -877,6 +958,8 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Dataflow info
|
||||
const infoEl = dataflowSection.createDiv();
|
||||
infoEl.addClass("experimental-info");
|
||||
infoEl.setText("The new dataflow architecture provides improved performance and scalability for task processing. It includes better caching, worker-based processing, and more efficient data structures.");
|
||||
infoEl.setText(
|
||||
"The new dataflow architecture provides improved performance and scalability for task processing. It includes better caching, worker-based processing, and more efficient data structures.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1535,7 +1535,6 @@
|
|||
}
|
||||
|
||||
.task-genius-add-format-btn:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1969,7 +1969,24 @@ const translations = {
|
|||
"Quick capture configured": "Quick capture configured",
|
||||
"Workflow settings enabled": "Workflow settings enabled",
|
||||
"Advanced features enabled": "Advanced features enabled",
|
||||
"File parsing customized": "File parsing customized"
|
||||
"File parsing customized": "File parsing customized",
|
||||
|
||||
// Settings Migration
|
||||
"Settings Migration Required": "Settings Migration Required",
|
||||
"Task Genius has detected duplicate settings that can cause confusion. ": "Task Genius has detected duplicate settings that can cause confusion. ",
|
||||
"We recommend migrating to the new unified FileSource system for better organization.": "We recommend migrating to the new unified FileSource system for better organization.",
|
||||
"Auto-Migrate Settings": "Auto-Migrate Settings",
|
||||
"Settings migrated successfully! ": "Settings migrated successfully! ",
|
||||
" changes applied.": " changes applied.",
|
||||
"Migration failed. Please check console for details.": "Migration failed. Please check console for details.",
|
||||
"Learn More": "Learn More",
|
||||
|
||||
// FileSource
|
||||
"FileSource": "FileSource",
|
||||
"FileSource Configuration": "FileSource Configuration",
|
||||
"Go to FileSource Settings": "Go to FileSource Settings",
|
||||
"Note: This setting will be deprecated in favor of the unified FileSource system.": "Note: This setting will be deprecated in favor of the unified FileSource system.",
|
||||
"Note: FileSource settings have been moved to a dedicated tab for better organization and to avoid duplication with file metadata parsing.": "Note: FileSource settings have been moved to a dedicated tab for better organization and to avoid duplication with file metadata parsing."
|
||||
};
|
||||
|
||||
export default translations;
|
||||
|
|
|
|||
226
src/types/file-source.d.ts
vendored
Normal file
226
src/types/file-source.d.ts
vendored
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
/**
|
||||
* FileSource Type Definitions
|
||||
*
|
||||
* Core types for the FileSource feature that enables files to be recognized
|
||||
* as tasks based on their metadata properties.
|
||||
*/
|
||||
|
||||
import type { StandardTaskMetadata } from "./task";
|
||||
|
||||
/** Recognition strategies for identifying files as tasks */
|
||||
export type RecognitionStrategy = "metadata" | "tag" | "template" | "path";
|
||||
|
||||
/** Extended metadata for file source tasks */
|
||||
export interface FileSourceTaskMetadata extends StandardTaskMetadata {
|
||||
/** Task source identifier */
|
||||
source: "file-source";
|
||||
|
||||
/** Recognition strategy that identified this file as a task */
|
||||
recognitionStrategy: RecognitionStrategy;
|
||||
|
||||
/** Recognition criteria that matched */
|
||||
recognitionCriteria: string;
|
||||
|
||||
/** File creation/modification timestamps */
|
||||
fileTimestamps: {
|
||||
created: number;
|
||||
modified: number;
|
||||
};
|
||||
|
||||
/** Child task relationships */
|
||||
childTasks: string[]; // IDs of tasks within this file
|
||||
|
||||
/** Project relationship (if file is also a project) */
|
||||
projectData?: {
|
||||
isProject: boolean;
|
||||
projectName?: string;
|
||||
projectType?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/** Configuration for metadata-based recognition */
|
||||
export interface MetadataRecognitionConfig {
|
||||
enabled: boolean;
|
||||
/** Metadata fields that make a file a task */
|
||||
taskFields: string[];
|
||||
/** Require all fields or any field */
|
||||
requireAllFields: boolean;
|
||||
}
|
||||
|
||||
/** Configuration for tag-based recognition */
|
||||
export interface TagRecognitionConfig {
|
||||
enabled: boolean;
|
||||
/** Tags that make a file a task */
|
||||
taskTags: string[];
|
||||
/** Tag matching mode */
|
||||
matchMode: "exact" | "prefix" | "contains";
|
||||
}
|
||||
|
||||
/** Configuration for template-based recognition */
|
||||
export interface TemplateRecognitionConfig {
|
||||
enabled: boolean;
|
||||
/** Template files or patterns */
|
||||
templatePaths: string[];
|
||||
/** Check template metadata */
|
||||
checkTemplateMetadata: boolean;
|
||||
}
|
||||
|
||||
/** Configuration for path-based recognition */
|
||||
export interface PathRecognitionConfig {
|
||||
enabled: boolean;
|
||||
/** Path patterns that contain file tasks */
|
||||
taskPaths: string[];
|
||||
/** Pattern matching mode */
|
||||
matchMode: "prefix" | "regex" | "glob";
|
||||
}
|
||||
|
||||
/** Recognition strategies configuration */
|
||||
export interface RecognitionStrategiesConfig {
|
||||
metadata: MetadataRecognitionConfig;
|
||||
tags: TagRecognitionConfig;
|
||||
templates: TemplateRecognitionConfig;
|
||||
paths: PathRecognitionConfig;
|
||||
}
|
||||
|
||||
/** File task properties configuration */
|
||||
export interface FileTaskPropertiesConfig {
|
||||
/** Default task content source */
|
||||
contentSource: "filename" | "title" | "h1" | "custom";
|
||||
/** Custom content field (if contentSource is "custom") */
|
||||
customContentField?: string;
|
||||
/** Strip file extension from content */
|
||||
stripExtension: boolean;
|
||||
/** Default status for new file tasks */
|
||||
defaultStatus: string;
|
||||
/** Default priority for new file tasks */
|
||||
defaultPriority?: number;
|
||||
}
|
||||
|
||||
/** Relationship configuration */
|
||||
export interface RelationshipsConfig {
|
||||
/** Enable file-task to child-task relationships */
|
||||
enableChildRelationships: boolean;
|
||||
/** Inherit metadata from file to child tasks */
|
||||
enableMetadataInheritance: boolean;
|
||||
/** Metadata fields to inherit */
|
||||
inheritanceFields: string[];
|
||||
}
|
||||
|
||||
/** Performance configuration */
|
||||
export interface PerformanceConfig {
|
||||
/** Enable worker processing for file tasks */
|
||||
enableWorkerProcessing: boolean;
|
||||
/** Cache file task results */
|
||||
enableCaching: boolean;
|
||||
/** Cache TTL in milliseconds */
|
||||
cacheTTL: number;
|
||||
}
|
||||
|
||||
/** Advanced configuration */
|
||||
export interface AdvancedConfig {
|
||||
/** Custom recognition function */
|
||||
customRecognitionFunction?: string;
|
||||
/** Exclude patterns */
|
||||
excludePatterns: string[];
|
||||
/** Maximum file size to process (bytes) */
|
||||
maxFileSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for mapping between file metadata status values and task status symbols
|
||||
*/
|
||||
export interface StatusMappingConfig {
|
||||
/** Enable status mapping between metadata values and symbols */
|
||||
enabled: boolean;
|
||||
|
||||
/** Map from human-readable metadata values to task status symbols
|
||||
* e.g., "completed" → "x", "in-progress" → "/" */
|
||||
metadataToSymbol: Record<string, string>;
|
||||
|
||||
/** Map from task status symbols to preferred metadata values
|
||||
* e.g., "x" → "completed", "/" → "in-progress" */
|
||||
symbolToMetadata: Record<string, string>;
|
||||
|
||||
/** Auto-detect common status patterns */
|
||||
autoDetect: boolean;
|
||||
|
||||
/** Case sensitivity for status value matching */
|
||||
caseSensitive: boolean;
|
||||
}
|
||||
|
||||
/** Main FileSource configuration interface */
|
||||
export interface FileSourceConfiguration {
|
||||
/** Enable FileSource feature */
|
||||
enabled: boolean;
|
||||
|
||||
/** Recognition strategies */
|
||||
recognitionStrategies: RecognitionStrategiesConfig;
|
||||
|
||||
/** File task properties */
|
||||
fileTaskProperties: FileTaskPropertiesConfig;
|
||||
|
||||
/** Relationship configuration */
|
||||
relationships: RelationshipsConfig;
|
||||
|
||||
/** Performance configuration */
|
||||
performance: PerformanceConfig;
|
||||
|
||||
/** Advanced configuration */
|
||||
advanced: AdvancedConfig;
|
||||
|
||||
/** Status mapping configuration */
|
||||
statusMapping: StatusMappingConfig;
|
||||
}
|
||||
|
||||
/** Statistics interface for FileSource */
|
||||
export interface FileSourceStats {
|
||||
/** Whether FileSource is initialized */
|
||||
initialized: boolean;
|
||||
/** Number of files being tracked as tasks */
|
||||
trackedFileCount: number;
|
||||
/** Breakdown by recognition strategy */
|
||||
recognitionBreakdown: Record<RecognitionStrategy, number>;
|
||||
/** Last update timestamp */
|
||||
lastUpdate: number;
|
||||
/** Last update sequence number */
|
||||
lastUpdateSeq: number;
|
||||
}
|
||||
|
||||
/** Interface for recognition strategy implementations */
|
||||
export interface RecognitionStrategyInterface {
|
||||
/** Strategy name */
|
||||
name: RecognitionStrategy;
|
||||
|
||||
/** Check if file matches this strategy */
|
||||
matches(filePath: string, fileContent: string, fileCache: any): boolean;
|
||||
|
||||
/** Extract task metadata from file */
|
||||
extractMetadata(filePath: string, fileContent: string, fileCache: any): Partial<FileSourceTaskMetadata>;
|
||||
|
||||
/** Get strategy configuration */
|
||||
getConfig(): any;
|
||||
|
||||
/** Update strategy configuration */
|
||||
updateConfig(config: any): void;
|
||||
}
|
||||
|
||||
/** Update decision for change detection */
|
||||
export interface UpdateDecision {
|
||||
update: boolean;
|
||||
reason: 'not-a-file-task' | 'task-status-changed' |
|
||||
'task-properties-changed' | 'children-structure-changed' |
|
||||
'no-relevant-changes';
|
||||
details?: any;
|
||||
}
|
||||
|
||||
/** File task cache structure for change detection */
|
||||
export interface FileTaskCache {
|
||||
/** Whether file is tracked as a task */
|
||||
fileTaskExists: boolean;
|
||||
/** Hash of frontmatter for quick property change detection */
|
||||
frontmatterHash: string;
|
||||
/** Set of child task IDs for structure tracking */
|
||||
childTaskIds: Set<string>;
|
||||
/** Last updated timestamp */
|
||||
lastUpdated: number;
|
||||
}
|
||||
78
styles.css
78
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue